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 +7 -0
- data/LICENSE +21 -0
- data/README.md +4 -0
- data/Rakefile +23 -0
- data/init.rb +1 -0
- data/install.rb +3 -0
- data/lib/annotranslate.rb +400 -0
- data/lib/import_export.rb +288 -0
- data/lib/version.rb +3 -0
- data/tasks/annotranslate.rake +61 -0
- data/uninstall.rb +1 -0
- metadata +56 -0
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
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,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,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: []
|