annotranslate 0.2.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 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: []