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.
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