diff options
Diffstat (limited to 'lib/gitlab/po_linter.rb')
-rw-r--r-- | lib/gitlab/po_linter.rb | 141 |
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 |