translatomatic 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|