discourse/script/i18n_lint.rb
Jarek Radosz a60e26e799
DEV: Clean up and refactor CI workflow(s) (#12144)
Includes:

* DEV: Remove external plugin linting (that's covered by CI in their repositories)
* DEV: Move lint stages to a separate workflow (partial de-`if`-ication of workflows)
* DEV: Run CI on `main` branch too
* DEV: Update postgres to 13
* DEV: Update redis to 6.x

Other changes:
* DEV: Remove matrix.os
* DEV: Remove env.BUILD_TYPE
* DEV: Remove env.TARGET
* DEV: Rename `build_types` config option to `build_type`
* DEV: Lowercase `target` and `build_type` names
* DEV: Rename `ci` to `tests`
* DEV: Rename `lint` to `linting`
* DEV: Lower the wizard qunit timeout (30 min -> 10)
* DEV: Ruby version is no longer configurable
* DEV: Run plugin tests only in the `plugins` target
* DEV: Use binstubs where applicable
* DEV: We don't open PRs to `tests-passed`
2021-02-22 10:28:32 +01:00

136 lines
3.9 KiB
Ruby
Executable File

# frozen_string_literal: true
require 'colored2'
require 'psych'
class I18nLinter
def initialize(filenames_or_patterns)
@filenames = filenames_or_patterns.map { |fp| Dir[fp] }.flatten
@errors = {}
end
def run
has_errors = false
@filenames.each do |filename|
validator = LocaleFileValidator.new(filename)
if validator.has_errors?
validator.print_errors
has_errors = true
end
end
exit 1 if has_errors
end
end
class LocaleFileValidator
ERROR_MESSAGES = {
invalid_relative_links: "The following keys have relative links, but do not start with %{base_url} or %{base_path}:",
invalid_relative_image_sources: "The following keys have relative image sources, but do not start with %{base_url} or %{base_path}:",
invalid_interpolation_key_format: "The following keys use {{key}} instead of %{key} for interpolation keys:",
wrong_pluralization_keys: "Pluralized strings must have only the sub-keys 'one' and 'other'.\nThe following keys have missing or additional keys:",
invalid_one_keys: "The following keys contain the number 1 instead of the interpolation key %{count}:",
invalid_message_format_one_key: "The following keys use 'one {1 foo}' instead of the generic 'one {# foo}':",
}
PLURALIZATION_KEYS = ['zero', 'one', 'two', 'few', 'many', 'other']
ENGLISH_KEYS = ['one', 'other']
def initialize(filename)
@filename = filename
@errors = {}
end
def has_errors?
yaml = Psych.safe_load(File.read(@filename), aliases: true)
yaml = yaml[yaml.keys.first]
validate_pluralizations(yaml)
validate_content(yaml)
@errors.any? { |_, value| value.any? }
end
def print_errors
puts "", "Errors in #{@filename}".red
@errors.each do |type, keys|
next if keys.empty?
ERROR_MESSAGES[type].split("\n").each { |msg| puts " #{msg}" }
keys.each { |key| puts " * #{key}" }
end
end
private
def each_translation(hash, parent_key = '', &block)
hash.each do |key, value|
current_key = parent_key.empty? ? key : "#{parent_key}.#{key}"
if Hash === value
each_translation(value, current_key, &block)
else
yield(current_key, value.to_s)
end
end
end
def validate_content(yaml)
@errors[:invalid_relative_links] = []
@errors[:invalid_relative_image_sources] = []
@errors[:invalid_interpolation_key_format] = []
@errors[:invalid_message_format_one_key] = []
each_translation(yaml) do |key, value|
if value.match?(/href\s*=\s*["']\/[^\/]|\]\(\/[^\/]/i)
@errors[:invalid_relative_links] << key
end
if value.match?(/src\s*=\s*["']\/[^\/]/i)
@errors[:invalid_relative_image_sources] << key
end
if value.match?(/{{.+?}}/) && !key.end_with?("_MF")
@errors[:invalid_interpolation_key_format] << key
end
if key.end_with?("_MF") && value.match?(/one {.*?1.*?}/)
@errors[:invalid_message_format_one_key] << key
end
end
end
def each_pluralization(hash, parent_key = '', &block)
hash.each do |key, value|
if Hash === value
current_key = parent_key.empty? ? key : "#{parent_key}.#{key}"
each_pluralization(value, current_key, &block)
elsif PLURALIZATION_KEYS.include? key
yield(parent_key, hash)
end
end
end
def validate_pluralizations(yaml)
@errors[:wrong_pluralization_keys] = []
@errors[:invalid_one_keys] = []
each_pluralization(yaml) do |key, hash|
# ignore errors from some ActiveRecord messages
next if key.include?("messages.restrict_dependent_destroy")
@errors[:wrong_pluralization_keys] << key if hash.keys.sort != ENGLISH_KEYS
one_value = hash['one']
if one_value && one_value.include?('1') && !one_value.match?(/%{count}|{{count}}/)
@errors[:invalid_one_keys] << key
end
end
end
end
I18nLinter.new(ARGV).run