translatomatic 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.travis.yml +51 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +137 -0
- data/LICENSE.txt +21 -0
- data/README.md +74 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/bin/translatomatic +6 -0
- data/db/database.yml +9 -0
- data/db/migrate/201712170000_initial.rb +23 -0
- data/lib/translatomatic/cli.rb +92 -0
- data/lib/translatomatic/config.rb +26 -0
- data/lib/translatomatic/converter.rb +157 -0
- data/lib/translatomatic/converter_stats.rb +27 -0
- data/lib/translatomatic/database.rb +105 -0
- data/lib/translatomatic/escaped_unicode.rb +90 -0
- data/lib/translatomatic/model/locale.rb +22 -0
- data/lib/translatomatic/model/text.rb +13 -0
- data/lib/translatomatic/model.rb +4 -0
- data/lib/translatomatic/option.rb +24 -0
- data/lib/translatomatic/resource_file/base.rb +137 -0
- data/lib/translatomatic/resource_file/html.rb +33 -0
- data/lib/translatomatic/resource_file/plist.rb +29 -0
- data/lib/translatomatic/resource_file/properties.rb +60 -0
- data/lib/translatomatic/resource_file/text.rb +28 -0
- data/lib/translatomatic/resource_file/xcode_strings.rb +65 -0
- data/lib/translatomatic/resource_file/xml.rb +64 -0
- data/lib/translatomatic/resource_file/yaml.rb +80 -0
- data/lib/translatomatic/resource_file.rb +74 -0
- data/lib/translatomatic/translation_result.rb +68 -0
- data/lib/translatomatic/translator/base.rb +47 -0
- data/lib/translatomatic/translator/frengly.rb +64 -0
- data/lib/translatomatic/translator/google.rb +30 -0
- data/lib/translatomatic/translator/microsoft.rb +32 -0
- data/lib/translatomatic/translator/my_memory.rb +55 -0
- data/lib/translatomatic/translator/yandex.rb +37 -0
- data/lib/translatomatic/translator.rb +63 -0
- data/lib/translatomatic/util.rb +24 -0
- data/lib/translatomatic/version.rb +3 -0
- data/lib/translatomatic.rb +27 -0
- data/translatomatic.gemspec +46 -0
- metadata +329 -0
@@ -0,0 +1,157 @@
|
|
1
|
+
class Translatomatic::Converter
|
2
|
+
include Translatomatic::Util
|
3
|
+
|
4
|
+
class << self
|
5
|
+
attr_reader :options
|
6
|
+
private
|
7
|
+
include Translatomatic::DefineOptions
|
8
|
+
end
|
9
|
+
|
10
|
+
define_options(
|
11
|
+
{ name: :translator, type: :string, aliases: "-t",
|
12
|
+
desc: "The translator implementation",
|
13
|
+
enum: Translatomatic::Translator.names },
|
14
|
+
{ name: :dry_run, type: :boolean, aliases: "-n", desc:
|
15
|
+
"Print actions without performing translations or writing files" }
|
16
|
+
)
|
17
|
+
|
18
|
+
# @return [Translatomatic::ConverterStats] translation statistics
|
19
|
+
attr_reader :stats
|
20
|
+
|
21
|
+
# Create a converter to translate files
|
22
|
+
#
|
23
|
+
# @param options A hash of converter and/or translator options.
|
24
|
+
def initialize(options = {})
|
25
|
+
@dry_run = options[:dry_run]
|
26
|
+
@translator = options[:translator]
|
27
|
+
if @translator.kind_of?(String) || @translator.kind_of?(Symbol)
|
28
|
+
klass = Translatomatic::Translator.find(@translator)
|
29
|
+
@translator = klass.new(options)
|
30
|
+
end
|
31
|
+
raise "translator required" unless @translator
|
32
|
+
@from_db = 0
|
33
|
+
@from_translator = 0
|
34
|
+
end
|
35
|
+
|
36
|
+
# @return [Translatomatic::ConverterStats] Translation statistics
|
37
|
+
def stats
|
38
|
+
Translatomatic::ConverterStats.new(@from_db, @from_translator)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Translate contents of source_file to the target locale.
|
42
|
+
# Automatically determines the target filename based on target locale.
|
43
|
+
#
|
44
|
+
# @param [String, Translatomatic::ResourceFile] source_file File to translate
|
45
|
+
# @param [String] to_locale The target locale, e.g. "fr"
|
46
|
+
# @return [Translatomatic::ResourceFile] The translated resource file
|
47
|
+
def translate(source_file, to_locale)
|
48
|
+
if source_file.kind_of?(Translatomatic::ResourceFile::Base)
|
49
|
+
source = source_file
|
50
|
+
else
|
51
|
+
source = Translatomatic::ResourceFile.load(source_file)
|
52
|
+
raise "unsupported file type #{source_file}" unless source
|
53
|
+
end
|
54
|
+
|
55
|
+
to_locale = parse_locale(to_locale)
|
56
|
+
target = Translatomatic::ResourceFile.load(source.path)
|
57
|
+
target.path = source.locale_path(to_locale)
|
58
|
+
target.locale = to_locale
|
59
|
+
translate_to_target(source, target)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Translates a resource file and writes results to a target resource file
|
63
|
+
#
|
64
|
+
# @param source [Translatomatic::ResourceFile] The source
|
65
|
+
# @param target [Translatomatic::ResourceFile] The file to write
|
66
|
+
# @return [Translatomatic::ResourceFile] The translated resource file
|
67
|
+
def translate_to_target(source, target)
|
68
|
+
# perform translation
|
69
|
+
log.info "translating #{source} to #{target}"
|
70
|
+
properties = translate_properties(source.properties, source.locale, target.locale)
|
71
|
+
target.properties = properties
|
72
|
+
target.save unless @dry_run
|
73
|
+
target
|
74
|
+
end
|
75
|
+
|
76
|
+
# Translate values in the hash of properties.
|
77
|
+
# Uses existing translations from the database if available.
|
78
|
+
#
|
79
|
+
# @param [Hash] properties Text to translate
|
80
|
+
# @param [String, Locale] from_locale The locale of the given properties
|
81
|
+
# @param [String, Locale] to_locale The target locale for translations
|
82
|
+
# @return [Hash] Translated properties
|
83
|
+
def translate_properties(properties, from_locale, to_locale)
|
84
|
+
from_locale = parse_locale(from_locale)
|
85
|
+
to_locale = parse_locale(to_locale)
|
86
|
+
|
87
|
+
# sanity check
|
88
|
+
return properties if from_locale.language == to_locale.language
|
89
|
+
|
90
|
+
result = Translatomatic::TranslationResult.new(properties,
|
91
|
+
from_locale, to_locale)
|
92
|
+
|
93
|
+
# find translations in database first
|
94
|
+
texts = find_database_translations(result)
|
95
|
+
result.update_db_strings(texts)
|
96
|
+
@from_db += texts.length
|
97
|
+
|
98
|
+
# send remaining unknown strings to translator
|
99
|
+
# (copy untranslated set from result)
|
100
|
+
untranslated = result.untranslated.to_a.select { |i| translatable?(i) }
|
101
|
+
@from_translator += untranslated.length
|
102
|
+
if !untranslated.empty? && !@dry_run
|
103
|
+
translated = @translator.translate(untranslated, from_locale, to_locale)
|
104
|
+
result.update_strings(untranslated, translated)
|
105
|
+
save_database_translations(result, untranslated, translated)
|
106
|
+
end
|
107
|
+
|
108
|
+
log.debug("translations from db: %d translator: %d untranslated: %d" %
|
109
|
+
[texts.length, untranslated.length, result.untranslated.length])
|
110
|
+
result.properties
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
def translatable?(string)
|
116
|
+
# don't translate numbers
|
117
|
+
!string.empty? && !string.match(/^[\d,]+$/)
|
118
|
+
end
|
119
|
+
|
120
|
+
def save_database_translations(result, untranslated, translated)
|
121
|
+
ActiveRecord::Base.transaction do
|
122
|
+
from = db_locale(result.from_locale)
|
123
|
+
to = db_locale(result.to_locale)
|
124
|
+
untranslated.zip(translated).each do |t1, t2|
|
125
|
+
save_database_translation(from, to, t1, t2)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def save_database_translation(from_locale, to_locale, t1, t2)
|
131
|
+
original_text = Translatomatic::Model::Text.find_or_create_by!(
|
132
|
+
locale: from_locale,
|
133
|
+
value: t1
|
134
|
+
)
|
135
|
+
|
136
|
+
Translatomatic::Model::Text.find_or_create_by!(
|
137
|
+
locale: to_locale,
|
138
|
+
value: t2,
|
139
|
+
from_text: original_text,
|
140
|
+
translator: @translator.class.name.demodulize
|
141
|
+
)
|
142
|
+
end
|
143
|
+
|
144
|
+
def find_database_translations(result)
|
145
|
+
from = db_locale(result.from_locale)
|
146
|
+
to = db_locale(result.to_locale)
|
147
|
+
|
148
|
+
Translatomatic::Model::Text.where({
|
149
|
+
locale: to,
|
150
|
+
from_texts_texts: { locale_id: from, value: result.untranslated.to_a }
|
151
|
+
}).joins(:from_text)
|
152
|
+
end
|
153
|
+
|
154
|
+
def db_locale(locale)
|
155
|
+
Translatomatic::Model::Locale.from_tag(locale)
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# Translation statistics
|
2
|
+
class Translatomatic::ConverterStats
|
3
|
+
|
4
|
+
# @return [Number] The total number of strings translated.
|
5
|
+
attr_reader :translations
|
6
|
+
|
7
|
+
# @return [Number] The number of translations that came from the database.
|
8
|
+
attr_reader :from_db
|
9
|
+
|
10
|
+
# @return [Number] The number of translations that came from the translator.
|
11
|
+
attr_reader :from_translator
|
12
|
+
|
13
|
+
def initialize(from_db, from_translator)
|
14
|
+
@translations = from_db + from_translator
|
15
|
+
@from_db = from_db
|
16
|
+
@from_translator = from_translator
|
17
|
+
end
|
18
|
+
|
19
|
+
def +(other)
|
20
|
+
self.class.new(@from_db + other.from_db, @from_translator + other.from_translator)
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_s
|
24
|
+
"Total translations: #{@translations} " +
|
25
|
+
"(#{@from_db} from database, #{@from_translator} from translator)"
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
|
3
|
+
class Translatomatic::Database
|
4
|
+
|
5
|
+
include Translatomatic::Util
|
6
|
+
|
7
|
+
class << self
|
8
|
+
attr_reader :options
|
9
|
+
private
|
10
|
+
include Translatomatic::DefineOptions
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(options = {})
|
14
|
+
db_config_path = db_config_path(options)
|
15
|
+
dbconfig = File.read(db_config_path)
|
16
|
+
dbconfig.gsub!(/\$HOME/, Dir.home)
|
17
|
+
dbconfig.gsub!(/\$GEM_ROOT/, GEM_ROOT)
|
18
|
+
@env = options[:database_env] || DEFAULT_ENV
|
19
|
+
@db_config = YAML::load(dbconfig) || {}
|
20
|
+
@env_config = @db_config
|
21
|
+
raise "no environment '#{@env}' in #{db_config_path}" unless @env_config[@env]
|
22
|
+
@env_config = @env_config[@env]
|
23
|
+
ActiveRecord::Base.configurations = @db_config
|
24
|
+
ActiveRecord::Tasks::DatabaseTasks.env = @env
|
25
|
+
ActiveRecord::Tasks::DatabaseTasks.db_dir = DB_PATH
|
26
|
+
ActiveRecord::Tasks::DatabaseTasks.root = DB_PATH
|
27
|
+
ActiveRecord::Tasks::DatabaseTasks.database_configuration = @db_config
|
28
|
+
create unless exists?
|
29
|
+
migrate
|
30
|
+
end
|
31
|
+
|
32
|
+
# Connect to the database
|
33
|
+
# @return [void]
|
34
|
+
def connect
|
35
|
+
ActiveRecord::Base.establish_connection(@env_config)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Disconnect from the database
|
39
|
+
# @return [void]
|
40
|
+
def disconnect
|
41
|
+
ActiveRecord::Base.remove_connection
|
42
|
+
end
|
43
|
+
|
44
|
+
# Test if the database exists
|
45
|
+
# @return [Boolean] true if the database exists
|
46
|
+
def exists?
|
47
|
+
begin
|
48
|
+
connect
|
49
|
+
ActiveRecord::Base.connection.tables
|
50
|
+
rescue
|
51
|
+
return false
|
52
|
+
end
|
53
|
+
true
|
54
|
+
end
|
55
|
+
|
56
|
+
# Run outstanding migrations against the database
|
57
|
+
# @return [void]
|
58
|
+
def migrate
|
59
|
+
connect
|
60
|
+
ActiveRecord::Migrator.migrate(MIGRATIONS_PATH)
|
61
|
+
ActiveRecord::Base.clear_cache!
|
62
|
+
log.debug "Database migrated."
|
63
|
+
end
|
64
|
+
|
65
|
+
# Create the database
|
66
|
+
# @return [void]
|
67
|
+
def create
|
68
|
+
ActiveRecord::Tasks::DatabaseTasks.create(@env_config)
|
69
|
+
log.debug "Database created."
|
70
|
+
end
|
71
|
+
|
72
|
+
# Drop the database
|
73
|
+
# @return [void]
|
74
|
+
def drop
|
75
|
+
disconnect
|
76
|
+
ActiveRecord::Tasks::DatabaseTasks.drop(@env_config)
|
77
|
+
log.debug "Database deleted."
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
DB_PATH = File.join(File.dirname(__FILE__), "..", "..", "db")
|
83
|
+
INTERNAL_DB_CONFIG = File.join(DB_PATH, "database.yml")
|
84
|
+
CUSTOM_DB_CONFIG = File.join(Dir.home, ".translatomatic", "database.yml")
|
85
|
+
DEFAULT_DB_CONFIG = File.exist?(CUSTOM_DB_CONFIG) ? CUSTOM_DB_CONFIG : INTERNAL_DB_CONFIG
|
86
|
+
MIGRATIONS_PATH = File.join(DB_PATH, "migrate")
|
87
|
+
GEM_ROOT = File.join(File.dirname(__FILE__), "..", "..")
|
88
|
+
DEFAULT_ENV = "production"
|
89
|
+
|
90
|
+
define_options(
|
91
|
+
{ name: :database_config, description: "Database config file",
|
92
|
+
default: DEFAULT_DB_CONFIG },
|
93
|
+
{ name: :database_env, description: "Database environment",
|
94
|
+
default: DEFAULT_ENV })
|
95
|
+
|
96
|
+
def db_config_path(options)
|
97
|
+
if options[:database_env] == "test"
|
98
|
+
INTERNAL_DB_CONFIG # rspec
|
99
|
+
elsif options[:database_config]
|
100
|
+
return options[:database_config]
|
101
|
+
else
|
102
|
+
DEFAULT_DB_CONFIG
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# Module to encode and decode unicode chars.
|
2
|
+
# This code is highly influced by Florian Frank's JSON gem
|
3
|
+
# @see https://github.com/jnbt/java-properties
|
4
|
+
# @see https://github.com/flori/json/
|
5
|
+
|
6
|
+
module Translatomatic::EscapedUnicode
|
7
|
+
|
8
|
+
# Decodes all unicode chars from escape sequences
|
9
|
+
# @param text [String]
|
10
|
+
# @return [String] The encoded text for chaining
|
11
|
+
def self.unescape(text)
|
12
|
+
string = text.dup
|
13
|
+
string = string.gsub(%r((?:\\[uU](?:[A-Fa-f\d]{4}))+)) do |c|
|
14
|
+
c.downcase!
|
15
|
+
bytes = EMPTY_8BIT_STRING.dup
|
16
|
+
i = 0
|
17
|
+
while c[6 * i] == ?\\ && c[6 * i + 1] == ?u
|
18
|
+
bytes << c[6 * i + 2, 2].to_i(16) << c[6 * i + 4, 2].to_i(16)
|
19
|
+
i += 1
|
20
|
+
end
|
21
|
+
bytes.encode("utf-8", "utf-16be")
|
22
|
+
end
|
23
|
+
string.force_encoding(::Encoding::UTF_8)
|
24
|
+
|
25
|
+
text.replace string
|
26
|
+
text
|
27
|
+
end
|
28
|
+
|
29
|
+
# Decodes all unicode chars into escape sequences
|
30
|
+
# @param text [String]
|
31
|
+
# @return [String] The decoded text for chaining
|
32
|
+
def self.escape(text)
|
33
|
+
string = text.dup
|
34
|
+
string.force_encoding(::Encoding::ASCII_8BIT)
|
35
|
+
string.gsub!(/["\\\x0-\x1f]/n) { |c| MAP[c] || c }
|
36
|
+
string.gsub!(/(
|
37
|
+
(?:
|
38
|
+
[\xc2-\xdf][\x80-\xbf] |
|
39
|
+
[\xe0-\xef][\x80-\xbf]{2} |
|
40
|
+
[\xf0-\xf4][\x80-\xbf]{3}
|
41
|
+
)+ |
|
42
|
+
[\x80-\xc1\xf5-\xff] # invalid
|
43
|
+
)/nx) { |c|
|
44
|
+
c.size == 1 and raise "Invalid utf8 byte: '#{c}'"
|
45
|
+
s = c.encode("utf-16be", "utf-8").unpack('H*')[0]
|
46
|
+
s.force_encoding(::Encoding::ASCII_8BIT)
|
47
|
+
s.gsub!(/.{4}/n, '\\\\u\&')
|
48
|
+
s.force_encoding(::Encoding::UTF_8)
|
49
|
+
}
|
50
|
+
string.force_encoding(::Encoding::UTF_8)
|
51
|
+
text.replace string
|
52
|
+
text
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
MAP = {
|
58
|
+
"\x0" => '\u0000',
|
59
|
+
"\x1" => '\u0001',
|
60
|
+
"\x2" => '\u0002',
|
61
|
+
"\x3" => '\u0003',
|
62
|
+
"\x4" => '\u0004',
|
63
|
+
"\x5" => '\u0005',
|
64
|
+
"\x6" => '\u0006',
|
65
|
+
"\x7" => '\u0007',
|
66
|
+
"\xb" => '\u000b',
|
67
|
+
"\xe" => '\u000e',
|
68
|
+
"\xf" => '\u000f',
|
69
|
+
"\x10" => '\u0010',
|
70
|
+
"\x11" => '\u0011',
|
71
|
+
"\x12" => '\u0012',
|
72
|
+
"\x13" => '\u0013',
|
73
|
+
"\x14" => '\u0014',
|
74
|
+
"\x15" => '\u0015',
|
75
|
+
"\x16" => '\u0016',
|
76
|
+
"\x17" => '\u0017',
|
77
|
+
"\x18" => '\u0018',
|
78
|
+
"\x19" => '\u0019',
|
79
|
+
"\x1a" => '\u001a',
|
80
|
+
"\x1b" => '\u001b',
|
81
|
+
"\x1c" => '\u001c',
|
82
|
+
"\x1d" => '\u001d',
|
83
|
+
"\x1e" => '\u001e',
|
84
|
+
"\x1f" => '\u001f',
|
85
|
+
}
|
86
|
+
|
87
|
+
EMPTY_8BIT_STRING = ''
|
88
|
+
EMPTY_8BIT_STRING.force_encoding(::Encoding::ASCII_8BIT)
|
89
|
+
|
90
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Translatomatic
|
2
|
+
module Model
|
3
|
+
class Locale < ActiveRecord::Base
|
4
|
+
has_many :texts, class_name: "Translatomatic::Model::Text"
|
5
|
+
validates_presence_of :language
|
6
|
+
validates_uniqueness_of :language, scope: [:script, :region]
|
7
|
+
|
8
|
+
class << self
|
9
|
+
include Translatomatic::Util
|
10
|
+
end
|
11
|
+
|
12
|
+
# create a locale record from an I18n::Locale::Tag object or string
|
13
|
+
def self.from_tag(tag)
|
14
|
+
tag = parse_locale(tag) if tag.kind_of?(String)
|
15
|
+
find_or_create_by!({
|
16
|
+
language: tag.language, script: tag.script, region: tag.region
|
17
|
+
})
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Translatomatic
|
2
|
+
module Model
|
3
|
+
class Text < ActiveRecord::Base
|
4
|
+
belongs_to :locale, class_name: "Translatomatic::Model::Locale"
|
5
|
+
belongs_to :from_text, class_name: "Translatomatic::Model::Text"
|
6
|
+
has_many :translations, class_name: "Translatomatic::Model::Text",
|
7
|
+
foreign_key: :from_text_id, dependent: :delete_all
|
8
|
+
|
9
|
+
validates_presence_of :value
|
10
|
+
validates_presence_of :locale
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Translatomatic
|
2
|
+
class Option
|
3
|
+
attr_reader :name, :required, :use_env, :description
|
4
|
+
|
5
|
+
def initialize(data = {})
|
6
|
+
@name = data[:name]
|
7
|
+
@required = data[:required]
|
8
|
+
@use_env = data[:use_env]
|
9
|
+
@description = data[:desc]
|
10
|
+
@data = data
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_hash
|
14
|
+
@data
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module DefineOptions
|
19
|
+
private
|
20
|
+
def define_options(*options)
|
21
|
+
@options = options.collect { |i| Translatomatic::Option.new(i) }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
# @abstract Subclasses implement different types of resource files
|
2
|
+
class Translatomatic::ResourceFile::Base
|
3
|
+
|
4
|
+
attr_accessor :locale
|
5
|
+
attr_accessor :path
|
6
|
+
|
7
|
+
# @return [Hash<String,String>] key -> value properties
|
8
|
+
attr_reader :properties
|
9
|
+
|
10
|
+
# Create a new resource file.
|
11
|
+
# If locale is unspecified, attempts to determine the locale of the file
|
12
|
+
# automatically, and if that fails, uses the default locale.
|
13
|
+
# @param [String] path Path to the file
|
14
|
+
# @param [String] locale Locale of the file contents
|
15
|
+
# @return [Translatomatic::ResourceFile::Base] the resource file.
|
16
|
+
def initialize(path, locale = nil)
|
17
|
+
@path = path.kind_of?(Pathname) ? path : Pathname.new(path)
|
18
|
+
@locale = locale || detect_locale || parse_locale(I18n.default_locale)
|
19
|
+
raise "unable to determine locale" unless @locale && @locale.language
|
20
|
+
@valid = false
|
21
|
+
@properties = {}
|
22
|
+
end
|
23
|
+
|
24
|
+
def format
|
25
|
+
self.class.name.demodulize.downcase.to_sym
|
26
|
+
end
|
27
|
+
|
28
|
+
# Create a path for the current resource file with a given locale
|
29
|
+
# @param [String] locale for the path
|
30
|
+
# @return [Pathname] The path of this resource file modified for the given locale
|
31
|
+
def locale_path(locale)
|
32
|
+
basename = path.sub_ext('').basename.to_s
|
33
|
+
|
34
|
+
extlist = extension_list
|
35
|
+
if extlist.length >= 2 && loc_idx = find_locale(extlist)
|
36
|
+
extlist[loc_idx] = locale.to_s
|
37
|
+
elsif valid_locale?(basename)
|
38
|
+
path.dirname + (locale.to_s + path.extname)
|
39
|
+
else
|
40
|
+
deunderscored = basename.sub(/_.*?$/, '')
|
41
|
+
filename = deunderscored + "_" + locale.to_s + path.extname
|
42
|
+
path.dirname + filename
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Set all properties
|
47
|
+
# @param [Hash<String,String>] properties New properties
|
48
|
+
def properties=(properties)
|
49
|
+
# use set rather that set @properties directly as subclasses override set()
|
50
|
+
properties.each do |key, value|
|
51
|
+
set(key, value)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Get the value of a property
|
56
|
+
# @param [String] name The name of the property
|
57
|
+
# @return [String] The value of the property
|
58
|
+
def get(name)
|
59
|
+
@properties[name]
|
60
|
+
end
|
61
|
+
|
62
|
+
# Set a property
|
63
|
+
# @param [String] key The name of the property
|
64
|
+
# @param [String] value The new value of the property
|
65
|
+
# @return [String] The new value of the property
|
66
|
+
def set(name, value)
|
67
|
+
@properties[name] = value
|
68
|
+
end
|
69
|
+
|
70
|
+
# Test if the current resource file is valid
|
71
|
+
# @return true if the current file is valid
|
72
|
+
def valid?
|
73
|
+
@valid
|
74
|
+
end
|
75
|
+
|
76
|
+
# Save the resource file.
|
77
|
+
# @param [Pathname] target The destination path
|
78
|
+
# @return [void]
|
79
|
+
def save(target = path)
|
80
|
+
raise "save(path) must be implemented by subclass"
|
81
|
+
end
|
82
|
+
|
83
|
+
# @return [String] String representation of this file
|
84
|
+
def to_s
|
85
|
+
"#{path.basename.to_s} (#{locale})"
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
include Translatomatic::Util
|
91
|
+
|
92
|
+
# detect locale from filename
|
93
|
+
def detect_locale
|
94
|
+
tag = nil
|
95
|
+
basename = path.sub_ext('').basename.to_s
|
96
|
+
directory = path.dirname.basename.to_s
|
97
|
+
extlist = extension_list
|
98
|
+
|
99
|
+
if basename.match(/_([-\w]{2,})$/i)
|
100
|
+
# locale after underscore in filename
|
101
|
+
tag = $1
|
102
|
+
elsif directory.match(/^([-\w]+)\.lproj$/)
|
103
|
+
# xcode localized strings
|
104
|
+
tag = $1
|
105
|
+
elsif extlist.length >= 2 && loc_idx = find_locale(extlist)
|
106
|
+
# multiple parts to extension, e.g. index.html.en
|
107
|
+
tag = extlist[loc_idx]
|
108
|
+
elsif valid_locale?(basename)
|
109
|
+
# try to match on entire basename
|
110
|
+
# (support for rails en.yml)
|
111
|
+
tag = basename
|
112
|
+
end
|
113
|
+
|
114
|
+
tag ? parse_locale(tag, true) : nil
|
115
|
+
end
|
116
|
+
|
117
|
+
# test if the list of strings contains a valid locale
|
118
|
+
# return the index to the locale, or nil if no locales found
|
119
|
+
def find_locale(list)
|
120
|
+
list.find_index { |i| valid_locale?(i) }
|
121
|
+
end
|
122
|
+
|
123
|
+
# ext_sub() only removes the last extension
|
124
|
+
def strip_extensions
|
125
|
+
filename = path.basename.to_s
|
126
|
+
filename.sub!(/\..*$/, '')
|
127
|
+
path.parent + filename
|
128
|
+
end
|
129
|
+
|
130
|
+
# for index.html.de, returns ['html', 'de']
|
131
|
+
def extension_list
|
132
|
+
filename = path.basename.to_s
|
133
|
+
idx = filename.index('.')
|
134
|
+
idx && idx < filename.length - 1 ? filename[idx + 1..-1].split('.') : []
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Translatomatic::ResourceFile
|
2
|
+
class HTML < XML
|
3
|
+
|
4
|
+
def self.extensions
|
5
|
+
%w{html htm shtml}
|
6
|
+
end
|
7
|
+
|
8
|
+
# (see Translatomatic::ResourceFile::Base#locale_path)
|
9
|
+
def locale_path(locale)
|
10
|
+
extlist = extension_list
|
11
|
+
if extlist.length >= 2 && loc_idx = find_locale(extlist)
|
12
|
+
# part of the extension is the locale
|
13
|
+
# replace that part with the new locale
|
14
|
+
extlist[loc_idx] = locale.to_s
|
15
|
+
new_extension = extlist.join(".")
|
16
|
+
return strip_extensions.sub_ext("." + new_extension)
|
17
|
+
else
|
18
|
+
# add locale extension
|
19
|
+
ext = path.extname
|
20
|
+
path.sub_ext("#{ext}." + locale.to_s)
|
21
|
+
end
|
22
|
+
|
23
|
+
# fall back to base functionality
|
24
|
+
#super(locale)
|
25
|
+
end
|
26
|
+
|
27
|
+
# (see Translatomatic::ResourceFile::Base#save(path))
|
28
|
+
def save(target = path)
|
29
|
+
target.write(@doc.to_html) if @doc
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Translatomatic::ResourceFile
|
2
|
+
class Plist < XML
|
3
|
+
|
4
|
+
def self.extensions
|
5
|
+
%w{plist}
|
6
|
+
end
|
7
|
+
|
8
|
+
# (see Translatomatic::ResourceFile::Base#locale_path)
|
9
|
+
# @note localization files in XCode use the following file name
|
10
|
+
# convention: Project/locale.lproj/filename
|
11
|
+
# @todo refactor this and xcode_strings.rb to use the same code
|
12
|
+
def locale_path(locale)
|
13
|
+
if path.to_s.match(/\/([-\w]+).lproj\/.+.plist$/)
|
14
|
+
# xcode style
|
15
|
+
filename = path.basename
|
16
|
+
path.parent.parent + (locale.to_s + ".lproj") + filename
|
17
|
+
else
|
18
|
+
super(locale)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def text_nodes_xpath
|
25
|
+
'//*[not(self::key)]/text()'
|
26
|
+
end
|
27
|
+
|
28
|
+
end # class
|
29
|
+
end # module
|