Merge branch 'bvl-validate-po-files' into 'master'
Validate PO files in static analysis See merge request !13000
This commit is contained in:
commit
dceb2112d2
32 changed files with 2413 additions and 13 deletions
2
Gemfile
2
Gemfile
|
@ -349,6 +349,8 @@ group :development, :test do
|
|||
gem 'activerecord_sane_schema_dumper', '0.2'
|
||||
|
||||
gem 'stackprof', '~> 0.2.10', require: false
|
||||
|
||||
gem 'simple_po_parser', '~> 1.1.2', require: false
|
||||
end
|
||||
|
||||
group :test do
|
||||
|
|
|
@ -833,6 +833,7 @@ GEM
|
|||
faraday (~> 0.9)
|
||||
jwt (~> 1.5)
|
||||
multi_json (~> 1.10)
|
||||
simple_po_parser (1.1.2)
|
||||
simplecov (0.14.1)
|
||||
docile (~> 1.1.0)
|
||||
json (>= 1.8, < 3)
|
||||
|
@ -1145,6 +1146,7 @@ DEPENDENCIES
|
|||
sidekiq (~> 5.0)
|
||||
sidekiq-cron (~> 0.6.0)
|
||||
sidekiq-limit_fetch (~> 3.4)
|
||||
simple_po_parser (~> 1.1.2)
|
||||
simplecov (~> 0.14.0)
|
||||
slack-notifier (~> 1.5.1)
|
||||
spinach-rails (~> 0.2.1)
|
||||
|
|
4
changelogs/unreleased/bvl-validate-po-files.yml
Normal file
4
changelogs/unreleased/bvl-validate-po-files.yml
Normal file
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Validate PO-files in static analysis
|
||||
merge_request: 13000
|
||||
author:
|
|
@ -1,4 +1,7 @@
|
|||
FastGettext.add_text_domain 'gitlab', path: File.join(Rails.root, 'locale'), type: :po
|
||||
FastGettext.add_text_domain 'gitlab',
|
||||
path: File.join(Rails.root, 'locale'),
|
||||
type: :po,
|
||||
ignore_fuzzy: true
|
||||
FastGettext.default_text_domain = 'gitlab'
|
||||
FastGettext.default_available_locales = Gitlab::I18n.available_locales
|
||||
FastGettext.default_locale = :en
|
||||
|
|
|
@ -138,6 +138,47 @@ translations. There's no need to generate `.po` files.
|
|||
Translations that aren't used in the source code anymore will be marked with
|
||||
`~#`; these can be removed to keep our translation files clutter-free.
|
||||
|
||||
### Validating PO files
|
||||
|
||||
To make sure we keep our translation files up to date, there's a linter that is
|
||||
running on CI as part of the `static-analysis` job.
|
||||
|
||||
To lint the adjustments in PO files locally you can run `rake gettext:lint`.
|
||||
|
||||
The linter will take the following into account:
|
||||
|
||||
- Valid PO-file syntax
|
||||
- Variable usage
|
||||
- Only one unnamed (`%d`) variable, since the order of variables might change
|
||||
in different languages
|
||||
- All variables used in the message-id are used in the translation
|
||||
- There should be no variables used in a translation that aren't in the
|
||||
message-id
|
||||
- Errors during translation.
|
||||
|
||||
The errors are grouped per file, and per message ID:
|
||||
|
||||
```
|
||||
Errors in `locale/zh_HK/gitlab.po`:
|
||||
PO-syntax errors
|
||||
SimplePoParser::ParserErrorSyntax error in lines
|
||||
Syntax error in msgctxt
|
||||
Syntax error in msgid
|
||||
Syntax error in msgstr
|
||||
Syntax error in message_line
|
||||
There should be only whitespace until the end of line after the double quote character of a message text.
|
||||
Parseing result before error: '{:msgid=>["", "You are going to remove %{project_name_with_namespace}.\\n", "Removed project CANNOT be restored!\\n", "Are you ABSOLUTELY sure?"]}'
|
||||
SimplePoParser filtered backtrace: SimplePoParser::ParserError
|
||||
Errors in `locale/zh_TW/gitlab.po`:
|
||||
1 pipeline
|
||||
<%d 條流水線> is using unknown variables: [%d]
|
||||
Failure translating to zh_TW with []: too few arguments
|
||||
```
|
||||
|
||||
In this output the `locale/zh_HK/gitlab.po` has syntax errors.
|
||||
The `locale/zh_TW/gitlab.po` has variables that are used in the translation that
|
||||
aren't in the message with id `1 pipeline`.
|
||||
|
||||
## Working with special content
|
||||
|
||||
### Interpolation
|
||||
|
|
27
lib/gitlab/i18n/metadata_entry.rb
Normal file
27
lib/gitlab/i18n/metadata_entry.rb
Normal file
|
@ -0,0 +1,27 @@
|
|||
module Gitlab
|
||||
module I18n
|
||||
class MetadataEntry
|
||||
attr_reader :entry_data
|
||||
|
||||
def initialize(entry_data)
|
||||
@entry_data = entry_data
|
||||
end
|
||||
|
||||
def expected_plurals
|
||||
return nil unless plural_information
|
||||
|
||||
plural_information['nplurals'].to_i
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def plural_information
|
||||
return @plural_information if defined?(@plural_information)
|
||||
|
||||
if plural_line = entry_data[:msgstr].detect { |metadata_line| metadata_line.starts_with?('Plural-Forms: ') }
|
||||
@plural_information = Hash[plural_line.scan(/(\w+)=([^;\n]+)/)]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
216
lib/gitlab/i18n/po_linter.rb
Normal file
216
lib/gitlab/i18n/po_linter.rb
Normal file
|
@ -0,0 +1,216 @@
|
|||
require 'simple_po_parser'
|
||||
|
||||
module Gitlab
|
||||
module I18n
|
||||
class PoLinter
|
||||
attr_reader :po_path, :translation_entries, :metadata_entry, :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)
|
||||
|
||||
# The first entry is the metadata entry if there is one.
|
||||
# This is an entry when empty `msgid`
|
||||
if entries.first[:msgid].empty?
|
||||
@metadata_entry = Gitlab::I18n::MetadataEntry.new(entries.shift)
|
||||
else
|
||||
return 'Missing metadata entry.'
|
||||
end
|
||||
|
||||
@translation_entries = entries.map do |entry_data|
|
||||
Gitlab::I18n::TranslationEntry.new(entry_data, metadata_entry.expected_plurals)
|
||||
end
|
||||
|
||||
nil
|
||||
rescue SimplePoParser::ParserError => e
|
||||
@translation_entries = []
|
||||
e.message
|
||||
end
|
||||
|
||||
def validate_entries
|
||||
errors = {}
|
||||
|
||||
translation_entries.each do |entry|
|
||||
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)
|
||||
validate_number_of_plurals(errors, entry)
|
||||
validate_unescaped_chars(errors, entry)
|
||||
|
||||
errors
|
||||
end
|
||||
|
||||
def validate_unescaped_chars(errors, entry)
|
||||
if entry.msgid_contains_unescaped_chars?
|
||||
errors << 'contains unescaped `%`, escape it using `%%`'
|
||||
end
|
||||
|
||||
if entry.plural_id_contains_unescaped_chars?
|
||||
errors << 'plural id contains unescaped `%`, escape it using `%%`'
|
||||
end
|
||||
|
||||
if entry.translations_contain_unescaped_chars?
|
||||
errors << 'translation contains unescaped `%`, escape it using `%%`'
|
||||
end
|
||||
end
|
||||
|
||||
def validate_number_of_plurals(errors, entry)
|
||||
return unless metadata_entry&.expected_plurals
|
||||
return unless entry.translated?
|
||||
|
||||
if entry.has_plural? && entry.all_translations.size != metadata_entry.expected_plurals
|
||||
errors << "should have #{metadata_entry.expected_plurals} "\
|
||||
"#{'translations'.pluralize(metadata_entry.expected_plurals)}"
|
||||
end
|
||||
end
|
||||
|
||||
def validate_newlines(errors, entry)
|
||||
if entry.msgid_contains_newlines?
|
||||
errors << 'is defined over multiple lines, this breaks some tooling.'
|
||||
end
|
||||
|
||||
if entry.plural_id_contains_newlines?
|
||||
errors << 'plural is defined over multiple lines, this breaks some tooling.'
|
||||
end
|
||||
|
||||
if entry.translations_contain_newlines?
|
||||
errors << 'has translations defined over multiple lines, this breaks some tooling.'
|
||||
end
|
||||
end
|
||||
|
||||
def validate_variables(errors, entry)
|
||||
if entry.has_singular_translation?
|
||||
validate_variables_in_message(errors, entry.msgid, entry.singular_translation)
|
||||
end
|
||||
|
||||
if entry.has_plural?
|
||||
entry.plural_translations.each do |translation|
|
||||
validate_variables_in_message(errors, entry.plural_id, translation)
|
||||
end
|
||||
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.
|
||||
# In this case we fall back to the default, which has all the the
|
||||
# required variables.
|
||||
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)
|
||||
errors << "is marked #{entry.flag}" if entry.flag
|
||||
end
|
||||
|
||||
def join_message(message)
|
||||
Array(message).join
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
92
lib/gitlab/i18n/translation_entry.rb
Normal file
92
lib/gitlab/i18n/translation_entry.rb
Normal file
|
@ -0,0 +1,92 @@
|
|||
module Gitlab
|
||||
module I18n
|
||||
class TranslationEntry
|
||||
PERCENT_REGEX = /(?:^|[^%])%(?!{\w*}|[a-z%])/.freeze
|
||||
|
||||
attr_reader :nplurals, :entry_data
|
||||
|
||||
def initialize(entry_data, nplurals)
|
||||
@entry_data = entry_data
|
||||
@nplurals = nplurals
|
||||
end
|
||||
|
||||
def msgid
|
||||
entry_data[:msgid]
|
||||
end
|
||||
|
||||
def plural_id
|
||||
entry_data[:msgid_plural]
|
||||
end
|
||||
|
||||
def has_plural?
|
||||
plural_id.present?
|
||||
end
|
||||
|
||||
def singular_translation
|
||||
all_translations.first if has_singular_translation?
|
||||
end
|
||||
|
||||
def all_translations
|
||||
@all_translations ||= entry_data.fetch_values(*translation_keys)
|
||||
.reject(&:empty?)
|
||||
end
|
||||
|
||||
def translated?
|
||||
all_translations.any?
|
||||
end
|
||||
|
||||
def plural_translations
|
||||
return [] unless has_plural?
|
||||
return [] unless translated?
|
||||
|
||||
@plural_translations ||= if has_singular_translation?
|
||||
all_translations.drop(1)
|
||||
else
|
||||
all_translations
|
||||
end
|
||||
end
|
||||
|
||||
def flag
|
||||
entry_data[:flag]
|
||||
end
|
||||
|
||||
def has_singular_translation?
|
||||
nplurals > 1 || !has_plural?
|
||||
end
|
||||
|
||||
def msgid_contains_newlines?
|
||||
msgid.is_a?(Array)
|
||||
end
|
||||
|
||||
def plural_id_contains_newlines?
|
||||
plural_id.is_a?(Array)
|
||||
end
|
||||
|
||||
def translations_contain_newlines?
|
||||
all_translations.any? { |translation| translation.is_a?(Array) }
|
||||
end
|
||||
|
||||
def msgid_contains_unescaped_chars?
|
||||
contains_unescaped_chars?(msgid)
|
||||
end
|
||||
|
||||
def plural_id_contains_unescaped_chars?
|
||||
contains_unescaped_chars?(plural_id)
|
||||
end
|
||||
|
||||
def translations_contain_unescaped_chars?
|
||||
all_translations.any? { |translation| contains_unescaped_chars?(translation) }
|
||||
end
|
||||
|
||||
def contains_unescaped_chars?(string)
|
||||
string =~ PERCENT_REGEX
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def translation_keys
|
||||
@translation_keys ||= entry_data.keys.select { |key| key.to_s =~ /\Amsgstr(\[\d+\])?\z/ }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -9,6 +9,8 @@ module Gitlab
|
|||
def self.context(current_user = nil)
|
||||
return unless self.enabled?
|
||||
|
||||
Raven.tags_context(locale: I18n.locale)
|
||||
|
||||
if current_user
|
||||
Raven.user_context(
|
||||
id: current_user.id,
|
||||
|
|
|
@ -42,5 +42,9 @@ module Gitlab
|
|||
'No'
|
||||
end
|
||||
end
|
||||
|
||||
def random_string
|
||||
Random.rand(Float::MAX.to_i).to_s(36)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -19,4 +19,44 @@ namespace :gettext do
|
|||
Rake::Task['gettext:pack'].invoke
|
||||
Rake::Task['gettext:po_to_json'].invoke
|
||||
end
|
||||
|
||||
desc 'Lint all po files in `locale/'
|
||||
task lint: :environment do
|
||||
FastGettext.silence_errors
|
||||
files = Dir.glob(Rails.root.join('locale/*/gitlab.po'))
|
||||
|
||||
linters = files.map do |file|
|
||||
locale = File.basename(File.dirname(file))
|
||||
|
||||
Gitlab::I18n::PoLinter.new(file, locale)
|
||||
end
|
||||
|
||||
pot_file = Rails.root.join('locale/gitlab.pot')
|
||||
linters.unshift(Gitlab::I18n::PoLinter.new(pot_file))
|
||||
|
||||
failed_linters = linters.select { |linter| linter.errors.any? }
|
||||
|
||||
if failed_linters.empty?
|
||||
puts 'All PO files are valid.'
|
||||
else
|
||||
failed_linters.each do |linter|
|
||||
report_errors_for_file(linter.po_path, linter.errors)
|
||||
end
|
||||
|
||||
raise "Not all PO-files are valid: #{failed_linters.map(&:po_path).to_sentence}"
|
||||
end
|
||||
end
|
||||
|
||||
def report_errors_for_file(file, errors_for_file)
|
||||
puts "Errors in `#{file}`:"
|
||||
|
||||
errors_for_file.each do |message_id, errors|
|
||||
puts " #{message_id}"
|
||||
errors.each do |error|
|
||||
spaces = ' ' * 4
|
||||
error = error.lines.join("#{spaces}")
|
||||
puts "#{spaces}#{error}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -82,6 +82,9 @@ msgstr ""
|
|||
msgid "Add new directory"
|
||||
msgstr ""
|
||||
|
||||
msgid "All"
|
||||
msgstr ""
|
||||
|
||||
msgid "Archived project! Repository is read-only"
|
||||
msgstr ""
|
||||
|
||||
|
@ -222,6 +225,9 @@ msgstr ""
|
|||
msgid "CiStatus|running"
|
||||
msgstr ""
|
||||
|
||||
msgid "Comments"
|
||||
msgstr ""
|
||||
|
||||
msgid "Commit"
|
||||
msgid_plural "Commits"
|
||||
msgstr[0] ""
|
||||
|
@ -394,6 +400,24 @@ msgstr ""
|
|||
msgid "Edit Pipeline Schedule %{id}"
|
||||
msgstr ""
|
||||
|
||||
msgid "EventFilterBy|Filter by all"
|
||||
msgstr ""
|
||||
|
||||
msgid "EventFilterBy|Filter by comments"
|
||||
msgstr ""
|
||||
|
||||
msgid "EventFilterBy|Filter by issue events"
|
||||
msgstr ""
|
||||
|
||||
msgid "EventFilterBy|Filter by merge events"
|
||||
msgstr ""
|
||||
|
||||
msgid "EventFilterBy|Filter by push events"
|
||||
msgstr ""
|
||||
|
||||
msgid "EventFilterBy|Filter by team"
|
||||
msgstr ""
|
||||
|
||||
msgid "Every day (at 4:00am)"
|
||||
msgstr ""
|
||||
|
||||
|
@ -489,6 +513,9 @@ msgstr ""
|
|||
msgid "Introducing Cycle Analytics"
|
||||
msgstr ""
|
||||
|
||||
msgid "Issue events"
|
||||
msgstr ""
|
||||
|
||||
msgid "Jobs for last month"
|
||||
msgstr ""
|
||||
|
||||
|
@ -518,6 +545,12 @@ msgstr ""
|
|||
msgid "Last commit"
|
||||
msgstr ""
|
||||
|
||||
msgid "LastPushEvent|You pushed to"
|
||||
msgstr ""
|
||||
|
||||
msgid "LastPushEvent|at"
|
||||
msgstr ""
|
||||
|
||||
msgid "Learn more in the"
|
||||
msgstr ""
|
||||
|
||||
|
@ -538,6 +571,9 @@ msgstr[1] ""
|
|||
msgid "Median"
|
||||
msgstr ""
|
||||
|
||||
msgid "Merge events"
|
||||
msgstr ""
|
||||
|
||||
msgid "MissingSSHKeyWarningLink|add an SSH key"
|
||||
msgstr ""
|
||||
|
||||
|
@ -741,6 +777,9 @@ msgstr ""
|
|||
msgid "Pipeline|with stages"
|
||||
msgstr ""
|
||||
|
||||
msgid "Project"
|
||||
msgstr ""
|
||||
|
||||
msgid "Project '%{project_name}' queued for deletion."
|
||||
msgstr ""
|
||||
|
||||
|
@ -774,6 +813,9 @@ msgstr ""
|
|||
msgid "Project home"
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectActivityRSS|Subscribe"
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectFeature|Disabled"
|
||||
msgstr ""
|
||||
|
||||
|
@ -795,6 +837,9 @@ msgstr ""
|
|||
msgid "ProjectNetworkGraph|Graph"
|
||||
msgstr ""
|
||||
|
||||
msgid "Push events"
|
||||
msgstr ""
|
||||
|
||||
msgid "Read more"
|
||||
msgstr ""
|
||||
|
||||
|
@ -925,6 +970,9 @@ msgstr ""
|
|||
msgid "Target Branch"
|
||||
msgstr ""
|
||||
|
||||
msgid "Team"
|
||||
msgstr ""
|
||||
|
||||
msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
# This file is distributed under the same license as the gitlab package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: gitlab 1.0.0\n"
|
||||
|
|
|
@ -45,7 +45,7 @@ msgstr ""
|
|||
|
||||
msgid "1 pipeline"
|
||||
msgid_plural "%d pipelines"
|
||||
msgstr[0] "1 個のパイプライン"
|
||||
msgstr[0] "%d 個のパイプライン"
|
||||
|
||||
msgid "A collection of graphs regarding Continuous Integration"
|
||||
msgstr "CIについてのグラフ"
|
||||
|
|
|
@ -45,7 +45,7 @@ msgstr ""
|
|||
|
||||
msgid "1 pipeline"
|
||||
msgid_plural "%d pipelines"
|
||||
msgstr[0] "1 파이프라인"
|
||||
msgstr[0] "%d 파이프라인"
|
||||
|
||||
msgid "A collection of graphs regarding Continuous Integration"
|
||||
msgstr "지속적인 통합에 관한 그래프 모음"
|
||||
|
|
|
@ -45,7 +45,7 @@ msgstr ""
|
|||
|
||||
msgid "1 pipeline"
|
||||
msgid_plural "%d pipelines"
|
||||
msgstr[0] "1 条流水线"
|
||||
msgstr[0] "%d 条流水线"
|
||||
|
||||
msgid "A collection of graphs regarding Continuous Integration"
|
||||
msgstr "持续集成数据图"
|
||||
|
|
|
@ -45,7 +45,7 @@ msgstr ""
|
|||
|
||||
msgid "1 pipeline"
|
||||
msgid_plural "%d pipelines"
|
||||
msgstr[0] "1 條流水線"
|
||||
msgstr[0] "%d 條流水線"
|
||||
|
||||
msgid "A collection of graphs regarding Continuous Integration"
|
||||
msgstr "相關持續集成的圖像集合"
|
||||
|
|
|
@ -45,7 +45,7 @@ msgstr ""
|
|||
|
||||
msgid "1 pipeline"
|
||||
msgid_plural "%d pipelines"
|
||||
msgstr[0] "1 條流水線"
|
||||
msgstr[0] "%d 條流水線"
|
||||
|
||||
msgid "A collection of graphs regarding Continuous Integration"
|
||||
msgstr "持續整合 (CI) 相關的圖表"
|
||||
|
@ -1208,16 +1208,16 @@ msgid "Withdraw Access Request"
|
|||
msgstr "取消權限申請"
|
||||
|
||||
msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
|
||||
msgstr "即將要刪除 %{group_name}。被刪除的群組完全無法救回來喔!真的「100%確定」要這麼做嗎?"
|
||||
msgstr "即將要刪除 %{group_name}。被刪除的群組無法復原!真的「確定」要這麼做嗎?"
|
||||
|
||||
msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?"
|
||||
msgstr "即將要刪除 %{project_name_with_namespace}。被刪除的專案完全無法救回來喔!真的「100%確定」要這麼做嗎?"
|
||||
msgstr "即將要刪除 %{project_name_with_namespace}。被刪除的專案無法復原!真的「確定」要這麼做嗎?"
|
||||
|
||||
msgid "You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?"
|
||||
msgstr "將要刪除本分支專案與主幹的所有關聯 (fork relationship) 。 %{forked_from_project} 真的「100%確定」要這麼做嗎?"
|
||||
msgstr "將要刪除本分支專案與主幹 %{forked_from_project} 的所有關聯。 真的「確定」要這麼做嗎?"
|
||||
|
||||
msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
|
||||
msgstr "將要把 %{project_name_with_namespace} 的所有權轉移給另一個人。真的「100%確定」要這麼做嗎?"
|
||||
msgstr "將要把 %{project_name_with_namespace} 的所有權轉移給另一個人。真的「確定」要這麼做嗎?"
|
||||
|
||||
msgid "You can only add files when you are on a branch"
|
||||
msgstr "只能在分支 (branch) 上建立檔案"
|
||||
|
|
|
@ -12,7 +12,8 @@ tasks = [
|
|||
%w[bundle exec license_finder],
|
||||
%w[yarn run eslint],
|
||||
%w[bundle exec rubocop --require rubocop-rspec],
|
||||
%w[scripts/lint-conflicts.sh]
|
||||
%w[scripts/lint-conflicts.sh],
|
||||
%w[bundle exec rake gettext:lint]
|
||||
]
|
||||
|
||||
failed_tasks = tasks.reduce({}) do |failures, task|
|
||||
|
|
27
spec/fixtures/fuzzy.po
vendored
Normal file
27
spec/fixtures/fuzzy.po
vendored
Normal file
|
@ -0,0 +1,27 @@
|
|||
# Spanish translations for gitlab package.
|
||||
# Copyright (C) 2017 THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the gitlab package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2017.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: gitlab 1.0.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2017-07-12 12:35-0500\n"
|
||||
"Language-Team: Spanish\n"
|
||||
"Language: es\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"Last-Translator: Bob Van Landuyt <bob@gitlab.com>\n"
|
||||
"X-Generator: Poedit 2.0.2\n"
|
||||
|
||||
msgid "1 commit"
|
||||
msgid_plural "%d commits"
|
||||
msgstr[0] "1 cambio"
|
||||
msgstr[1] "%d cambios"
|
||||
|
||||
#, fuzzy
|
||||
msgid "PipelineSchedules|Remove variable row"
|
||||
msgstr "Схема"
|
25
spec/fixtures/invalid.po
vendored
Normal file
25
spec/fixtures/invalid.po
vendored
Normal file
|
@ -0,0 +1,25 @@
|
|||
# Spanish translations for gitlab package.
|
||||
# Copyright (C) 2017 THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the gitlab package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2017.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: gitlab 1.0.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2017-07-12 12:35-0500\n"
|
||||
"Language-Team: Spanish\n"
|
||||
"Language: es\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"Last-Translator: Bob Van Landuyt <bob@gitlab.com>\n"
|
||||
"X-Generator: Poedit 2.0.2\n"
|
||||
|
||||
msgid "%d commit"
|
||||
msgid_plural "%d commits"
|
||||
msgstr[0] "%d cambio"
|
||||
msgstr[1] "%d cambios"
|
||||
|
||||
But this doesn't even look like an PO-entry
|
4
spec/fixtures/missing_metadata.po
vendored
Normal file
4
spec/fixtures/missing_metadata.po
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
msgid "1 commit"
|
||||
msgid_plural "%d commits"
|
||||
msgstr[0] "1 cambio"
|
||||
msgstr[1] "%d cambios"
|
22
spec/fixtures/missing_plurals.po
vendored
Normal file
22
spec/fixtures/missing_plurals.po
vendored
Normal file
|
@ -0,0 +1,22 @@
|
|||
# Spanish translations for gitlab package.
|
||||
# Copyright (C) 2017 THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the gitlab package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2017.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: gitlab 1.0.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2017-07-13 12:10-0500\n"
|
||||
"Language-Team: Spanish\n"
|
||||
"Language: es\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"Last-Translator: Bob Van Landuyt <bob@gitlab.com>\n"
|
||||
"X-Generator: Poedit 2.0.2\n"
|
||||
|
||||
msgid "%d commit"
|
||||
msgid_plural "%d commits"
|
||||
msgstr[0] "%d cambio"
|
26
spec/fixtures/multiple_plurals.po
vendored
Normal file
26
spec/fixtures/multiple_plurals.po
vendored
Normal file
|
@ -0,0 +1,26 @@
|
|||
# Arthur Charron <arthur.charron@hotmail.fr>, 2017. #zanata
|
||||
# Huang Tao <htve@outlook.com>, 2017. #zanata
|
||||
# Kohei Ota <inductor@kela.jp>, 2017. #zanata
|
||||
# Taisuke Inoue <taisuke.inoue.jp@gmail.com>, 2017. #zanata
|
||||
# Takuya Noguchi <takninnovationresearch@gmail.com>, 2017. #zanata
|
||||
# YANO Tethurou <tetuyano+zana@gmail.com>, 2017. #zanata
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: gitlab 1.0.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"PO-Revision-Date: 2017-08-06 11:23-0400\n"
|
||||
"Last-Translator: Taisuke Inoue <taisuke.inoue.jp@gmail.com>\n"
|
||||
"Language-Team: Japanese \"Language-Team: Russian (https://translate.zanata.org/"
|
||||
"project/view/GitLab)\n"
|
||||
"Language: ja\n"
|
||||
"X-Generator: Zanata 3.9.6\n"
|
||||
"Plural-Forms: nplurals=3; plural=n\n"
|
||||
|
||||
msgid "%d commit"
|
||||
msgid_plural "%d commits"
|
||||
msgstr[0] "%d個のコミット"
|
||||
msgstr[1] "%d個のコミット"
|
||||
msgstr[2] "missing a variable"
|
48
spec/fixtures/newlines.po
vendored
Normal file
48
spec/fixtures/newlines.po
vendored
Normal file
|
@ -0,0 +1,48 @@
|
|||
# Spanish translations for gitlab package.
|
||||
# Copyright (C) 2017 THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the gitlab package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2017.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: gitlab 1.0.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2017-07-12 12:35-0500\n"
|
||||
"Language-Team: Spanish\n"
|
||||
"Language: es\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"Last-Translator: Bob Van Landuyt <bob@gitlab.com>\n"
|
||||
"X-Generator: Poedit 2.0.2\n"
|
||||
|
||||
msgid "1 commit"
|
||||
msgid_plural "%d commits"
|
||||
msgstr[0] "1 cambio"
|
||||
msgstr[1] "%d cambios"
|
||||
|
||||
msgid ""
|
||||
"You are going to remove %{group_name}.\n"
|
||||
"Removed groups CANNOT be restored!\n"
|
||||
"Are you ABSOLUTELY sure?"
|
||||
msgstr ""
|
||||
"Va a eliminar %{group_name}.\n"
|
||||
"¡El grupo eliminado NO puede ser restaurado!\n"
|
||||
"¿Estás TOTALMENTE seguro?"
|
||||
|
||||
msgid "With plural"
|
||||
msgid_plural "with plurals"
|
||||
msgstr[0] "first"
|
||||
msgstr[1] "second"
|
||||
msgstr[2] ""
|
||||
"with"
|
||||
"multiple"
|
||||
"lines"
|
||||
|
||||
msgid "multiline plural id"
|
||||
msgid_plural ""
|
||||
"Plural"
|
||||
"Id"
|
||||
msgstr[0] "first"
|
||||
msgstr[1] "second"
|
21
spec/fixtures/unescaped_chars.po
vendored
Normal file
21
spec/fixtures/unescaped_chars.po
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
# Spanish translations for gitlab package.
|
||||
# Copyright (C) 2017 THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the gitlab package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2017.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: gitlab 1.0.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2017-07-13 12:10-0500\n"
|
||||
"Language-Team: Spanish\n"
|
||||
"Language: es\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"Last-Translator: Bob Van Landuyt <bob@gitlab.com>\n"
|
||||
"X-Generator: Poedit 2.0.2\n"
|
||||
|
||||
msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
|
||||
msgstr "將要把 %{project_name_with_namespace} 的所有權轉移給另一個人。真的「100%確定」要這麼做嗎?"
|
1136
spec/fixtures/valid.po
vendored
Normal file
1136
spec/fixtures/valid.po
vendored
Normal file
File diff suppressed because it is too large
Load diff
51
spec/lib/gitlab/i18n/metadata_entry_spec.rb
Normal file
51
spec/lib/gitlab/i18n/metadata_entry_spec.rb
Normal file
|
@ -0,0 +1,51 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::I18n::MetadataEntry do
|
||||
describe '#expected_plurals' do
|
||||
it 'returns the number of plurals' do
|
||||
data = {
|
||||
msgid: "",
|
||||
msgstr: [
|
||||
"",
|
||||
"Project-Id-Version: gitlab 1.0.0\\n",
|
||||
"Report-Msgid-Bugs-To: \\n",
|
||||
"PO-Revision-Date: 2017-07-13 12:10-0500\\n",
|
||||
"Language-Team: Spanish\\n",
|
||||
"Language: es\\n",
|
||||
"MIME-Version: 1.0\\n",
|
||||
"Content-Type: text/plain; charset=UTF-8\\n",
|
||||
"Content-Transfer-Encoding: 8bit\\n",
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\\n",
|
||||
"Last-Translator: Bob Van Landuyt <bob@gitlab.com>\\n",
|
||||
"X-Generator: Poedit 2.0.2\\n"
|
||||
]
|
||||
}
|
||||
entry = described_class.new(data)
|
||||
|
||||
expect(entry.expected_plurals).to eq(2)
|
||||
end
|
||||
|
||||
it 'returns 0 for the POT-metadata' do
|
||||
data = {
|
||||
msgid: "",
|
||||
msgstr: [
|
||||
"",
|
||||
"Project-Id-Version: gitlab 1.0.0\\n",
|
||||
"Report-Msgid-Bugs-To: \\n",
|
||||
"PO-Revision-Date: 2017-07-13 12:10-0500\\n",
|
||||
"Language-Team: Spanish\\n",
|
||||
"Language: es\\n",
|
||||
"MIME-Version: 1.0\\n",
|
||||
"Content-Type: text/plain; charset=UTF-8\\n",
|
||||
"Content-Transfer-Encoding: 8bit\\n",
|
||||
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n",
|
||||
"Last-Translator: Bob Van Landuyt <bob@gitlab.com>\\n",
|
||||
"X-Generator: Poedit 2.0.2\\n"
|
||||
]
|
||||
}
|
||||
entry = described_class.new(data)
|
||||
|
||||
expect(entry.expected_plurals).to eq(0)
|
||||
end
|
||||
end
|
||||
end
|
337
spec/lib/gitlab/i18n/po_linter_spec.rb
Normal file
337
spec/lib/gitlab/i18n/po_linter_spec.rb
Normal file
|
@ -0,0 +1,337 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::I18n::PoLinter do
|
||||
let(:linter) { described_class.new(po_path) }
|
||||
let(:po_path) { 'spec/fixtures/valid.po' }
|
||||
|
||||
describe '#errors' do
|
||||
it 'only calls validation once' do
|
||||
expect(linter).to receive(:validate_po).once.and_call_original
|
||||
|
||||
2.times { linter.errors }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#validate_po' do
|
||||
subject(:errors) { linter.validate_po }
|
||||
|
||||
context 'for a fuzzy message' do
|
||||
let(:po_path) { 'spec/fixtures/fuzzy.po' }
|
||||
|
||||
it 'has an error' do
|
||||
is_expected.to include('PipelineSchedules|Remove variable row' => ['is marked fuzzy'])
|
||||
end
|
||||
end
|
||||
|
||||
context 'for a translations with newlines' do
|
||||
let(:po_path) { 'spec/fixtures/newlines.po' }
|
||||
|
||||
it 'has an error for a normal string' do
|
||||
message_id = "You are going to remove %{group_name}.\\nRemoved groups CANNOT be restored!\\nAre you ABSOLUTELY sure?"
|
||||
expected_message = "is defined over multiple lines, this breaks some tooling."
|
||||
|
||||
expect(errors[message_id]).to include(expected_message)
|
||||
end
|
||||
|
||||
it 'has an error when a translation is defined over multiple lines' do
|
||||
message_id = "You are going to remove %{group_name}.\\nRemoved groups CANNOT be restored!\\nAre you ABSOLUTELY sure?"
|
||||
expected_message = "has translations defined over multiple lines, this breaks some tooling."
|
||||
|
||||
expect(errors[message_id]).to include(expected_message)
|
||||
end
|
||||
|
||||
it 'raises an error when a plural translation is defined over multiple lines' do
|
||||
message_id = 'With plural'
|
||||
expected_message = "has translations defined over multiple lines, this breaks some tooling."
|
||||
|
||||
expect(errors[message_id]).to include(expected_message)
|
||||
end
|
||||
|
||||
it 'raises an error when the plural id is defined over multiple lines' do
|
||||
message_id = 'multiline plural id'
|
||||
expected_message = "plural is defined over multiple lines, this breaks some tooling."
|
||||
|
||||
expect(errors[message_id]).to include(expected_message)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an invalid po' do
|
||||
let(:po_path) { 'spec/fixtures/invalid.po' }
|
||||
|
||||
it 'returns the error' do
|
||||
is_expected.to include('PO-syntax errors' => a_kind_of(Array))
|
||||
end
|
||||
|
||||
it 'does not validate entries' do
|
||||
expect(linter).not_to receive(:validate_entries)
|
||||
|
||||
linter.validate_po
|
||||
end
|
||||
end
|
||||
|
||||
context 'with missing metadata' do
|
||||
let(:po_path) { 'spec/fixtures/missing_metadata.po' }
|
||||
|
||||
it 'returns the an error' do
|
||||
is_expected.to include('PO-syntax errors' => a_kind_of(Array))
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a valid po' do
|
||||
it 'parses the file' do
|
||||
expect(linter).to receive(:parse_po).and_call_original
|
||||
|
||||
linter.validate_po
|
||||
end
|
||||
|
||||
it 'validates the entries' do
|
||||
expect(linter).to receive(:validate_entries).and_call_original
|
||||
|
||||
linter.validate_po
|
||||
end
|
||||
|
||||
it 'has no errors' do
|
||||
is_expected.to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'with missing plurals' do
|
||||
let(:po_path) { 'spec/fixtures/missing_plurals.po' }
|
||||
|
||||
it 'has errors' do
|
||||
is_expected.not_to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'with multiple plurals' do
|
||||
let(:po_path) { 'spec/fixtures/multiple_plurals.po' }
|
||||
|
||||
it 'has errors' do
|
||||
is_expected.not_to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'with unescaped chars' do
|
||||
let(:po_path) { 'spec/fixtures/unescaped_chars.po' }
|
||||
|
||||
it 'contains an error' do
|
||||
message_id = 'You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?'
|
||||
expected_error = 'translation contains unescaped `%`, escape it using `%%`'
|
||||
|
||||
expect(errors[message_id]).to include(expected_error)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#parse_po' do
|
||||
context 'with a valid po' do
|
||||
it 'fills in the entries' do
|
||||
linter.parse_po
|
||||
|
||||
expect(linter.translation_entries).not_to be_empty
|
||||
expect(linter.metadata_entry).to be_kind_of(Gitlab::I18n::MetadataEntry)
|
||||
end
|
||||
|
||||
it 'does not have errors' do
|
||||
expect(linter.parse_po).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an invalid po' do
|
||||
let(:po_path) { 'spec/fixtures/invalid.po' }
|
||||
|
||||
it 'contains an error' do
|
||||
expect(linter.parse_po).not_to be_nil
|
||||
end
|
||||
|
||||
it 'sets the entries to an empty array' do
|
||||
linter.parse_po
|
||||
|
||||
expect(linter.translation_entries).to eq([])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#validate_entries' do
|
||||
it 'keeps track of errors for entries' do
|
||||
fake_invalid_entry = Gitlab::I18n::TranslationEntry.new(
|
||||
{ msgid: "Hello %{world}", msgstr: "Bonjour %{monde}" }, 2
|
||||
)
|
||||
allow(linter).to receive(:translation_entries) { [fake_invalid_entry] }
|
||||
|
||||
expect(linter).to receive(:validate_entry)
|
||||
.with(fake_invalid_entry)
|
||||
.and_call_original
|
||||
|
||||
expect(linter.validate_entries).to include("Hello %{world}" => an_instance_of(Array))
|
||||
end
|
||||
end
|
||||
|
||||
describe '#validate_entry' do
|
||||
it 'validates the flags, variable usage, newlines, and unescaped chars' do
|
||||
fake_entry = double
|
||||
|
||||
expect(linter).to receive(:validate_flags).with([], fake_entry)
|
||||
expect(linter).to receive(:validate_variables).with([], fake_entry)
|
||||
expect(linter).to receive(:validate_newlines).with([], fake_entry)
|
||||
expect(linter).to receive(:validate_number_of_plurals).with([], fake_entry)
|
||||
expect(linter).to receive(:validate_unescaped_chars).with([], fake_entry)
|
||||
|
||||
linter.validate_entry(fake_entry)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#validate_number_of_plurals' do
|
||||
it 'validates when there are an incorrect number of translations' do
|
||||
fake_metadata = double
|
||||
allow(fake_metadata).to receive(:expected_plurals).and_return(2)
|
||||
allow(linter).to receive(:metadata_entry).and_return(fake_metadata)
|
||||
|
||||
fake_entry = Gitlab::I18n::TranslationEntry.new(
|
||||
{ msgid: 'the singular', msgid_plural: 'the plural', 'msgstr[0]' => 'the singular' },
|
||||
2
|
||||
)
|
||||
errors = []
|
||||
|
||||
linter.validate_number_of_plurals(errors, fake_entry)
|
||||
|
||||
expect(errors).to include('should have 2 translations')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#validate_variables' do
|
||||
it 'validates both signular and plural in a pluralized string when the entry has a singular' do
|
||||
pluralized_entry = Gitlab::I18n::TranslationEntry.new(
|
||||
{ msgid: 'Hello %{world}',
|
||||
msgid_plural: 'Hello all %{world}',
|
||||
'msgstr[0]' => 'Bonjour %{world}',
|
||||
'msgstr[1]' => 'Bonjour tous %{world}' },
|
||||
2
|
||||
)
|
||||
|
||||
expect(linter).to receive(:validate_variables_in_message)
|
||||
.with([], 'Hello %{world}', 'Bonjour %{world}')
|
||||
.and_call_original
|
||||
expect(linter).to receive(:validate_variables_in_message)
|
||||
.with([], 'Hello all %{world}', 'Bonjour tous %{world}')
|
||||
.and_call_original
|
||||
|
||||
linter.validate_variables([], pluralized_entry)
|
||||
end
|
||||
|
||||
it 'only validates plural when there is no separate singular' do
|
||||
pluralized_entry = Gitlab::I18n::TranslationEntry.new(
|
||||
{ msgid: 'Hello %{world}',
|
||||
msgid_plural: 'Hello all %{world}',
|
||||
'msgstr[0]' => 'Bonjour %{world}' },
|
||||
1
|
||||
)
|
||||
|
||||
expect(linter).to receive(:validate_variables_in_message)
|
||||
.with([], 'Hello all %{world}', 'Bonjour %{world}')
|
||||
|
||||
linter.validate_variables([], pluralized_entry)
|
||||
end
|
||||
|
||||
it 'validates the message variables' do
|
||||
entry = Gitlab::I18n::TranslationEntry.new(
|
||||
{ msgid: 'Hello', msgstr: 'Bonjour' },
|
||||
2
|
||||
)
|
||||
|
||||
expect(linter).to receive(:validate_variables_in_message)
|
||||
.with([], 'Hello', 'Bonjour')
|
||||
|
||||
linter.validate_variables([], entry)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#validate_variables_in_message' do
|
||||
it 'detects when a variables are used incorrectly' do
|
||||
errors = []
|
||||
|
||||
expected_errors = ['<hello %{world} %d> is missing: [%{hello}]',
|
||||
'<hello %{world} %d> is using unknown variables: [%{world}]',
|
||||
'is combining multiple unnamed variables']
|
||||
|
||||
linter.validate_variables_in_message(errors, '%{hello} world %d', 'hello %{world} %d')
|
||||
|
||||
expect(errors).to include(*expected_errors)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#validate_translation' do
|
||||
it 'succeeds with valid variables' do
|
||||
errors = []
|
||||
|
||||
linter.validate_translation(errors, 'Hello %{world}', ['%{world}'])
|
||||
|
||||
expect(errors).to be_empty
|
||||
end
|
||||
|
||||
it 'adds an error message when translating fails' do
|
||||
errors = []
|
||||
|
||||
expect(FastGettext::Translation).to receive(:_) { raise 'broken' }
|
||||
|
||||
linter.validate_translation(errors, 'Hello', [])
|
||||
|
||||
expect(errors).to include('Failure translating to en with []: broken')
|
||||
end
|
||||
|
||||
it 'adds an error message when translating fails when translating with context' do
|
||||
errors = []
|
||||
|
||||
expect(FastGettext::Translation).to receive(:s_) { raise 'broken' }
|
||||
|
||||
linter.validate_translation(errors, 'Tests|Hello', [])
|
||||
|
||||
expect(errors).to include('Failure translating to en with []: broken')
|
||||
end
|
||||
|
||||
it "adds an error when trying to translate with incorrect variables when using unnamed variables" do
|
||||
errors = []
|
||||
|
||||
linter.validate_translation(errors, 'Hello %d', ['%s'])
|
||||
|
||||
expect(errors.first).to start_with("Failure translating to en with")
|
||||
end
|
||||
|
||||
it "adds an error when trying to translate with named variables when unnamed variables are expected" do
|
||||
errors = []
|
||||
|
||||
linter.validate_translation(errors, 'Hello %d', ['%{world}'])
|
||||
|
||||
expect(errors.first).to start_with("Failure translating to en with")
|
||||
end
|
||||
|
||||
it 'adds an error when translated with incorrect variables using named variables' do
|
||||
errors = []
|
||||
|
||||
linter.validate_translation(errors, 'Hello %{thing}', ['%d'])
|
||||
|
||||
expect(errors.first).to start_with("Failure translating to en with")
|
||||
end
|
||||
end
|
||||
|
||||
describe '#fill_in_variables' do
|
||||
it 'builds an array for %d translations' do
|
||||
result = linter.fill_in_variables(['%d'])
|
||||
|
||||
expect(result).to contain_exactly(a_kind_of(Integer))
|
||||
end
|
||||
|
||||
it 'builds an array for %s translations' do
|
||||
result = linter.fill_in_variables(['%s'])
|
||||
|
||||
expect(result).to contain_exactly(a_kind_of(String))
|
||||
end
|
||||
|
||||
it 'builds a hash for named variables' do
|
||||
result = linter.fill_in_variables(['%{hello}'])
|
||||
|
||||
expect(result).to be_a(Hash)
|
||||
expect(result).to include('hello' => an_instance_of(String))
|
||||
end
|
||||
end
|
||||
end
|
203
spec/lib/gitlab/i18n/translation_entry_spec.rb
Normal file
203
spec/lib/gitlab/i18n/translation_entry_spec.rb
Normal file
|
@ -0,0 +1,203 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::I18n::TranslationEntry do
|
||||
describe '#singular_translation' do
|
||||
it 'returns the normal `msgstr` for translations without plural' do
|
||||
data = { msgid: 'Hello world', msgstr: 'Bonjour monde' }
|
||||
entry = described_class.new(data, 2)
|
||||
|
||||
expect(entry.singular_translation).to eq('Bonjour monde')
|
||||
end
|
||||
|
||||
it 'returns the first string for entries with plurals' do
|
||||
data = {
|
||||
msgid: 'Hello world',
|
||||
msgid_plural: 'Hello worlds',
|
||||
'msgstr[0]' => 'Bonjour monde',
|
||||
'msgstr[1]' => 'Bonjour mondes'
|
||||
}
|
||||
entry = described_class.new(data, 2)
|
||||
|
||||
expect(entry.singular_translation).to eq('Bonjour monde')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#all_translations' do
|
||||
it 'returns all translations for singular translations' do
|
||||
data = { msgid: 'Hello world', msgstr: 'Bonjour monde' }
|
||||
entry = described_class.new(data, 2)
|
||||
|
||||
expect(entry.all_translations).to eq(['Bonjour monde'])
|
||||
end
|
||||
|
||||
it 'returns all translations when including plural translations' do
|
||||
data = {
|
||||
msgid: 'Hello world',
|
||||
msgid_plural: 'Hello worlds',
|
||||
'msgstr[0]' => 'Bonjour monde',
|
||||
'msgstr[1]' => 'Bonjour mondes'
|
||||
}
|
||||
entry = described_class.new(data, 2)
|
||||
|
||||
expect(entry.all_translations).to eq(['Bonjour monde', 'Bonjour mondes'])
|
||||
end
|
||||
end
|
||||
|
||||
describe '#plural_translations' do
|
||||
it 'returns all translations if there is only one plural' do
|
||||
data = {
|
||||
msgid: 'Hello world',
|
||||
msgid_plural: 'Hello worlds',
|
||||
'msgstr[0]' => 'Bonjour monde'
|
||||
}
|
||||
entry = described_class.new(data, 1)
|
||||
|
||||
expect(entry.plural_translations).to eq(['Bonjour monde'])
|
||||
end
|
||||
|
||||
it 'returns all translations except for the first one if there are multiple' do
|
||||
data = {
|
||||
msgid: 'Hello world',
|
||||
msgid_plural: 'Hello worlds',
|
||||
'msgstr[0]' => 'Bonjour monde',
|
||||
'msgstr[1]' => 'Bonjour mondes',
|
||||
'msgstr[2]' => 'Bonjour tous les mondes'
|
||||
}
|
||||
entry = described_class.new(data, 3)
|
||||
|
||||
expect(entry.plural_translations).to eq(['Bonjour mondes', 'Bonjour tous les mondes'])
|
||||
end
|
||||
end
|
||||
|
||||
describe '#has_singular_translation?' do
|
||||
it 'has a singular when the translation is not pluralized' do
|
||||
data = {
|
||||
msgid: 'hello world',
|
||||
msgstr: 'hello'
|
||||
}
|
||||
entry = described_class.new(data, 2)
|
||||
|
||||
expect(entry).to have_singular_translation
|
||||
end
|
||||
|
||||
it 'has a singular when plural and singular are separately defined' do
|
||||
data = {
|
||||
msgid: 'hello world',
|
||||
msgid_plural: 'hello worlds',
|
||||
"msgstr[0]" => 'hello world',
|
||||
"msgstr[1]" => 'hello worlds'
|
||||
}
|
||||
entry = described_class.new(data, 2)
|
||||
|
||||
expect(entry).to have_singular_translation
|
||||
end
|
||||
|
||||
it 'does not have a separate singular if the plural string only has one translation' do
|
||||
data = {
|
||||
msgid: 'hello world',
|
||||
msgid_plural: 'hello worlds',
|
||||
"msgstr[0]" => 'hello worlds'
|
||||
}
|
||||
entry = described_class.new(data, 1)
|
||||
|
||||
expect(entry).not_to have_singular_translation
|
||||
end
|
||||
end
|
||||
|
||||
describe '#msgid_contains_newlines' do
|
||||
it 'is true when the msgid is an array' do
|
||||
data = { msgid: %w(hello world) }
|
||||
entry = described_class.new(data, 2)
|
||||
|
||||
expect(entry.msgid_contains_newlines?).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
describe '#plural_id_contains_newlines' do
|
||||
it 'is true when the msgid is an array' do
|
||||
data = { msgid_plural: %w(hello world) }
|
||||
entry = described_class.new(data, 2)
|
||||
|
||||
expect(entry.plural_id_contains_newlines?).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
describe '#translations_contain_newlines' do
|
||||
it 'is true when the msgid is an array' do
|
||||
data = { msgstr: %w(hello world) }
|
||||
entry = described_class.new(data, 2)
|
||||
|
||||
expect(entry.translations_contain_newlines?).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
describe '#contains_unescaped_chars' do
|
||||
let(:data) { { msgid: '' } }
|
||||
let(:entry) { described_class.new(data, 2) }
|
||||
it 'is true when the msgid is an array' do
|
||||
string = '「100%確定」'
|
||||
|
||||
expect(entry.contains_unescaped_chars?(string)).to be_truthy
|
||||
end
|
||||
|
||||
it 'is false when the `%` char is escaped' do
|
||||
string = '「100%%確定」'
|
||||
|
||||
expect(entry.contains_unescaped_chars?(string)).to be_falsy
|
||||
end
|
||||
|
||||
it 'is false when using an unnamed variable' do
|
||||
string = '「100%d確定」'
|
||||
|
||||
expect(entry.contains_unescaped_chars?(string)).to be_falsy
|
||||
end
|
||||
|
||||
it 'is false when using a named variable' do
|
||||
string = '「100%{named}確定」'
|
||||
|
||||
expect(entry.contains_unescaped_chars?(string)).to be_falsy
|
||||
end
|
||||
|
||||
it 'is true when an unnamed variable is not closed' do
|
||||
string = '「100%{named確定」'
|
||||
|
||||
expect(entry.contains_unescaped_chars?(string)).to be_truthy
|
||||
end
|
||||
|
||||
it 'is true when the string starts with a `%`' do
|
||||
string = '%10'
|
||||
|
||||
expect(entry.contains_unescaped_chars?(string)).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
describe '#msgid_contains_unescaped_chars' do
|
||||
it 'is true when the msgid contains a `%`' do
|
||||
data = { msgid: '「100%確定」' }
|
||||
entry = described_class.new(data, 2)
|
||||
|
||||
expect(entry).to receive(:contains_unescaped_chars?).and_call_original
|
||||
expect(entry.msgid_contains_unescaped_chars?).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
describe '#plural_id_contains_unescaped_chars' do
|
||||
it 'is true when the plural msgid contains a `%`' do
|
||||
data = { msgid_plural: '「100%確定」' }
|
||||
entry = described_class.new(data, 2)
|
||||
|
||||
expect(entry).to receive(:contains_unescaped_chars?).and_call_original
|
||||
expect(entry.plural_id_contains_unescaped_chars?).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
describe '#translations_contain_unescaped_chars' do
|
||||
it 'is true when the translation contains a `%`' do
|
||||
data = { msgstr: '「100%確定」' }
|
||||
entry = described_class.new(data, 2)
|
||||
|
||||
expect(entry).to receive(:contains_unescaped_chars?).and_call_original
|
||||
expect(entry.translations_contain_unescaped_chars?).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
13
spec/lib/gitlab/sentry_spec.rb
Normal file
13
spec/lib/gitlab/sentry_spec.rb
Normal file
|
@ -0,0 +1,13 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::Sentry do
|
||||
describe '.context' do
|
||||
it 'adds the locale to the tags' do
|
||||
expect(described_class).to receive(:enabled?).and_return(true)
|
||||
|
||||
described_class.context(nil)
|
||||
|
||||
expect(Raven.tags_context[:locale]).to eq(I18n.locale.to_s)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,7 +1,7 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::Utils do
|
||||
delegate :to_boolean, :boolean_to_yes_no, :slugify, to: :described_class
|
||||
delegate :to_boolean, :boolean_to_yes_no, :slugify, :random_string, to: :described_class
|
||||
|
||||
describe '.slugify' do
|
||||
{
|
||||
|
@ -53,4 +53,10 @@ describe Gitlab::Utils do
|
|||
expect(boolean_to_yes_no(false)).to eq('No')
|
||||
end
|
||||
end
|
||||
|
||||
describe '.random_string' do
|
||||
it 'generates a string' do
|
||||
expect(random_string).to be_kind_of(String)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue