summaryrefslogtreecommitdiff
path: root/lib/gitlab/po_linter.rb
blob: abf048815a1db30ea2a2a437c7d35693576f6c8b (plain)
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