summaryrefslogtreecommitdiff
path: root/lib/gitlab/po_linter.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/gitlab/po_linter.rb')
-rw-r--r--lib/gitlab/po_linter.rb141
1 files changed, 141 insertions, 0 deletions
diff --git a/lib/gitlab/po_linter.rb b/lib/gitlab/po_linter.rb
new file mode 100644
index 00000000000..abf048815a1
--- /dev/null
+++ b/lib/gitlab/po_linter.rb
@@ -0,0 +1,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