annotranslate 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 9844e84f3c73277b845c29446f59abaf97a5920d
4
+ data.tar.gz: ed9e7b2bd804335cd33699f6c526de3a8e4ceee1
5
+ SHA512:
6
+ metadata.gz: 5efe9d3b9ebeee917e8799b2d6b332c0f1c81cc7829644b1661c2cb4951c8d2bfb163af0d634122a750ae7ab394a3ed75da3b8f2fd6ad20f07b6aee146356f84
7
+ data.tar.gz: 7cbb6539e463afcf867d90d81e20141424c5e590093e20e1d8ad9a047c87907c0bf148b53eceafe0ec5a86034c23201fcddb745c9a51e4f9ccaaf9fd3d3d661f
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 Atomic Object
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,4 @@
1
+ annotranslate
2
+ =============
3
+
4
+ Rails plugin which provides annotation of translatable strings
data/Rakefile ADDED
@@ -0,0 +1,23 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rdoc/task'
4
+
5
+ desc 'Generate documentation for AnnoTranslate plugin'
6
+ Rake::RDocTask.new(:rdoc) do |rdoc|
7
+ rdoc.rdoc_dir = 'rdoc'
8
+ rdoc.title = 'AnnoTranslate - annotate translations for i18n-based rails apps'
9
+ rdoc.options << '--line-numbers' << '--inline-source' << '--webcvs=https://github.com/atomicobject/annotranslate/tree/master'
10
+ rdoc.rdoc_files.include('README.md')
11
+ rdoc.rdoc_files.include('lib/**/*.rb')
12
+ end
13
+
14
+ def git(cmd)
15
+ safe_system("git " + cmd)
16
+ end
17
+
18
+ def safe_system(cmd)
19
+ if !system(cmd)
20
+ puts "Failed: #{cmd}"
21
+ exit
22
+ end
23
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'annotranslate'
data/install.rb ADDED
@@ -0,0 +1,3 @@
1
+ unless Rails::VERSION::MAJOR >= 2 && Rails::VERSION::MINOR >= 2
2
+ raise "This version of AnnoTranslate requires Rails 2.2 or higher."
3
+ end
@@ -0,0 +1,400 @@
1
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__))
2
+
3
+ require 'rails/all'
4
+ require 'active_support'
5
+ require 'action_view/helpers/translation_helper'
6
+ require 'logger'
7
+ require 'import_export'
8
+ require 'version'
9
+
10
+ # Extentions to make internationalization (i18n) of a Rails application simpler.
11
+ # Support the method +translate+ (or shorter +t+) in models/view/controllers/mailers.
12
+ module AnnoTranslate
13
+
14
+ # Error for use within AnnoTranslate
15
+ class AnnoTranslateError < StandardError #:nodoc:
16
+ end
17
+
18
+ # Define empty logger until instanced
19
+ @@log_file = nil
20
+ @@logger = nil
21
+
22
+ # Whether to pseudo-translate all fetched strings
23
+ @@pseudo_translate = false
24
+
25
+ # Pseudo-translation text to prend to fetched strings.
26
+ # Used as a visible marker. Default is "["
27
+ @@pseudo_prepend = "["
28
+
29
+ # Pseudo-translation text to append to fetched strings.
30
+ # Used as a visible marker. Default is "]"
31
+ @@pseudo_append = "]"
32
+
33
+ # An optional callback to be notified when there are missing translations in views
34
+ @@missing_translation_callback = nil
35
+
36
+ # Plugin-specific Rails logger
37
+ def self.log
38
+ # Create logger if it doesn't exist yet
39
+ if @@logger.nil?
40
+ log_file = Rails.root.join('log', 'annotranslate.log').to_s
41
+ puts "AnnoTranslate is logging to: #{log_file}"
42
+ @@logger = Logger.new(File.open(log_file, "w", encoding: 'UTF-8'))
43
+ @@logger.info "Started new AnnoTranslate logging session!"
44
+ end
45
+ # Return the logger instance
46
+ @@logger
47
+ end
48
+
49
+ class TagHelper
50
+ include Singleton
51
+ include ActionView::Helpers::TagHelper
52
+ include ActionView::Helpers::AssetTagHelper
53
+ end
54
+
55
+ def self.tag_helper
56
+ TagHelper.instance
57
+ end
58
+
59
+ # Invokes the missing translation callback, if it is defined
60
+ def self.missing_translation_callback(exception, key, options = {}) #:nodoc:
61
+ @@missing_translation_callback.call(exception, key, options) if !@@missing_translation_callback.nil?
62
+ end
63
+
64
+ # Set an optional block that gets called when there's a missing translation within a view.
65
+ # This can be used to log missing translations in production.
66
+ #
67
+ # Block takes two required parameters:
68
+ # - exception (original I18n::MissingTranslationData that was raised for the failed translation)
69
+ # - key (key that was missing)
70
+ # - options (hash of options sent to annotranslate)
71
+ # Example:
72
+ # set_missing_translation_callback do |ex, key, options|
73
+ # logger.info("Failed to find #{key}")
74
+ # end
75
+ def self.set_missing_translation_callback(&block)
76
+ @@missing_translation_callback = block
77
+ end
78
+
79
+ def self.translate_with_annotation(scope, path, key, options={})
80
+ AnnoTranslate.log.info "translate_with_annotation(scope=#{scope}, path=#{path}, key=#{key}, options=#{options.inspect})"
81
+
82
+ scope ||= [] # guard against nil scope
83
+
84
+ # Let Rails 2.3 handle keys starting with "."
85
+ # raise AnnoTranslateError, "Skip keys with leading dot" if key.to_s.first == "."
86
+
87
+ # Keep the original options clean
88
+ original_scope = scope.dup
89
+ scoped_options = {}.merge(options)
90
+
91
+ # Raise to know if the key was found
92
+ scoped_options[:raise] = true
93
+
94
+ # Remove any default value when searching with scope
95
+ scoped_options.delete(:default)
96
+
97
+ str = nil # the string being looked for
98
+
99
+ # Apply scoping to partial keys
100
+ key = AnnoTranslate.scope_key_by_partial(key, path)
101
+
102
+ # Loop through each scope until a string is found.
103
+ # Example: starts with scope of [:blog_posts :show] then tries scope [:blog_posts] then
104
+ # without any automatically added scope ("[]").
105
+ while str.nil?
106
+ # Set scope to use for search
107
+ scoped_options[:scope] = scope
108
+
109
+ begin
110
+ # try to find key within scope (dup the options because I18n modifies the hash)
111
+ str = I18n.translate(key, scoped_options.dup)
112
+ rescue I18n::MissingTranslationData => exc
113
+ # did not find the string, remove a layer of scoping.
114
+ # break when there are no more layers to remove (pop returns nil)
115
+ break if scope.pop.nil?
116
+ end
117
+ end
118
+
119
+ # If a string is not yet found, potentially check the default locale if in fallback mode.
120
+ if str.nil? && AnnoTranslate.fallback? && (I18n.locale != I18n.default_locale) && options[:locale].nil?
121
+ # Recurse original request, but in the context of the default locale
122
+ str ||= AnnoTranslate.translate_with_scope(original_scope, key, options.merge({:locale => I18n.default_locale}))
123
+ end
124
+
125
+ # If a string was still not found, fall back to trying original request (gets default behavior)
126
+ str ||= I18n.translate(key, options)
127
+
128
+ # If pseudo-translating, prepend / append marker text
129
+ if AnnoTranslate.pseudo_translate? && !str.nil?
130
+ str = AnnoTranslate.pseudo_prepend + str + AnnoTranslate.pseudo_append
131
+ end
132
+
133
+ tag = tag_helper.content_tag('span', str, :class => 'translation_annotated', :title => key)
134
+ AnnoTranslate.log.info " => full_key=#{key}, translation=#{str}, tag=#{tag.inspect}"
135
+ tag
136
+ end
137
+
138
+ def self.scope_key_by_partial(key, path)
139
+ if key.to_s.first == "."
140
+ if path
141
+ path.gsub(%r{/_?}, ".") + key.to_s
142
+ else
143
+ error = "Cannot use t(#{key.inspect}) shortcut because path is not available"
144
+ AnnoTranslate.log.error error
145
+ raise error
146
+ end
147
+ else
148
+ key
149
+ end
150
+ end
151
+
152
+ class << AnnoTranslate
153
+
154
+ # Generic translate method that mimics <tt>I18n.translate</tt> (e.g. no automatic scoping) but includes locale fallback
155
+ # and strict mode behavior.
156
+ def translate(key, options={})
157
+ AnnoTranslate.translate_with_annotation(key, @virtual_path, options)
158
+ end
159
+
160
+ alias :t :translate
161
+ end
162
+
163
+ # When fallback mode is enabled if a key cannot be found in the set locale,
164
+ # it uses the default locale. So, for example, if an app is mostly localized
165
+ # to Spanish (:es), but a new page is added then Spanish users will continue
166
+ # to see mostly Spanish content but the English version (assuming the <tt>default_locale</tt> is :en)
167
+ # for the new page that has not yet been translated to Spanish.
168
+ def self.fallback(enable = true)
169
+ @@fallback_mode = enable
170
+ end
171
+
172
+ # If fallback mode is enabled
173
+ def self.fallback?
174
+ @@fallback_mode
175
+ end
176
+
177
+ # Toggle whether to true an exception on *all* +MissingTranslationData+ exceptions
178
+ # Useful during testing to ensure all keys are found.
179
+ # Passing +true+ enables strict mode, +false+ installs the default exception handler which
180
+ # does not raise on +MissingTranslationData+
181
+ def self.strict_mode(enable_strict = true)
182
+ @@strict_mode = enable_strict
183
+
184
+ if enable_strict
185
+ # Switch to using contributed exception handler
186
+ I18n.exception_handler = :strict_i18n_exception_handler
187
+ else
188
+ I18n.exception_handler = :default_exception_handler
189
+ end
190
+ end
191
+
192
+ # Get if it is in strict mode
193
+ def self.strict_mode?
194
+ @@strict_mode
195
+ end
196
+
197
+ # Toggle a pseudo-translation mode that will prepend / append special text
198
+ # to all fetched strings. This is useful during testing to view pages and visually
199
+ # confirm that strings have been fully extracted into locale bundles.
200
+ def self.pseudo_translate(enable = true)
201
+ @@pseudo_translate = enable
202
+ end
203
+
204
+ # If pseudo-translated is enabled
205
+ def self.pseudo_translate?
206
+ @@pseudo_translate
207
+ end
208
+
209
+ # Pseudo-translation text to prepend to fetched strings.
210
+ # Used as a visible marker. Default is "[["
211
+ def self.pseudo_prepend
212
+ @@pseudo_prepend
213
+ end
214
+
215
+ # Set the pseudo-translation text to prepend to fetched strings.
216
+ # Used as a visible marker.
217
+ def self.pseudo_prepend=(v)
218
+ @@pseudo_prepend = v
219
+ end
220
+
221
+ # Pseudo-translation text to append to fetched strings.
222
+ # Used as a visible marker. Default is "]]"
223
+ def self.pseudo_append
224
+ @@pseudo_append
225
+ end
226
+
227
+ # Set the pseudo-translation text to append to fetched strings.
228
+ # Used as a visible marker.
229
+ def self.pseudo_append=(v)
230
+ @@pseudo_append = v
231
+ end
232
+
233
+ # Additions to TestUnit to make testing i18n easier
234
+ module Assertions
235
+
236
+ # Assert that within the block there are no missing translation keys.
237
+ # This can be used in a more tailored way that the global +strict_mode+
238
+ #
239
+ # Example:
240
+ # assert_translated do
241
+ # str = "Test will fail for #{I18n.t('a_missing_key')}"
242
+ # end
243
+ #
244
+ def assert_translated(msg = nil, &block)
245
+
246
+ # Enable strict mode to force raising of MissingTranslationData
247
+ AnnoTranslate.strict_mode(true)
248
+
249
+ msg ||= "Expected no missing translation keys"
250
+
251
+ begin
252
+ yield
253
+ # Credtit for running the assertion
254
+ assert(true, msg)
255
+ rescue I18n::MissingTranslationData => e
256
+ # Fail!
257
+ error = build_message(msg, "Exception raised:\n?", e)
258
+ AnnoTranslate.log.error
259
+ assert_block(error) {false}
260
+ ensure
261
+ # uninstall strict exception handler
262
+ AnnoTranslate.strict_mode(false)
263
+ end
264
+
265
+ end
266
+ end
267
+
268
+ module I18nExtensions
269
+ # Add an strict exception handler for testing that will raise all exceptions
270
+ def strict_i18n_exception_handler(exception, locale, key, options)
271
+ # Raise *all* exceptions
272
+ raise exception
273
+ end
274
+
275
+ end
276
+ end
277
+
278
+ module ActionView #:nodoc:
279
+ class Base
280
+ # Redefine the +translate+ method in ActionView (contributed by TranslationHelper) that is
281
+ # context-aware of what view (or partial) is being rendered.
282
+ # Initial scoping will be scoped to [:controller_name :view_name]
283
+ def translate_with_annotation(key, options={})
284
+ # default to an empty scope
285
+ scope = []
286
+
287
+ # In the case of a missing translation, fall back to letting TranslationHelper
288
+ # put in span tag for a translation_missing.
289
+ begin
290
+ AnnoTranslate.translate_with_annotation(scope, @virtual_path, key, options.merge({:raise => true}))
291
+ rescue AnnoTranslate::AnnoTranslateError, I18n::MissingTranslationData => exc
292
+ # Call the original translate method
293
+ str = translate_without_annotation(key, options)
294
+
295
+ # View helper adds the translation missing span like:
296
+ # In strict mode, do not allow TranslationHelper to add "translation missing" span like:
297
+ # <span class="translation_missing">en, missing_string</span>
298
+ if str =~ /span class\=\"translation_missing\"/
299
+ # In strict mode, do not allow TranslationHelper to add "translation missing"
300
+ raise if AnnoTranslate.strict_mode?
301
+
302
+ # Invoke callback if it is defined
303
+ AnnoTranslate.missing_translation_callback(exc, key, options)
304
+ end
305
+
306
+ str
307
+ end
308
+ end
309
+
310
+ alias_method_chain :translate, :annotation
311
+ alias :t :translate
312
+ end
313
+ end
314
+
315
+ module ActionController #:nodoc:
316
+ class Base
317
+
318
+ # Add a +translate+ (or +t+) method to ActionController
319
+ def translate_with_annotation(key, options={})
320
+ AnnoTranslate.translate_with_annotation([self.controller_name, self.action_name], @virtual_path, key, options)
321
+ end
322
+
323
+ alias_method_chain :translate, :annotation
324
+ alias :t :translate
325
+ end
326
+ end
327
+
328
+ module ActiveRecord #:nodoc:
329
+ class Base
330
+ # Add a +translate+ (or +t+) method to ActiveRecord
331
+ def translate(key, options={})
332
+ AnnoTranslate.translate_with_annotation([self.class.name.underscore], @virtual_path, key, options)
333
+ end
334
+
335
+ alias :t :translate
336
+
337
+ # Add translate as a class method as well so that it can be used in validate statements, etc.
338
+ class << Base
339
+
340
+ def translate(key, options={}) #:nodoc:
341
+ AnnoTranslate.translate_with_annotation([self.name.underscore], @virtual_path, key, options)
342
+ end
343
+
344
+ alias :t :translate
345
+ end
346
+ end
347
+ end
348
+
349
+ module ActionMailer #:nodoc:
350
+ class Base
351
+
352
+ # Add a +translate+ (or +t+) method to ActionMailer
353
+ def translate(key, options={})
354
+ AnnoTranslate.translate_with_annotation([self.mailer_name, self.action_name], @virtual_path, key, options)
355
+ end
356
+
357
+ alias :t :translate
358
+ end
359
+ end
360
+
361
+ module I18n
362
+ # Install the strict exception handler for testing
363
+ extend AnnoTranslate::I18nExtensions
364
+
365
+ module Backend
366
+ module Fallbacks
367
+ def translate(locale, key, options={})
368
+ AnnoTranslate.translate_with_annotation([], @virtual_path, key, options)
369
+ end
370
+ end
371
+ end
372
+
373
+ # Override +translate+ (and +t+) method to I18n
374
+ def translate(key, options={})
375
+ AnnoTranslate.translate_with_annotation([], @virtual_path, key, options)
376
+ end
377
+
378
+ alias :t :translate
379
+ end
380
+
381
+ module ActiveSupport
382
+ module Inflector
383
+ def humanize(lower_case_and_underscored_word, options = {})
384
+ raise "ActiveSupport::Inflector.humanize is disabled!"
385
+ end
386
+ end
387
+ end
388
+
389
+ module Test # :nodoc: all
390
+ module Unit
391
+ class TestCase
392
+ include AnnoTranslate::Assertions
393
+ end
394
+ end
395
+ end
396
+
397
+ # In test environment, enable strict exception handling for missing translations
398
+ if (defined? RAILS_ENV) && (RAILS_ENV == "test")
399
+ AnnoTranslate.strict_mode(true)
400
+ end
@@ -0,0 +1,288 @@
1
+ require 'yaml'
2
+ require 'csv'
3
+ require 'rake'
4
+
5
+ module AnnoTranslate
6
+
7
+ class TranslationsExporter
8
+
9
+ def self.export(file_prefix, export_to=nil)
10
+ exporter = self.new(file_prefix, export_to)
11
+ exporter.export_translations
12
+ end
13
+
14
+ def initialize(file_prefix, export_to=nil)
15
+ # Format pertinent paths for files
16
+ here = File.expand_path(File.dirname(__FILE__))
17
+ config = File.expand_path(File.join(here, "..", "..", "config"))
18
+ @locales_folder = File.join(config, "locales")
19
+ @base_yml_file = File.join(@locales_folder, "en.yml")
20
+ @prefix = file_prefix
21
+ @translations_support = File.join(config, "translations")
22
+ @duplicates_file = File.join(@translations_support, "#{@prefix}_shared_strings.yml")
23
+ @export_folder = export_to ? export_to : File.join(@translations_support, "export")
24
+
25
+ @base_locale = YAML.load_file(@base_yml_file)
26
+ @cache = YAML.load_file(@duplicates_file)
27
+
28
+ FileUtils.rm_f Dir["#{@export_folder}/*.csv"]
29
+
30
+ # Create supported foreign languages collection
31
+ @foreign_languages = FOREIGN_LOCALES.keys.map do |code|
32
+ source_yml = File.join(@locales_folder, "#{code}.yml")
33
+ dest_csv = File.join(@export_folder, "#{@prefix}.#{code}.csv")
34
+ {code: code, name: FOREIGN_LOCALES[code], yml: source_yml, csv: dest_csv}
35
+ end
36
+ end
37
+
38
+ def export_translations
39
+ # Load English strings first to use as golden copy of all translatable strings/keys
40
+ load_english_strings
41
+
42
+ # Generate CSV export files for each foreign language
43
+ @foreign_languages.each do |lang|
44
+ puts "Exporting #{lang[:name]} (#{lang[:code]}) translations"
45
+ puts " from: #{lang[:yml]}" if File.exist? lang[:yml]
46
+ puts " using: #{@base_yml_file}"
47
+ puts " to: #{lang[:csv]}"
48
+
49
+ # Export keys/translations to the proper CSV file
50
+ CSV.open(lang[:csv], "wb", encoding: 'UTF-8') do |csv|
51
+ csv << ["Key", "String#", "English Version", "#{lang[:name]} Translation"]
52
+ index = 0
53
+ load_translations(lang).each do |id, translation|
54
+ csv << [id, index+1, @english[index].last, translation]
55
+ index += 1
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ def load_english_strings
64
+ @cache = {}
65
+ new_hash = YAML.load_file(@base_yml_file)
66
+ @english = hash_to_pairs(new_hash, @cache)
67
+ linked_keys = @cache.values.select{|r| r.count > 1}
68
+
69
+ # Create empty translations template with keys from English
70
+ @valid_ids = []
71
+ @template = []
72
+ @english.each do |id, string|
73
+ @valid_ids << replace_id_locale(id)
74
+ @template << [id, '']
75
+ end
76
+
77
+ # Report duplicated strings for sharing translations
78
+ if !linked_keys.empty?
79
+ puts "#{linked_keys.count} duplicate strings found! (see #{@duplicates_file} for details)"
80
+ else
81
+ puts "No duplicate strings detected"
82
+ end
83
+ File.open(@duplicates_file, "wb") do |cf|
84
+ cf.print YAML.dump(linked_keys)
85
+ end
86
+ puts "Found a total of #{@english.count} translatable strings"
87
+ end
88
+
89
+ def load_translations(config)
90
+ # Create from template
91
+ translations = @template.dup.map{|id, translation| [replace_id_locale(id, config[:code]), translation]}
92
+
93
+ # Merge in existing translations, if they exist
94
+ if File.exist? config[:yml]
95
+ hash_to_pairs(YAML.load_file(config[:yml])).each do |id, translation|
96
+ found_at = @valid_ids.index(replace_id_locale(id))
97
+ raise "Invalid translation ID '#{id}' found in #{config[:yml]}" unless found_at
98
+ translations[found_at] = [id, translation]
99
+ end
100
+ end
101
+
102
+ translations
103
+ end
104
+
105
+ private
106
+
107
+ def replace_id_locale(id, replacement='')
108
+ replacement += '.' if !replacement.empty? && !replacement !~ /\.$/
109
+ id.dup.sub(/^[a-z]{2,2}-?[A-Z]{0,2}\./, replacement)
110
+ end
111
+
112
+ def hash_to_pairs(h, cache={}, prefix=nil)
113
+ h.flat_map do |k,v|
114
+ k = "#{prefix}.#{k}" if prefix
115
+ case v
116
+ when Hash
117
+ hash_to_pairs v, cache, k
118
+ else
119
+ cache[v] ||= []
120
+ cache[v] << k
121
+ if cache[v].count > 1
122
+ # The string content has already been tracked; let's just make a note in the cache
123
+ # that this key also refers to the current string.
124
+ #puts "#{v.inspect} is referred to by multiple keys: #{cache[v].join(" ")}"
125
+ []
126
+ else
127
+ [[k,v]]
128
+ end
129
+ end
130
+ end
131
+ end
132
+
133
+ end
134
+
135
+ class TranslationsImporter
136
+ include Rake::DSL
137
+
138
+ def self.import(file_prefix, import_from=nil)
139
+ importer = self.new(file_prefix, import_from)
140
+ importer.import_translations
141
+ end
142
+
143
+ def initialize(file_prefix, import_from=nil)
144
+ # Format pertinent paths for files
145
+ here = File.expand_path(File.dirname(__FILE__))
146
+ config = File.expand_path(File.join(here, "../../config"))
147
+ @translations_support = File.join(config, "translations")
148
+ @import_folder = import_from ? import_from : File.join(@translations_support, "import")
149
+ @locales_folder = File.join(config, "locales")
150
+ @base_yml_file = File.join(@locales_folder, "en.yml")
151
+ @prefix = file_prefix
152
+ @duplicates_file = File.join(@translations_support, "#{@prefix}_shared_strings.yml")
153
+
154
+ @base_locale = YAML.load_file(@base_yml_file)
155
+ load_base_ids
156
+ @cache = YAML.load_file(@duplicates_file)
157
+
158
+ @foreign_languages = Dir[File.join(@import_folder, "*#{@prefix}*.csv")].map do |csv|
159
+ m = csv.match(/\.([a-z]{2,2}-?[A-Z]{0,2})\.csv$/)
160
+ raise "Failed parsing language code from #{csv}" if m.nil?
161
+ lang_code = m[1]
162
+ raise "Parsed language code '#{lang_code}' is not supported" if !FOREIGN_LOCALES[lang_code]
163
+ dest_yml = File.join(@locales_folder, "#{lang_code}.yml")
164
+ source_csv = File.join(@import_folder, "#{@prefix}.#{lang_code}.csv")
165
+ untranslated_strings_report = File.join(@import_folder, "#{@prefix}.missing.#{lang_code}.txt")
166
+ {
167
+ code: lang_code,
168
+ name: FOREIGN_LOCALES[lang_code],
169
+ yml: dest_yml,
170
+ csv: source_csv,
171
+ missing_translations: [],
172
+ untranslated_report: untranslated_strings_report,
173
+ }
174
+ end
175
+ end
176
+
177
+ def import_translations
178
+ raise "No CSV files to import\nimport folder: #{@import_folder}\n\n" if @foreign_languages.empty?
179
+ missing_translations = 0
180
+ @foreign_languages.each do |lang|
181
+ puts "Importing #{lang[:name]} (#{lang[:code]}) translations"
182
+ puts " from: #{lang[:csv]}"
183
+ puts " to: #{lang[:yml]}"
184
+ csv_to_yml(lang)
185
+ not_translated = lang[:missing_translations].count
186
+ puts " WARNING: #{not_translated} untranslated strings found!" if not_translated > 0
187
+ missing_translations += not_translated
188
+ end
189
+ puts "\n#{missing_translations} untranslated strings\nImport complete"
190
+ end
191
+
192
+ private
193
+
194
+ def load_base_ids
195
+ @valid_ids = {}
196
+ hash_to_ids(@base_locale.dup)
197
+ end
198
+
199
+ def hash_to_ids(h, prefix=nil)
200
+ h.each do |k,v|
201
+ k = "#{prefix}.#{k}" if prefix
202
+ case v
203
+ when Hash
204
+ hash_to_ids v, k
205
+ else
206
+ common_key = replace_id_locale(k)
207
+ @valid_ids[common_key] = v
208
+ end
209
+ end
210
+ end
211
+
212
+ def replace_id_locale(id, replacement='')
213
+ replacement += '.' if !replacement.empty? && !replacement !~ /\.$/
214
+ id.dup.sub(/^[a-zA-Z_-]+\./, replacement)
215
+ end
216
+
217
+ def csv_to_yml(config)
218
+ load_translations(config).tap do |translations|
219
+ result = translate_it(config, @base_locale.dup, translations, '')
220
+ File.open(config[:yml], "wb") do |cf|
221
+ cf.print YAML.dump(result)
222
+ end
223
+
224
+ # Report untranslated strings
225
+ FileUtils.rm_f config[:untranslated_report]
226
+ if !config[:missing_translations].empty?
227
+ File.open(config[:untranslated_report], "wb", encoding: 'UTF-8') do |report|
228
+ report.puts "Untranslated String ID"
229
+ report.puts "======================"
230
+ config[:missing_translations].each{|id| report.puts id}
231
+ end
232
+ puts " Missing translations report: #{config[:untranslated_report]}"
233
+ end
234
+ end
235
+ end
236
+
237
+ def load_translations(config)
238
+ {}.tap do |translations|
239
+ CSV.foreach(config[:csv], headers: true, encoding: 'UTF-8') do |row|
240
+ id = row[0] # id = row["Key"]
241
+ translation = row[3] # translation = row["Translated Version"]
242
+ raise "Invalid translation ID found: #{id} - #{translation}" if @valid_ids[replace_id_locale(id)].nil?
243
+ translations[id] = translation
244
+ end
245
+
246
+ # Populate keys for duplicated strings with common translation
247
+ @cache.each do |keys|
248
+ primary_key = keys.first
249
+ foreign_key = replace_id_locale(primary_key, config[:code])
250
+ raise "Whoa! No value for #{primary_key}" unless translations.has_key?(foreign_key)
251
+ keys.select{|k| k != primary_key}.each do |dup_key|
252
+ dup_key = replace_id_locale(dup_key, config[:code])
253
+ translations[dup_key] = translations[foreign_key]
254
+ end
255
+ end
256
+ end
257
+ end
258
+
259
+ def translate_it(config, input, translations, base_key)
260
+ input.keys.each do |key|
261
+
262
+ value = input[key]
263
+ if base_key && !base_key.empty?
264
+ full_key = "#{base_key}.#{key}"
265
+ else
266
+ full_key = key
267
+ end
268
+
269
+ case value
270
+ when Hash
271
+ translate_it(config, value, translations, full_key)
272
+ else
273
+ foreign_key = replace_id_locale(full_key, config[:code])
274
+ translation = translations[foreign_key]
275
+ if !translation || translation.empty?
276
+ config[:missing_translations] << foreign_key
277
+ end
278
+ input[key] = translation
279
+ end
280
+
281
+ end
282
+
283
+ input
284
+ end
285
+
286
+ end
287
+
288
+ end
data/lib/version.rb ADDED
@@ -0,0 +1,3 @@
1
+ module AnnoTranslate
2
+ VERSION = "0.2.0"
3
+ end
@@ -0,0 +1,61 @@
1
+ require 'yaml'
2
+ require 'csv'
3
+ require 'fileutils'
4
+ require_relative '../annotranslate'
5
+
6
+ BASE_LOCALE = 'en' unless defined? BASE_LOCALE
7
+
8
+ SUPPORTED_LOCALES =
9
+ {
10
+ 'en' => "English",
11
+ "de" => "German",
12
+ "fr" => "French",
13
+ "pt" => "Portuguese",
14
+ 'es' => "Spanish",
15
+ "ja" => "Japanese",
16
+ "zh-CN" => "Mandarin Chinese",
17
+ }
18
+
19
+ FOREIGN_LOCALES = SUPPORTED_LOCALES.delete('en')
20
+
21
+ namespace :translations do
22
+
23
+ file_prefix = "twweb"
24
+ here = File.expand_path(File.dirname(__FILE__))
25
+ root = File.expand_path(File.join(here, "..", ".."))
26
+ config_folder = File.join(root, "config")
27
+ directory(import_folder = File.join(config_folder, "translations", "import"))
28
+ directory(export_folder = File.join(config_folder, "translations", "export"))
29
+
30
+ desc "Import CSVs from #{import_folder.sub(/^#{root}/,'')}"
31
+ task :import => [import_folder] do
32
+ TranslationsImporter.import(file_prefix, import_folder)
33
+ end
34
+
35
+ desc "Export CSVs to #{export_folder.sub(/^#{root}/,'')}"
36
+ task :export => [export_folder] do
37
+ TranslationsExporter.export(file_prefix, export_folder)
38
+ end
39
+
40
+ end
41
+
42
+ # Internationalization tasks
43
+ namespace :i18n do
44
+
45
+ desc "Validates YAML locale bundles"
46
+ task :validate_yml => [:environment] do |t, args|
47
+
48
+ # Grab all the yaml bundles in config/locales
49
+ bundles = Dir.glob(File.join(RAILS_ROOT, 'config', 'locales', '**', '*.yml'))
50
+
51
+ # Attempt to load each bundle
52
+ bundles.each do |bundle|
53
+ begin
54
+ YAML.load_file( bundle )
55
+ rescue Exception => exc
56
+ puts "Error loading: #{bundle}"
57
+ puts exc.to_s
58
+ end
59
+ end
60
+ end
61
+ end
data/uninstall.rb ADDED
@@ -0,0 +1 @@
1
+ # Uninstall hook code here
metadata ADDED
@@ -0,0 +1,56 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: annotranslate
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Greg Williams
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-07-12 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Rails plugin which provides annotation and import/export of translatable
14
+ strings to/from CSV files
15
+ email: greg.williams@atomicobject.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - README.md
21
+ - Rakefile
22
+ - LICENSE
23
+ - init.rb
24
+ - install.rb
25
+ - uninstall.rb
26
+ - ./lib/annotranslate.rb
27
+ - ./lib/import_export.rb
28
+ - ./lib/version.rb
29
+ - ./tasks/annotranslate.rake
30
+ homepage: http://github.com/atomicobject/annotranslate
31
+ licenses:
32
+ - MIT
33
+ metadata: {}
34
+ post_install_message:
35
+ rdoc_options:
36
+ - --charset=UTF-8
37
+ require_paths:
38
+ - lib
39
+ - tasks
40
+ required_ruby_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - '>='
43
+ - !ruby/object:Gem::Version
44
+ version: '0'
45
+ required_rubygems_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - '>='
48
+ - !ruby/object:Gem::Version
49
+ version: '0'
50
+ requirements: []
51
+ rubyforge_project:
52
+ rubygems_version: 2.0.14
53
+ signing_key:
54
+ specification_version: 2
55
+ summary: Provides annotation and import/export for Rails translations
56
+ test_files: []