Commit dceb2112 authored by Douwe Maan's avatar Douwe Maan

Merge branch 'bvl-validate-po-files' into 'master'

Validate PO files in static analysis

See merge request !13000
parents ba3cfd07 4761235f
...@@ -349,6 +349,8 @@ group :development, :test do ...@@ -349,6 +349,8 @@ group :development, :test do
gem 'activerecord_sane_schema_dumper', '0.2' gem 'activerecord_sane_schema_dumper', '0.2'
gem 'stackprof', '~> 0.2.10', require: false gem 'stackprof', '~> 0.2.10', require: false
gem 'simple_po_parser', '~> 1.1.2', require: false
end end
group :test do group :test do
......
...@@ -833,6 +833,7 @@ GEM ...@@ -833,6 +833,7 @@ GEM
faraday (~> 0.9) faraday (~> 0.9)
jwt (~> 1.5) jwt (~> 1.5)
multi_json (~> 1.10) multi_json (~> 1.10)
simple_po_parser (1.1.2)
simplecov (0.14.1) simplecov (0.14.1)
docile (~> 1.1.0) docile (~> 1.1.0)
json (>= 1.8, < 3) json (>= 1.8, < 3)
...@@ -1145,6 +1146,7 @@ DEPENDENCIES ...@@ -1145,6 +1146,7 @@ DEPENDENCIES
sidekiq (~> 5.0) sidekiq (~> 5.0)
sidekiq-cron (~> 0.6.0) sidekiq-cron (~> 0.6.0)
sidekiq-limit_fetch (~> 3.4) sidekiq-limit_fetch (~> 3.4)
simple_po_parser (~> 1.1.2)
simplecov (~> 0.14.0) simplecov (~> 0.14.0)
slack-notifier (~> 1.5.1) slack-notifier (~> 1.5.1)
spinach-rails (~> 0.2.1) spinach-rails (~> 0.2.1)
......
---
title: Validate PO-files in static analysis
merge_request: 13000
author:
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_text_domain = 'gitlab'
FastGettext.default_available_locales = Gitlab::I18n.available_locales FastGettext.default_available_locales = Gitlab::I18n.available_locales
FastGettext.default_locale = :en FastGettext.default_locale = :en
......
...@@ -138,6 +138,47 @@ translations. There's no need to generate `.po` files. ...@@ -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 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. `~#`; 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 ## Working with special content
### Interpolation ### Interpolation
......
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
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
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 ...@@ -9,6 +9,8 @@ module Gitlab
def self.context(current_user = nil) def self.context(current_user = nil)
return unless self.enabled? return unless self.enabled?
Raven.tags_context(locale: I18n.locale)
if current_user if current_user
Raven.user_context( Raven.user_context(
id: current_user.id, id: current_user.id,
......
...@@ -42,5 +42,9 @@ module Gitlab ...@@ -42,5 +42,9 @@ module Gitlab
'No' 'No'
end end
end end
def random_string
Random.rand(Float::MAX.to_i).to_s(36)
end
end end
end end
...@@ -19,4 +19,44 @@ namespace :gettext do ...@@ -19,4 +19,44 @@ namespace :gettext do
Rake::Task['gettext:pack'].invoke Rake::Task['gettext:pack'].invoke
Rake::Task['gettext:po_to_json'].invoke Rake::Task['gettext:po_to_json'].invoke
end 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 end
...@@ -82,6 +82,9 @@ msgstr "" ...@@ -82,6 +82,9 @@ msgstr ""
msgid "Add new directory" msgid "Add new directory"
msgstr "" msgstr ""
msgid "All"
msgstr ""
msgid "Archived project! Repository is read-only" msgid "Archived project! Repository is read-only"
msgstr "" msgstr ""
...@@ -222,6 +225,9 @@ msgstr "" ...@@ -222,6 +225,9 @@ msgstr ""
msgid "CiStatus|running" msgid "CiStatus|running"
msgstr "" msgstr ""
msgid "Comments"
msgstr ""
msgid "Commit" msgid "Commit"
msgid_plural "Commits" msgid_plural "Commits"
msgstr[0] "" msgstr[0] ""
...@@ -394,6 +400,24 @@ msgstr "" ...@@ -394,6 +400,24 @@ msgstr ""
msgid "Edit Pipeline Schedule %{id}" msgid "Edit Pipeline Schedule %{id}"
msgstr "" 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)" msgid "Every day (at 4:00am)"
msgstr "" msgstr ""
...@@ -489,6 +513,9 @@ msgstr "" ...@@ -489,6 +513,9 @@ msgstr ""
msgid "Introducing Cycle Analytics" msgid "Introducing Cycle Analytics"
msgstr "" msgstr ""
msgid "Issue events"
msgstr ""
msgid "Jobs for last month" msgid "Jobs for last month"
msgstr "" msgstr ""
...@@ -518,6 +545,12 @@ msgstr "" ...@@ -518,6 +545,12 @@ msgstr ""
msgid "Last commit" msgid "Last commit"
msgstr "" msgstr ""
msgid "LastPushEvent|You pushed to"
msgstr ""
msgid "LastPushEvent|at"
msgstr ""
msgid "Learn more in the" msgid "Learn more in the"
msgstr "" msgstr ""
...@@ -538,6 +571,9 @@ msgstr[1] "" ...@@ -538,6 +571,9 @@ msgstr[1] ""
msgid "Median" msgid "Median"
msgstr "" msgstr ""
msgid "Merge events"
msgstr ""
msgid "MissingSSHKeyWarningLink|add an SSH key" msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "" msgstr ""
...@@ -741,6 +777,9 @@ msgstr "" ...@@ -741,6 +777,9 @@ msgstr ""
msgid "Pipeline|with stages" msgid "Pipeline|with stages"
msgstr "" msgstr ""
msgid "Project"
msgstr ""
msgid "Project '%{project_name}' queued for deletion." msgid "Project '%{project_name}' queued for deletion."
msgstr "" msgstr ""
...@@ -774,6 +813,9 @@ msgstr "" ...@@ -774,6 +813,9 @@ msgstr ""
msgid "Project home" msgid "Project home"
msgstr "" msgstr ""
msgid "ProjectActivityRSS|Subscribe"
msgstr ""
msgid "ProjectFeature|Disabled" msgid "ProjectFeature|Disabled"
msgstr "" msgstr ""
...@@ -795,6 +837,9 @@ msgstr "" ...@@ -795,6 +837,9 @@ msgstr ""
msgid "ProjectNetworkGraph|Graph" msgid "ProjectNetworkGraph|Graph"
msgstr "" msgstr ""
msgid "Push events"
msgstr ""
msgid "Read more" msgid "Read more"
msgstr "" msgstr ""
...@@ -925,6 +970,9 @@ msgstr "" ...@@ -925,6 +970,9 @@ msgstr ""
msgid "Target Branch" msgid "Target Branch"
msgstr "" 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." 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 "" msgstr ""
......
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
# This file is distributed under the same license as the gitlab package. # This file is distributed under the same license as the gitlab package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
# #
#, fuzzy
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: gitlab 1.0.0\n" "Project-Id-Version: gitlab 1.0.0\n"
......
...@@ -45,7 +45,7 @@ msgstr "" ...@@ -45,7 +45,7 @@ msgstr ""
msgid "1 pipeline" msgid "1 pipeline"
msgid_plural "%d pipelines" msgid_plural "%d pipelines"
msgstr[0] "1 個のパイプライン" msgstr[0] "%d 個のパイプライン"
msgid "A collection of graphs regarding Continuous Integration" msgid "A collection of graphs regarding Continuous Integration"
msgstr "CIについてのグラフ" msgstr "CIについてのグラフ"
......
...@@ -45,7 +45,7 @@ msgstr "" ...@@ -45,7 +45,7 @@ msgstr ""
msgid "1 pipeline" msgid "1 pipeline"
msgid_plural "%d pipelines" msgid_plural "%d pipelines"
msgstr[0] "1 파이프라인" msgstr[0] "%d 파이프라인"
msgid "A collection of graphs regarding Continuous Integration" msgid "A collection of graphs regarding Continuous Integration"
msgstr "지속적인 통합에 관한 그래프 모음" msgstr "지속적인 통합에 관한 그래프 모음"
......
...@@ -45,7 +45,7 @@ msgstr "" ...@@ -45,7 +45,7 @@ msgstr ""
msgid "1 pipeline" msgid "1 pipeline"
msgid_plural "%d pipelines" msgid_plural "%d pipelines"
msgstr[0] "1 条流水线" msgstr[0] "%d 条流水线"
msgid "A collection of graphs regarding Continuous Integration" msgid "A collection of graphs regarding Continuous Integration"
msgstr "持续集成数据图" msgstr "持续集成数据图"
......
...@@ -45,7 +45,7 @@ msgstr "" ...@@ -45,7 +45,7 @@ msgstr ""
msgid "1 pipeline" msgid "1 pipeline"
msgid_plural "%d pipelines" msgid_plural "%d pipelines"
msgstr[0] "1 條流水線" msgstr[0] "%d 條流水線"
msgid "A collection of graphs regarding Continuous Integration" msgid "A collection of graphs regarding Continuous Integration"
msgstr "相關持續集成的圖像集合" msgstr "相關持續集成的圖像集合"
......
...@@ -45,7 +45,7 @@ msgstr "" ...@@ -45,7 +45,7 @@ msgstr ""
msgid "1 pipeline" msgid "1 pipeline"
msgid_plural "%d pipelines" msgid_plural "%d pipelines"
msgstr[0] "1 條流水線" msgstr[0] "%d 條流水線"
msgid "A collection of graphs regarding Continuous Integration" msgid "A collection of graphs regarding Continuous Integration"
msgstr "持續整合 (CI) 相關的圖表" msgstr "持續整合 (CI) 相關的圖表"
...@@ -1208,16 +1208,16 @@ msgid "Withdraw Access Request" ...@@ -1208,16 +1208,16 @@ msgid "Withdraw Access Request"
msgstr "取消權限申請" msgstr "取消權限申請"
msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?" 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?" 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?" 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?" 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" msgid "You can only add files when you are on a branch"
msgstr "只能在分支 (branch) 上建立檔案" msgstr "只能在分支 (branch) 上建立檔案"
......
...@@ -12,7 +12,8 @@ tasks = [ ...@@ -12,7 +12,8 @@ tasks = [
%w[bundle exec license_finder], %w[bundle exec license_finder],
%w[yarn run eslint], %w[yarn run eslint],
%w[bundle exec rubocop --require rubocop-rspec], %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| failed_tasks = tasks.reduce({}) do |failures, task|
......
# 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 "Схема"
# 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
\ No newline at end of file
msgid "1 commit"
msgid_plural "%d commits"
msgstr[0] "1 cambio"
msgstr[1] "%d cambios"
# 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"
# 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"
# 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"
# 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%確定」要這麼做嗎?"
This diff is collapsed.
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
This diff is collapsed.
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
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
require 'spec_helper' require 'spec_helper'
describe Gitlab::Utils do 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 describe '.slugify' do
{ {
...@@ -53,4 +53,10 @@ describe Gitlab::Utils do ...@@ -53,4 +53,10 @@ describe Gitlab::Utils do
expect(boolean_to_yes_no(false)).to eq('No') expect(boolean_to_yes_no(false)).to eq('No')
end end
end end
describe '.random_string' do
it 'generates a string' do
expect(random_string).to be_kind_of(String)
end
end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment