translatomatic 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +51 -0
  5. data/CODE_OF_CONDUCT.md +74 -0
  6. data/Gemfile +6 -0
  7. data/Gemfile.lock +137 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +74 -0
  10. data/Rakefile +6 -0
  11. data/bin/console +14 -0
  12. data/bin/setup +8 -0
  13. data/bin/translatomatic +6 -0
  14. data/db/database.yml +9 -0
  15. data/db/migrate/201712170000_initial.rb +23 -0
  16. data/lib/translatomatic/cli.rb +92 -0
  17. data/lib/translatomatic/config.rb +26 -0
  18. data/lib/translatomatic/converter.rb +157 -0
  19. data/lib/translatomatic/converter_stats.rb +27 -0
  20. data/lib/translatomatic/database.rb +105 -0
  21. data/lib/translatomatic/escaped_unicode.rb +90 -0
  22. data/lib/translatomatic/model/locale.rb +22 -0
  23. data/lib/translatomatic/model/text.rb +13 -0
  24. data/lib/translatomatic/model.rb +4 -0
  25. data/lib/translatomatic/option.rb +24 -0
  26. data/lib/translatomatic/resource_file/base.rb +137 -0
  27. data/lib/translatomatic/resource_file/html.rb +33 -0
  28. data/lib/translatomatic/resource_file/plist.rb +29 -0
  29. data/lib/translatomatic/resource_file/properties.rb +60 -0
  30. data/lib/translatomatic/resource_file/text.rb +28 -0
  31. data/lib/translatomatic/resource_file/xcode_strings.rb +65 -0
  32. data/lib/translatomatic/resource_file/xml.rb +64 -0
  33. data/lib/translatomatic/resource_file/yaml.rb +80 -0
  34. data/lib/translatomatic/resource_file.rb +74 -0
  35. data/lib/translatomatic/translation_result.rb +68 -0
  36. data/lib/translatomatic/translator/base.rb +47 -0
  37. data/lib/translatomatic/translator/frengly.rb +64 -0
  38. data/lib/translatomatic/translator/google.rb +30 -0
  39. data/lib/translatomatic/translator/microsoft.rb +32 -0
  40. data/lib/translatomatic/translator/my_memory.rb +55 -0
  41. data/lib/translatomatic/translator/yandex.rb +37 -0
  42. data/lib/translatomatic/translator.rb +63 -0
  43. data/lib/translatomatic/util.rb +24 -0
  44. data/lib/translatomatic/version.rb +3 -0
  45. data/lib/translatomatic.rb +27 -0
  46. data/translatomatic.gemspec +46 -0
  47. 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,4 @@
1
+ module Translatomatic::Model; end
2
+
3
+ require 'translatomatic/model/locale'
4
+ require 'translatomatic/model/text'
@@ -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