Move PoLinter into Gitlab::I18n

This commit is contained in:
Bob Van Landuyt 2017-08-25 09:04:50 +02:00
parent 49b3819477
commit 0fa0ed7d85
4 changed files with 196 additions and 194 deletions

View file

@ -0,0 +1,193 @@
require 'simple_po_parser'
module Gitlab
module I18n
class PoLinter
attr_reader :po_path, :entries, :locale
VARIABLE_REGEX = /%{\w*}|%[a-z]/.freeze
def initialize(po_path, locale = I18n.locale.to_s)
@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 = validate_entry(entry)
errors[join_message(entry[:msgid])] = errors_for_entry if errors_for_entry.any?
end
errors
end
def validate_entry(entry)
errors = []
validate_flags(errors, entry)
validate_variables(errors, entry)
validate_newlines(errors, entry)
errors
end
def validate_newlines(errors, entry)
message_id = join_message(entry[:msgid])
if entry[:msgid].is_a?(Array)
errors << "<#{message_id}> is defined over multiple lines, this breaks some tooling."
end
if translations_in_entry(entry).any? { |translation| translation.is_a?(Array) }
errors << "<#{message_id}> has translations defined over multiple lines, this breaks some tooling."
end
end
def validate_variables(errors, entry)
if entry[:msgid_plural].present?
validate_variables_in_message(errors, entry[:msgid], entry['msgstr[0]'])
# Validate all plurals
entry.keys.select { |key_name| key_name =~ /msgstr\[[1-9]\]/ }.each do |plural_key|
validate_variables_in_message(errors, entry[:msgid_plural], entry[plural_key])
end
else
validate_variables_in_message(errors, entry[:msgid], entry[:msgstr])
end
end
def validate_variables_in_message(errors, message_id, message_translation)
message_id = join_message(message_id)
required_variables = message_id.scan(VARIABLE_REGEX)
validate_unnamed_variables(errors, required_variables)
validate_translation(errors, message_id, required_variables)
validate_variable_usage(errors, message_translation, required_variables)
end
def validate_translation(errors, message_id, used_variables)
variables = fill_in_variables(used_variables)
begin
Gitlab::I18n.with_locale(locale) do
translated = if message_id.include?('|')
FastGettext::Translation.s_(message_id)
else
FastGettext::Translation._(message_id)
end
translated % variables
end
# `sprintf` could raise an `ArgumentError` when invalid passing something
# other than a Hash when using named variables
#
# `sprintf` could raise `TypeError` when passing a wrong type when using
# unnamed variables
#
# FastGettext::Translation could raise `RuntimeError` (raised as a string),
# or as subclassess `NoTextDomainConfigured` & `InvalidFormat`
#
# `FastGettext::Translation` could raise `ArgumentError` as subclassess
# `InvalidEncoding`, `IllegalSequence` & `InvalidCharacter`
rescue ArgumentError, TypeError, RuntimeError => e
errors << "Failure translating to #{locale} with #{variables}: #{e.message}"
end
end
def fill_in_variables(variables)
if variables.empty?
[]
elsif variables.any? { |variable| unnamed_variable?(variable) }
variables.map do |variable|
variable == '%d' ? Random.rand(1000) : Gitlab::Utils.random_string
end
else
variables.inject({}) do |hash, variable|
variable_name = variable[/\w+/]
hash[variable_name] = Gitlab::Utils.random_string
hash
end
end
end
def validate_unnamed_variables(errors, variables)
if variables.size > 1 && variables.any? { |variable_name| unnamed_variable?(variable_name) }
errors << 'is combining multiple unnamed variables'
end
end
def validate_variable_usage(errors, translation, required_variables)
translation = join_message(translation)
# We don't need to validate when the message is empty.
# Translations could fallback to the default, or we could be validating a
# language that does not have plurals.
return if translation.empty?
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
def translations_in_entry(entry)
if entry[:msgid_plural].present?
entry.fetch_values(*plural_translation_keys_in_entry(entry))
else
[entry[:msgstr]]
end
end
def plural_translation_keys_in_entry(entry)
entry.keys.select { |key| key =~ /msgstr\[\d*\]/ }
end
end
end
end

View file

@ -1,191 +0,0 @@
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 = I18n.locale.to_s)
@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 = validate_entry(entry)
errors[join_message(entry[:msgid])] = errors_for_entry if errors_for_entry.any?
end
errors
end
def validate_entry(entry)
errors = []
validate_flags(errors, entry)
validate_variables(errors, entry)
validate_newlines(errors, entry)
errors
end
def validate_newlines(errors, entry)
message_id = join_message(entry[:msgid])
if entry[:msgid].is_a?(Array)
errors << "<#{message_id}> is defined over multiple lines, this breaks some tooling."
end
if translations_in_entry(entry).any? { |translation| translation.is_a?(Array) }
errors << "<#{message_id}> has translations defined over multiple lines, this breaks some tooling."
end
end
def validate_variables(errors, entry)
if entry[:msgid_plural].present?
validate_variables_in_message(errors, entry[:msgid], entry['msgstr[0]'])
# Validate all plurals
entry.keys.select { |key_name| key_name =~ /msgstr\[[1-9]\]/ }.each do |plural_key|
validate_variables_in_message(errors, entry[:msgid_plural], entry[plural_key])
end
else
validate_variables_in_message(errors, entry[:msgid], entry[:msgstr])
end
end
def validate_variables_in_message(errors, message_id, message_translation)
message_id = join_message(message_id)
required_variables = message_id.scan(VARIABLE_REGEX)
validate_unnamed_variables(errors, required_variables)
validate_translation(errors, message_id, required_variables)
validate_variable_usage(errors, message_translation, required_variables)
end
def validate_translation(errors, message_id, used_variables)
variables = fill_in_variables(used_variables)
begin
Gitlab::I18n.with_locale(locale) do
translated = if message_id.include?('|')
FastGettext::Translation.s_(message_id)
else
FastGettext::Translation._(message_id)
end
translated % variables
end
# `sprintf` could raise an `ArgumentError` when invalid passing something
# other than a Hash when using named variables
#
# `sprintf` could raise `TypeError` when passing a wrong type when using
# unnamed variables
#
# FastGettext::Translation could raise `RuntimeError` (raised as a string),
# or as subclassess `NoTextDomainConfigured` & `InvalidFormat`
#
# `FastGettext::Translation` could raise `ArgumentError` as subclassess
# `InvalidEncoding`, `IllegalSequence` & `InvalidCharacter`
rescue ArgumentError, TypeError, RuntimeError => e
errors << "Failure translating to #{locale} with #{variables}: #{e.message}"
end
end
def fill_in_variables(variables)
if variables.empty?
[]
elsif variables.any? { |variable| unnamed_variable?(variable) }
variables.map do |variable|
variable == '%d' ? Random.rand(1000) : Gitlab::Utils.random_string
end
else
variables.inject({}) do |hash, variable|
variable_name = variable[/\w+/]
hash[variable_name] = Gitlab::Utils.random_string
hash
end
end
end
def validate_unnamed_variables(errors, variables)
if variables.size > 1 && variables.any? { |variable_name| unnamed_variable?(variable_name) }
errors << 'is combining multiple unnamed variables'
end
end
def validate_variable_usage(errors, translation, required_variables)
translation = join_message(translation)
# We don't need to validate when the message is empty.
# Translations could fallback to the default, or we could be validating a
# language that does not have plurals.
return if translation.empty?
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
def translations_in_entry(entry)
if entry[:msgid_plural].present?
entry.fetch_values(*plural_translation_keys_in_entry(entry))
else
[entry[:msgstr]]
end
end
def plural_translation_keys_in_entry(entry)
entry.keys.select { |key| key =~ /msgstr\[\d*\]/ }
end
end
end

View file

@ -28,11 +28,11 @@ namespace :gettext do
linters = files.map do |file|
locale = File.basename(File.dirname(file))
Gitlab::PoLinter.new(file, locale)
Gitlab::I18n::PoLinter.new(file, locale)
end
pot_file = Rails.root.join('locale/gitlab.pot')
linters.unshift(Gitlab::PoLinter.new(pot_file))
linters.unshift(Gitlab::I18n::PoLinter.new(pot_file))
failed_linters = linters.select { |linter| linter.errors.any? }

View file

@ -1,6 +1,6 @@
require 'spec_helper'
describe Gitlab::PoLinter do
describe Gitlab::I18n::PoLinter do
let(:linter) { described_class.new(po_path) }
let(:po_path) { 'spec/fixtures/valid.po' }