mirror of
https://github.com/discourse/discourse.git
synced 2025-02-07 20:08:26 +00:00
Merge pull request #3787 from gschlager/locale-keys
FIX: Some strings in locale files were not translatable
This commit is contained in:
commit
d00762dcd5
@ -153,7 +153,6 @@ en:
|
|||||||
sign_up: "Sign Up"
|
sign_up: "Sign Up"
|
||||||
log_in: "Log In"
|
log_in: "Log In"
|
||||||
age: "Age"
|
age: "Age"
|
||||||
last_post: "Last Post"
|
|
||||||
joined: "Joined"
|
joined: "Joined"
|
||||||
admin_title: "Admin"
|
admin_title: "Admin"
|
||||||
flags_title: "Flags"
|
flags_title: "Flags"
|
||||||
|
@ -20,17 +20,21 @@ en:
|
|||||||
short_date: "D MMM, YYYY"
|
short_date: "D MMM, YYYY"
|
||||||
long_date: "MMMM D, YYYY h:mma"
|
long_date: "MMMM D, YYYY h:mma"
|
||||||
|
|
||||||
datetime: &datetime
|
datetime_formats: &datetime_formats
|
||||||
month_names:
|
|
||||||
[~, January, February, March, April, May, June, July, August, September, October, November, December]
|
|
||||||
formats:
|
formats:
|
||||||
|
# Format directives: http://ruby-doc.org/core-2.2.0/Time.html#method-i-strftime
|
||||||
short: "%m-%d-%Y"
|
short: "%m-%d-%Y"
|
||||||
|
# Format directives: http://ruby-doc.org/core-2.2.0/Time.html#method-i-strftime
|
||||||
short_no_year: "%B %-d"
|
short_no_year: "%B %-d"
|
||||||
|
# Format directives: http://ruby-doc.org/core-2.2.0/Time.html#method-i-strftime
|
||||||
date_only: "%B %-d, %Y"
|
date_only: "%B %-d, %Y"
|
||||||
date:
|
date:
|
||||||
<<: *datetime
|
# Do not remove the brackets and commas and do not translate the first month name. It should be "null".
|
||||||
|
month_names:
|
||||||
|
[~, January, February, March, April, May, June, July, August, September, October, November, December]
|
||||||
|
<<: *datetime_formats
|
||||||
time:
|
time:
|
||||||
<<: *datetime
|
<<: *datetime_formats
|
||||||
|
|
||||||
title: "Discourse"
|
title: "Discourse"
|
||||||
topics: "Topics"
|
topics: "Topics"
|
||||||
@ -62,8 +66,10 @@ en:
|
|||||||
exclusion: is reserved
|
exclusion: is reserved
|
||||||
greater_than: must be greater than %{count}
|
greater_than: must be greater than %{count}
|
||||||
greater_than_or_equal_to: must be greater than or equal to %{count}
|
greater_than_or_equal_to: must be greater than or equal to %{count}
|
||||||
|
has_already_been_used: "has already been used"
|
||||||
inclusion: is not included in the list
|
inclusion: is not included in the list
|
||||||
invalid: is invalid
|
invalid: is invalid
|
||||||
|
is_invalid: "is invalid; try to be a little more descriptive"
|
||||||
less_than: must be less than %{count}
|
less_than: must be less than %{count}
|
||||||
less_than_or_equal_to: must be less than or equal to %{count}
|
less_than_or_equal_to: must be less than or equal to %{count}
|
||||||
not_a_number: is not a number
|
not_a_number: is not a number
|
||||||
@ -101,9 +107,6 @@ en:
|
|||||||
activemodel:
|
activemodel:
|
||||||
errors:
|
errors:
|
||||||
<<: *errors
|
<<: *errors
|
||||||
activerecord:
|
|
||||||
errors:
|
|
||||||
<<: *errors
|
|
||||||
|
|
||||||
bulk_invite:
|
bulk_invite:
|
||||||
file_should_be_csv: "The uploaded file should be of csv or txt format."
|
file_should_be_csv: "The uploaded file should be of csv or txt format."
|
||||||
@ -280,9 +283,7 @@ en:
|
|||||||
user:
|
user:
|
||||||
ip_address: ""
|
ip_address: ""
|
||||||
errors:
|
errors:
|
||||||
messages:
|
<<: *errors
|
||||||
is_invalid: "is invalid; try to be a little more descriptive"
|
|
||||||
has_already_been_used: "has already been used"
|
|
||||||
models:
|
models:
|
||||||
topic:
|
topic:
|
||||||
attributes:
|
attributes:
|
||||||
|
48
lib/locale_file_walker.rb
Normal file
48
lib/locale_file_walker.rb
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
require 'psych'
|
||||||
|
|
||||||
|
class LocaleFileWalker
|
||||||
|
protected
|
||||||
|
|
||||||
|
def handle_document(document)
|
||||||
|
# we want to ignore the language (first key), so let's start at -1
|
||||||
|
handle_nodes(document.root.children, -1, [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_nodes(nodes, depth, parents)
|
||||||
|
if nodes
|
||||||
|
consecutive_scalars = 0
|
||||||
|
nodes.each do |node|
|
||||||
|
consecutive_scalars = handle_node(node, depth, parents, consecutive_scalars)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_node(node, depth, parents, consecutive_scalars)
|
||||||
|
node_is_scalar = node.is_a?(Psych::Nodes::Scalar)
|
||||||
|
|
||||||
|
if node_is_scalar
|
||||||
|
handle_scalar(node, depth, parents) if valid_scalar?(depth, consecutive_scalars)
|
||||||
|
elsif node.is_a?(Psych::Nodes::Alias)
|
||||||
|
handle_alias(node, depth, parents)
|
||||||
|
elsif node.is_a?(Psych::Nodes::Mapping)
|
||||||
|
handle_mapping(node, depth, parents)
|
||||||
|
handle_nodes(node.children, depth + 1, parents.dup)
|
||||||
|
end
|
||||||
|
|
||||||
|
node_is_scalar ? consecutive_scalars + 1 : 0
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid_scalar?(depth, consecutive_scalars)
|
||||||
|
depth >= 0 && consecutive_scalars.even?
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_scalar(node, depth, parents)
|
||||||
|
parents[depth] = node.value
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_alias(node, depth, parents)
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_mapping(node, depth, parents)
|
||||||
|
end
|
||||||
|
end
|
@ -6,6 +6,7 @@
|
|||||||
# team will pull them in.
|
# team will pull them in.
|
||||||
|
|
||||||
require 'open3'
|
require 'open3'
|
||||||
|
require_relative '../lib/locale_file_walker'
|
||||||
|
|
||||||
if `which tx`.strip.empty?
|
if `which tx`.strip.empty?
|
||||||
puts '', 'The Transifex client needs to be installed to use this script.'
|
puts '', 'The Transifex client needs to be installed to use this script.'
|
||||||
@ -17,10 +18,11 @@ if `which tx`.strip.empty?
|
|||||||
exit 1
|
exit 1
|
||||||
end
|
end
|
||||||
|
|
||||||
locales = Dir.glob(File.expand_path('../../config/locales/client.*.yml', __FILE__)).map {|x| x.split('.')[-2]}.select {|x| x != 'en'}.sort.join(',')
|
languages = Dir.glob(File.expand_path('../../config/locales/client.*.yml', __FILE__))
|
||||||
|
.map { |x| x.split('.')[-2] }.select { |x| x != 'en' }.sort
|
||||||
|
|
||||||
puts 'Pulling new translations...', ''
|
puts 'Pulling new translations...', ''
|
||||||
command = "tx pull --mode=developer --language=#{locales} #{ARGV.include?('force') ? '-f' : ''}"
|
command = "tx pull --mode=developer --language=#{languages.join(',')} #{ARGV.include?('force') ? '-f' : ''}"
|
||||||
|
|
||||||
Open3.popen2e(command) do |stdin, stdout_err, wait_thr|
|
Open3.popen2e(command) do |stdin, stdout_err, wait_thr|
|
||||||
while (line = stdout_err.gets)
|
while (line = stdout_err.gets)
|
||||||
@ -46,19 +48,180 @@ END
|
|||||||
YML_DIRS = ['config/locales',
|
YML_DIRS = ['config/locales',
|
||||||
'plugins/poll/config/locales',
|
'plugins/poll/config/locales',
|
||||||
'vendor/gems/discourse_imgur/lib/discourse_imgur/locale']
|
'vendor/gems/discourse_imgur/lib/discourse_imgur/locale']
|
||||||
|
YML_FILE_PREFIXES = ['server', 'client']
|
||||||
|
|
||||||
# Add comments to the top of files
|
def yml_path(dir, prefix, language)
|
||||||
['client', 'server'].each do |base|
|
path = "../../#{dir}/#{prefix}.#{language}.yml"
|
||||||
YML_DIRS.each do |dir|
|
path = File.expand_path(path, __FILE__)
|
||||||
Dir.glob(File.expand_path("../../#{dir}/#{base}.*.yml", __FILE__)).each do |file_name|
|
File.exists?(path) ? path : nil
|
||||||
language = File.basename(file_name).match(Regexp.new("#{base}\\.([^\\.]*)\\.yml"))[1]
|
end
|
||||||
|
|
||||||
lines = File.readlines(file_name)
|
# Add comments to the top of files and replace the language (first key in YAML file)
|
||||||
lines.collect! {|line| line =~ /^[a-z_]+:$/i ? "#{language}:" : line}
|
def update_file_header(filename, language)
|
||||||
|
lines = File.readlines(filename)
|
||||||
|
lines.collect! {|line| line =~ /^[a-z_]+:$/i ? "#{language}:" : line}
|
||||||
|
|
||||||
File.open(file_name, 'w+') do |f|
|
File.open(filename, 'w+') do |f|
|
||||||
f.puts(YML_FILE_COMMENTS, '') unless lines[0][0] == '#'
|
f.puts(YML_FILE_COMMENTS, '') unless lines[0][0] == '#'
|
||||||
f.puts(lines)
|
f.puts(lines)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class YamlAliasFinder < LocaleFileWalker
|
||||||
|
def initialize
|
||||||
|
@anchors = {}
|
||||||
|
@aliases = Hash.new { |hash, key| hash[key] = [] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse_file(filename)
|
||||||
|
document = Psych.parse_file(filename)
|
||||||
|
handle_document(document)
|
||||||
|
{anchors: @anchors, aliases: @aliases}
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def handle_alias(node, depth, parents)
|
||||||
|
@aliases[node.anchor] << parents.dup
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_mapping(node, depth, parents)
|
||||||
|
if node.anchor
|
||||||
|
@anchors[parents.dup] = node.anchor
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class YamlAliasSynchronizer < LocaleFileWalker
|
||||||
|
def initialize(original_alias_data)
|
||||||
|
@anchors = original_alias_data[:anchors]
|
||||||
|
@aliases = original_alias_data[:aliases]
|
||||||
|
@used_anchors = Set.new
|
||||||
|
|
||||||
|
calculate_required_keys
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_to(filename)
|
||||||
|
stream = Psych.parse_stream(File.read(filename))
|
||||||
|
stream.children.each { |document| handle_document(document) }
|
||||||
|
|
||||||
|
add_aliases
|
||||||
|
write_yaml(stream, filename)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def calculate_required_keys
|
||||||
|
@required_keys = {}
|
||||||
|
|
||||||
|
@aliases.each_value do |key_sets|
|
||||||
|
key_sets.each do |keys|
|
||||||
|
until keys.empty?
|
||||||
|
add_needed_node(keys)
|
||||||
|
keys = keys.dup
|
||||||
|
keys.pop
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
add_needed_node([]) unless @required_keys.empty?
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_needed_node(keys)
|
||||||
|
@required_keys[keys] = {mapping: nil, scalar: nil, alias: nil}
|
||||||
|
end
|
||||||
|
|
||||||
|
def write_yaml(stream, filename)
|
||||||
|
yaml = stream.to_yaml(nil, {:line_width => -1})
|
||||||
|
|
||||||
|
File.open(filename, 'w') do |file|
|
||||||
|
file.write(yaml)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_scalar(node, depth, parents)
|
||||||
|
super(node, depth, parents)
|
||||||
|
|
||||||
|
if @required_keys.has_key?(parents)
|
||||||
|
@required_keys[parents][:scalar] = node
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_alias(node, depth, parents)
|
||||||
|
if @required_keys.has_key?(parents)
|
||||||
|
@required_keys[parents][:alias] = node
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_mapping(node, depth, parents)
|
||||||
|
if @anchors.has_key?(parents)
|
||||||
|
node.anchor = @anchors[parents]
|
||||||
|
@used_anchors.add(node.anchor)
|
||||||
|
end
|
||||||
|
|
||||||
|
if @required_keys.has_key?(parents)
|
||||||
|
@required_keys[parents][:mapping] = node
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_aliases
|
||||||
|
@used_anchors.each do |anchor|
|
||||||
|
@aliases[anchor].each do |keys|
|
||||||
|
parents = []
|
||||||
|
parent_node = @required_keys[[]]
|
||||||
|
|
||||||
|
keys.each_with_index do |key, index|
|
||||||
|
parents << key
|
||||||
|
current_node = @required_keys[parents]
|
||||||
|
is_last = index == keys.size - 1
|
||||||
|
add_node(current_node, parent_node, key, is_last ? anchor : nil)
|
||||||
|
parent_node = current_node
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_node(node, parent_node, scalar_name, anchor)
|
||||||
|
parent_mapping = parent_node[:mapping]
|
||||||
|
parent_mapping.children ||= []
|
||||||
|
|
||||||
|
if node[:scalar].nil?
|
||||||
|
node[:scalar] = Psych::Nodes::Scalar.new(scalar_name)
|
||||||
|
parent_mapping.children << node[:scalar]
|
||||||
|
end
|
||||||
|
|
||||||
|
if anchor.nil?
|
||||||
|
if node[:mapping].nil?
|
||||||
|
node[:mapping] = Psych::Nodes::Mapping.new
|
||||||
|
parent_mapping.children << node[:mapping]
|
||||||
|
end
|
||||||
|
elsif node[:alias].nil?
|
||||||
|
parent_mapping.children << Psych::Nodes::Alias.new(anchor)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_english_alias_data(dir, prefix)
|
||||||
|
filename = yml_path(dir, prefix, 'en')
|
||||||
|
filename ? YamlAliasFinder.new.parse_file(filename) : nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_anchors_and_aliases(english_alias_data, filename)
|
||||||
|
if english_alias_data
|
||||||
|
YamlAliasSynchronizer.new(english_alias_data).add_to(filename)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
YML_DIRS.each do |dir|
|
||||||
|
YML_FILE_PREFIXES.each do |prefix|
|
||||||
|
english_alias_data = get_english_alias_data(dir, prefix)
|
||||||
|
|
||||||
|
languages.each do |language|
|
||||||
|
filename = yml_path(dir, prefix, language)
|
||||||
|
|
||||||
|
if filename
|
||||||
|
add_anchors_and_aliases(english_alias_data, filename)
|
||||||
|
update_file_header(filename, language)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
require 'locale_file_walker'
|
||||||
|
|
||||||
describe "i18n integrity checks" do
|
describe "i18n integrity checks" do
|
||||||
|
|
||||||
@ -57,4 +58,40 @@ describe "i18n integrity checks" do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'keys in English locale files' do
|
||||||
|
locale_files = ['config/locales', 'plugins/**/locales']
|
||||||
|
.product(['server.en.yml', 'client.en.yml'])
|
||||||
|
.collect { |dir, filename| Dir["#{Rails.root}/#{dir}/#{filename}"] }
|
||||||
|
.flatten
|
||||||
|
.map { |path| Pathname.new(path).relative_path_from(Rails.root) }
|
||||||
|
|
||||||
|
class DuplicateKeyFinder < LocaleFileWalker
|
||||||
|
def find_duplicates(filename)
|
||||||
|
@keys_with_count = {}
|
||||||
|
|
||||||
|
document = Psych.parse_file(filename)
|
||||||
|
handle_document(document)
|
||||||
|
|
||||||
|
@keys_with_count.delete_if { |key, count| count <= 1 }.keys
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
def handle_scalar(node, depth, parents)
|
||||||
|
super(node, depth, parents)
|
||||||
|
|
||||||
|
key = parents.join('.')
|
||||||
|
@keys_with_count[key] = @keys_with_count.fetch(key, 0) + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
locale_files.each do |path|
|
||||||
|
context path do
|
||||||
|
it 'has no duplicate keys' do
|
||||||
|
duplicates = DuplicateKeyFinder.new.find_duplicates("#{Rails.root}/#{path}")
|
||||||
|
expect(duplicates).to be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
Loading…
x
Reference in New Issue
Block a user