1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
|
require 'simple_po_parser'
module Gitlab
class PoLinter
attr_reader :po_path, :entries, :locale
VARIABLE_REGEX = /%{\w*}|%[a-z]/.freeze
def initialize(po_path, locale = 'en')
@po_path = po_path
@locale = locale
end
def errors
@errors ||= validate_po
end
def validate_po
if parse_error = parse_po
return 'PO-syntax errors' => [parse_error]
end
validate_entries
end
def parse_po
@entries = SimplePoParser.parse(po_path)
nil
rescue SimplePoParser::ParserError => e
@entries = []
e.message
end
def validate_entries
errors = {}
entries.each do |entry|
# Skip validation of metadata
next if entry[:msgid].empty?
errors_for_entry = errors_for_entry(entry)
errors[join_message(entry[:msgid])] = errors_for_entry if errors_for_entry.any?
end
errors
end
def errors_for_entry(entry)
errors = []
validate_flags(errors, entry)
validate_variables(errors, entry)
errors
end
def validate_variables(errors, entry)
if entry[:msgid_plural].present?
validate_variables_in_message(errors, entry[:msgid], entry['msgstr[0]'])
validate_variables_in_message(errors, entry[:msgid_plural], entry['msgstr[1]'])
else
validate_variables_in_message(errors, entry[:msgid], entry[:msgstr])
end
end
def validate_variables_in_message(errors, message_id, message_translation)
# An empty translation is fine, we fall back to English
return unless message_translation.present?
message_id = join_message(message_id)
message_translation = join_message(message_translation)
used_variables = message_id.scan(VARIABLE_REGEX)
validate_unnamed_variables(errors, used_variables)
return if message_translation.empty?
validate_variable_usage(errors, message_translation, used_variables)
validate_translation(errors, message_id, used_variables)
end
def validate_translation(errors, message_id, used_variables)
variables = fill_in_variables(used_variables)
Gitlab::I18n.with_locale(locale) do
translated = message_id.include?('|') ? s_(message_id) : _(message_id)
translated % variables
end
rescue => e
errors << "Failure translating to #{locale} with #{variables}: #{e.message}"
end
def fill_in_variables(variables)
if variables.empty?
[]
elsif variables.any? { |variable| unnamed_variable?(variable) }
variables.map { |variable| variable == '%d' ? Random.rand : SecureRandom.hex }
else
variables.inject({}) do |hash, variable|
variable_name = variable.match(/[^%{].*[^}]/).to_s
hash[variable_name] = SecureRandom.hex
hash
end
end
end
def validate_unnamed_variables(errors, variables)
if variables.any? { |variable_name| unnamed_variable?(variable_name) } && variables.size > 1
errors << 'is combining multiple unnamed variables'
end
end
def validate_variable_usage(errors, translation, required_variables)
found_variables = translation.scan(VARIABLE_REGEX)
missing_variables = required_variables - found_variables
if missing_variables.any?
errors << "<#{translation}> is missing: [#{missing_variables.to_sentence}]"
end
unknown_variables = found_variables - required_variables
if unknown_variables.any?
errors << "<#{translation}> is using unknown variables: [#{unknown_variables.to_sentence}]"
end
end
def unnamed_variable?(variable_name)
!variable_name.start_with?('%{')
end
def validate_flags(errors, entry)
if flag = entry[:flag]
errors << "is marked #{flag}"
end
end
def join_message(message)
Array(message).join
end
end
end
|