i18n-js 2.1.2 → 3.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. checksums.yaml +7 -0
  2. data/.editorconfig +24 -0
  3. data/.github/FUNDING.yml +3 -0
  4. data/.github/workflows/tests.yaml +106 -0
  5. data/.gitignore +6 -4
  6. data/.npmignore +27 -0
  7. data/Appraisals +52 -0
  8. data/CHANGELOG.md +575 -0
  9. data/Gemfile +1 -1
  10. data/README.md +1099 -0
  11. data/Rakefile +19 -7
  12. data/app/assets/javascripts/i18n/filtered.js.erb +23 -0
  13. data/app/assets/javascripts/i18n/shims.js +240 -0
  14. data/app/assets/javascripts/i18n/translations.js +3 -0
  15. data/app/assets/javascripts/i18n.js +1095 -0
  16. data/gemfiles/i18n_0_6.gemfile +7 -0
  17. data/gemfiles/i18n_0_7.gemfile +7 -0
  18. data/gemfiles/i18n_0_8.gemfile +7 -0
  19. data/gemfiles/i18n_0_9.gemfile +7 -0
  20. data/gemfiles/i18n_1_0.gemfile +7 -0
  21. data/gemfiles/i18n_1_1.gemfile +7 -0
  22. data/gemfiles/i18n_1_10.gemfile +7 -0
  23. data/gemfiles/i18n_1_2.gemfile +7 -0
  24. data/gemfiles/i18n_1_3.gemfile +7 -0
  25. data/gemfiles/i18n_1_4.gemfile +7 -0
  26. data/gemfiles/i18n_1_5.gemfile +7 -0
  27. data/gemfiles/i18n_1_6.gemfile +7 -0
  28. data/gemfiles/i18n_1_7.gemfile +7 -0
  29. data/gemfiles/i18n_1_8.gemfile +7 -0
  30. data/gemfiles/i18n_1_9.gemfile +7 -0
  31. data/i18n-js.gemspec +13 -10
  32. data/i18njs.png +0 -0
  33. data/lib/i18n/js/dependencies.rb +67 -0
  34. data/lib/i18n/js/engine.rb +87 -0
  35. data/lib/i18n/js/fallback_locales.rb +70 -0
  36. data/lib/i18n/js/formatters/base.rb +25 -0
  37. data/lib/i18n/js/formatters/js.rb +39 -0
  38. data/lib/i18n/js/formatters/json.rb +13 -0
  39. data/lib/{i18n-js → i18n/js}/middleware.rb +32 -9
  40. data/lib/i18n/js/private/config_store.rb +31 -0
  41. data/lib/i18n/js/private/hash_with_symbol_keys.rb +36 -0
  42. data/lib/i18n/js/segment.rb +81 -0
  43. data/lib/i18n/js/utils.rb +91 -0
  44. data/lib/i18n/js/version.rb +7 -0
  45. data/lib/i18n/js.rb +274 -0
  46. data/lib/i18n-js.rb +1 -177
  47. data/lib/rails/generators/i18n/js/config/config_generator.rb +19 -0
  48. data/{config → lib/rails/generators/i18n/js/config/templates}/i18n-js.yml +11 -6
  49. data/lib/tasks/export.rake +8 -0
  50. data/package.json +25 -0
  51. data/spec/fixtures/custom_path.yml +5 -0
  52. data/spec/fixtures/default.yml +5 -0
  53. data/spec/fixtures/erb.yml +5 -0
  54. data/spec/fixtures/except_condition.yml +7 -0
  55. data/spec/fixtures/js_available_locales_custom.yml +1 -0
  56. data/spec/fixtures/js_export_dir_custom.yml +7 -0
  57. data/spec/fixtures/js_export_dir_none.yml +6 -0
  58. data/spec/fixtures/js_extend_parent.yml +6 -0
  59. data/spec/fixtures/js_extend_segment.yml +6 -0
  60. data/spec/fixtures/js_file_per_locale.yml +7 -0
  61. data/spec/fixtures/js_file_per_locale_with_fallbacks_as_default_locale_symbol.yml +4 -0
  62. data/spec/fixtures/js_file_per_locale_with_fallbacks_as_hash.yml +6 -0
  63. data/spec/fixtures/js_file_per_locale_with_fallbacks_as_locale.yml +4 -0
  64. data/spec/fixtures/js_file_per_locale_with_fallbacks_as_locale_without_fallback_translations.yml +4 -0
  65. data/spec/fixtures/js_file_per_locale_with_fallbacks_enabled.yml +4 -0
  66. data/spec/fixtures/js_file_per_locale_without_fallbacks.yml +4 -0
  67. data/spec/fixtures/js_file_with_namespace_prefix_and_pretty_print.yml +9 -0
  68. data/spec/fixtures/js_sort_translation_keys_false.yml +6 -0
  69. data/spec/fixtures/js_sort_translation_keys_true.yml +6 -0
  70. data/spec/fixtures/json_only.yml +18 -0
  71. data/spec/{resources → fixtures}/locales.yml +58 -1
  72. data/spec/fixtures/merge_plurals.yml +6 -0
  73. data/spec/fixtures/merge_plurals_with_no_overrides.yml +4 -0
  74. data/spec/fixtures/merge_plurals_with_partial_overrides.yml +4 -0
  75. data/spec/fixtures/millions.yml +4 -0
  76. data/spec/fixtures/multiple_conditions.yml +7 -0
  77. data/spec/fixtures/multiple_conditions_per_locale.yml +7 -0
  78. data/spec/fixtures/multiple_files.yml +7 -0
  79. data/spec/{resources → fixtures}/no_config.yml +0 -0
  80. data/spec/fixtures/no_scope.yml +4 -0
  81. data/spec/fixtures/simple_scope.yml +5 -0
  82. data/spec/js/currency.spec.js +62 -0
  83. data/spec/js/current_locale.spec.js +19 -0
  84. data/spec/js/dates.spec.js +276 -0
  85. data/spec/js/defaults.spec.js +31 -0
  86. data/spec/js/extend.spec.js +110 -0
  87. data/spec/js/interpolation.spec.js +124 -0
  88. data/spec/js/jasmine/MIT.LICENSE +20 -0
  89. data/spec/js/jasmine/jasmine-html.js +190 -0
  90. data/spec/js/jasmine/jasmine.css +166 -0
  91. data/spec/js/jasmine/jasmine.js +2476 -0
  92. data/spec/js/jasmine/jasmine_favicon.png +0 -0
  93. data/spec/js/json_parsable.spec.js +14 -0
  94. data/spec/js/locales.spec.js +31 -0
  95. data/spec/js/localization.spec.js +78 -0
  96. data/spec/js/numbers.spec.js +174 -0
  97. data/spec/js/placeholder.spec.js +24 -0
  98. data/spec/js/pluralization.spec.js +228 -0
  99. data/spec/js/prepare_options.spec.js +41 -0
  100. data/spec/js/require.js +2083 -0
  101. data/spec/js/specs.html +49 -0
  102. data/spec/js/specs_requirejs.html +72 -0
  103. data/spec/js/translate.spec.js +304 -0
  104. data/spec/js/translations.js +188 -0
  105. data/spec/js/utility_functions.spec.js +20 -0
  106. data/spec/ruby/i18n/js/fallback_locales_spec.rb +84 -0
  107. data/spec/ruby/i18n/js/segment_spec.rb +286 -0
  108. data/spec/ruby/i18n/js/utils_spec.rb +138 -0
  109. data/spec/ruby/i18n/js_spec.rb +797 -0
  110. data/spec/spec_helper.rb +75 -14
  111. data/yarn.lock +138 -0
  112. metadata +225 -96
  113. data/.rspec +0 -1
  114. data/Gemfile.lock +0 -51
  115. data/README.rdoc +0 -305
  116. data/lib/i18n-js/engine.rb +0 -62
  117. data/lib/i18n-js/railtie.rb +0 -13
  118. data/lib/i18n-js/rake.rb +0 -16
  119. data/lib/i18n-js/version.rb +0 -10
  120. data/spec/i18n_spec.js +0 -768
  121. data/spec/i18n_spec.rb +0 -205
  122. data/spec/resources/custom_path.yml +0 -4
  123. data/spec/resources/default.yml +0 -4
  124. data/spec/resources/js_file_per_locale.yml +0 -3
  125. data/spec/resources/multiple_conditions.yml +0 -6
  126. data/spec/resources/multiple_files.yml +0 -6
  127. data/spec/resources/no_scope.yml +0 -3
  128. data/spec/resources/simple_scope.yml +0 -4
  129. data/vendor/assets/javascripts/i18n/translations.js.erb +0 -7
  130. data/vendor/assets/javascripts/i18n.js +0 -450
@@ -0,0 +1,81 @@
1
+ require "i18n/js/private/hash_with_symbol_keys"
2
+ require "i18n/js/formatters/js"
3
+ require "i18n/js/formatters/json"
4
+
5
+ module I18n
6
+ module JS
7
+
8
+ # Class which enscapulates a translations hash and outputs a single JSON translation file
9
+ class Segment
10
+ OPTIONS = [:namespace, :pretty_print, :js_extend, :prefix, :suffix, :sort_translation_keys, :json_only].freeze
11
+ LOCALE_INTERPOLATOR = /%\{locale\}/
12
+
13
+ attr_reader *([:file, :translations] | OPTIONS)
14
+
15
+ def initialize(file, translations, options = {})
16
+ @file = file
17
+ # `#slice` will be used
18
+ # But when activesupport is absent,
19
+ # the core extension from `i18n` gem will be used instead
20
+ # And it's causing errors (at least in test)
21
+ #
22
+ # So the input is wrapped by our class for better `#slice`
23
+ @translations = Private::HashWithSymbolKeys.new(translations)
24
+ @namespace = options[:namespace] || 'I18n'
25
+ @pretty_print = !!options[:pretty_print]
26
+ @js_extend = options.key?(:js_extend) ? !!options[:js_extend] : true
27
+ @prefix = options.key?(:prefix) ? options[:prefix] : nil
28
+ @suffix = options.key?(:suffix) ? options[:suffix] : nil
29
+ @sort_translation_keys = options.key?(:sort_translation_keys) ? !!options[:sort_translation_keys] : true
30
+ @json_only = options.key?(:json_only) ? !!options[:json_only] : false
31
+ end
32
+
33
+ # Saves JSON file containing translations
34
+ def save!
35
+ if @file =~ LOCALE_INTERPOLATOR
36
+ I18n::JS.js_available_locales.each do |locale|
37
+ write_file(file_for_locale(locale), @translations.slice(locale))
38
+ end
39
+ else
40
+ write_file
41
+ end
42
+ end
43
+
44
+ protected
45
+
46
+ def file_for_locale(locale)
47
+ @file.gsub(LOCALE_INTERPOLATOR, locale.to_s)
48
+ end
49
+
50
+ def write_file(_file = @file, _translations = @translations)
51
+ FileUtils.mkdir_p File.dirname(_file)
52
+ _translations = Utils.deep_key_sort(_translations) if @sort_translation_keys
53
+ _translations = Utils.deep_remove_procs(_translations)
54
+ contents = formatter.format(_translations)
55
+
56
+ return if File.exist?(_file) && File.read(_file) == contents
57
+
58
+ File.open(_file, "w+") do |f|
59
+ f << contents
60
+ end
61
+ end
62
+
63
+ def formatter
64
+ if @json_only
65
+ Formatters::JSON.new(**formatter_options)
66
+ else
67
+ Formatters::JS.new(**formatter_options)
68
+ end
69
+ end
70
+
71
+ def formatter_options
72
+ { js_extend: @js_extend,
73
+ namespace: @namespace,
74
+ pretty_print: @pretty_print,
75
+ prefix: @prefix,
76
+ suffix: @suffix
77
+ }
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,91 @@
1
+ module I18n
2
+ module JS
3
+ module Utils
4
+ PLURAL_KEYS = %i[zero one two few many other].freeze
5
+
6
+ # Based on deep_merge by Stefan Rusterholz, see <https://www.ruby-forum.com/topic/142809>.
7
+ # This method is used to handle I18n fallbacks. Given two equivalent path nodes in two locale trees:
8
+ # 1. If the node in the current locale appears to be an I18n pluralization (:one, :other, etc.),
9
+ # use the node, but merge in any missing/non-nil keys from the fallback (default) locale.
10
+ # 2. Else if both nodes are Hashes, combine (merge) the key-value pairs of the two nodes into one,
11
+ # prioritizing the current locale.
12
+ # 3. Else if either node is nil, use the other node.
13
+ PLURAL_MERGER = proc do |_key, v1, v2|
14
+ v1 || v2
15
+ end
16
+ MERGER = proc do |_key, v1, v2|
17
+ if Hash === v1 && Hash === v2
18
+ if (v2.keys - PLURAL_KEYS).empty?
19
+ slice(v2.merge(v1, &PLURAL_MERGER), v2.keys)
20
+ else
21
+ v1.merge(v2, &MERGER)
22
+ end
23
+ else
24
+ v2 || v1
25
+ end
26
+ end
27
+
28
+ HASH_NIL_VALUE_CLEANER_PROC = proc do |k, v|
29
+ v.kind_of?(Hash) ? (v.delete_if(&HASH_NIL_VALUE_CLEANER_PROC); false) : v.nil?
30
+ end
31
+
32
+ def self.slice(hash, keys)
33
+ if hash.respond_to?(:slice) # ruby 2.5 onwards
34
+ hash.slice(*keys)
35
+ else
36
+ hash.select {|key, _| keys.include?(key)}
37
+ end
38
+ end
39
+
40
+ def self.strip_keys_with_nil_values(hash)
41
+ hash.dup.delete_if(&HASH_NIL_VALUE_CLEANER_PROC)
42
+ end
43
+
44
+ def self.deep_merge(target_hash, hash) # :nodoc:
45
+ target_hash.merge(hash, &MERGER)
46
+ end
47
+
48
+ def self.deep_merge!(target_hash, hash) # :nodoc:
49
+ target_hash.merge!(hash, &MERGER)
50
+ end
51
+
52
+ def self.deep_reject(hash, scopes = [], &block)
53
+ hash.each_with_object({}) do |(k, v), memo|
54
+ unless block.call(k, v, scopes + [k.to_s])
55
+ memo[k] = v.kind_of?(Hash) ? deep_reject(v, scopes + [k.to_s], &block) : v
56
+ end
57
+ end
58
+ end
59
+
60
+ def self.scopes_match?(scopes1, scopes2)
61
+ if scopes1.length == scopes2.length
62
+ [scopes1, scopes2].transpose.all? do |scope1, scope2|
63
+ scope1.to_s == '*' || scope2.to_s == '*' || scope1.to_s == scope2.to_s
64
+ end
65
+ end
66
+ end
67
+
68
+ def self.deep_key_sort(hash)
69
+ # Avoid things like `true` or `1` from YAML which causes error
70
+ hash.keys.sort {|a, b| a.to_s <=> b.to_s}.
71
+ each_with_object({}) do |key, seed|
72
+ value = hash[key]
73
+ seed[key] = value.is_a?(Hash) ? deep_key_sort(value) : value
74
+ end
75
+ end
76
+
77
+ def self.deep_remove_procs(hash)
78
+ # procs exist in `i18n.plural.rule` as pluralizer
79
+ # But having it in translation causes the exported JS/JSON changes every time
80
+ # https://github.com/ruby-i18n/i18n/blob/v1.8.7/lib/i18n/backend/pluralization.rb#L51
81
+ hash.keys.
82
+ each_with_object({}) do |key, seed|
83
+ value = hash[key]
84
+ next if value.is_a?(Proc)
85
+
86
+ seed[key] = value.is_a?(Hash) ? deep_remove_procs(value) : value
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module I18n
4
+ module JS
5
+ VERSION = "3.9.2"
6
+ end
7
+ end
data/lib/i18n/js.rb ADDED
@@ -0,0 +1,274 @@
1
+ require "yaml"
2
+ require "fileutils"
3
+ require "i18n"
4
+
5
+ require "i18n/js/utils"
6
+ require "i18n/js/private/hash_with_symbol_keys"
7
+ require "i18n/js/private/config_store"
8
+
9
+ module I18n
10
+ module JS
11
+ require "i18n/js/dependencies"
12
+ require "i18n/js/fallback_locales"
13
+ require "i18n/js/segment"
14
+ if JS::Dependencies.rails?
15
+ require "i18n/js/middleware"
16
+ require "i18n/js/engine"
17
+ end
18
+
19
+ DEFAULT_CONFIG_PATH = "config/i18n-js.yml"
20
+ DEFAULT_EXPORT_DIR_PATH = "public/javascripts"
21
+
22
+ # The configuration file. This defaults to the `config/i18n-js.yml` file.
23
+ #
24
+ def self.config_file_path
25
+ @config_file_path ||= DEFAULT_CONFIG_PATH
26
+ end
27
+
28
+ def self.config_file_path=(new_path)
29
+ @config_file_path = new_path
30
+ # new config file path = need to re-read config from new file
31
+ Private::ConfigStore.instance.flush_cache
32
+ end
33
+
34
+ # Allow using a different backend than the one globally configured
35
+ def self.backend
36
+ @backend ||= I18n.backend
37
+ end
38
+
39
+ def self.backend=(alternative_backend)
40
+ @backend = alternative_backend
41
+ end
42
+
43
+ # Export translations to JavaScript, considering settings
44
+ # from configuration file
45
+ def self.export
46
+ export_i18n_js
47
+
48
+ translation_segments.each(&:save!)
49
+ end
50
+
51
+ def self.segment_for_scope(scope, exceptions)
52
+ if scope == "*"
53
+ exclude(translations, exceptions)
54
+ else
55
+ scoped_translations(scope, exceptions)
56
+ end
57
+ end
58
+
59
+ def self.configured_segments
60
+ config[:translations].inject([]) do |segments, options_hash|
61
+ options_hash_with_symbol_keys = Private::HashWithSymbolKeys.new(options_hash)
62
+ file = options_hash_with_symbol_keys[:file]
63
+ only = options_hash_with_symbol_keys[:only] || '*'
64
+ exceptions = [options_hash_with_symbol_keys[:except] || []].flatten
65
+
66
+ result = segment_for_scope(only, exceptions)
67
+
68
+ merge_with_fallbacks!(result) if fallbacks
69
+
70
+ unless result.empty?
71
+ segments << Segment.new(
72
+ file,
73
+ result,
74
+ extract_segment_options(options_hash_with_symbol_keys),
75
+ )
76
+ end
77
+
78
+ segments
79
+ end
80
+ end
81
+
82
+ # deep_merge! given result with result for fallback locale
83
+ def self.merge_with_fallbacks!(result)
84
+ js_available_locales.each do |locale|
85
+ fallback_locales = FallbackLocales.new(fallbacks, locale)
86
+ fallback_locales.each do |fallback_locale|
87
+ # `result[fallback_locale]` could be missing
88
+ result[locale] = Utils.deep_merge(result[fallback_locale] || {}, result[locale] || {})
89
+ end
90
+ end
91
+ end
92
+
93
+ def self.filtered_translations
94
+ translations = {}.tap do |result|
95
+ translation_segments.each do |segment|
96
+ Utils.deep_merge!(result, segment.translations)
97
+ end
98
+ end
99
+ return Utils.deep_key_sort(translations) if I18n::JS.sort_translation_keys?
100
+ translations
101
+ end
102
+
103
+ def self.translation_segments
104
+ if config_file_exists? && config[:translations]
105
+ configured_segments
106
+ else
107
+ [Segment.new("#{DEFAULT_EXPORT_DIR_PATH}/translations.js", translations)]
108
+ end
109
+ end
110
+
111
+ # Load configuration file for partial exporting and
112
+ # custom output directory
113
+ def self.config
114
+ Private::ConfigStore.instance.fetch do
115
+ if config_file_exists?
116
+ erb_result_from_yaml_file = ERB.new(File.read(config_file_path)).result
117
+ Private::HashWithSymbolKeys.new(
118
+ (::YAML.load(erb_result_from_yaml_file) || {})
119
+ )
120
+ else
121
+ Private::HashWithSymbolKeys.new({})
122
+ end.freeze
123
+ end
124
+ end
125
+
126
+ # @api private
127
+ # Check if configuration file exist
128
+ def self.config_file_exists?
129
+ File.file? config_file_path
130
+ end
131
+
132
+ def self.scoped_translations(scopes, exceptions = []) # :nodoc:
133
+ result = {}
134
+
135
+ [scopes].flatten.each do |scope|
136
+ translations_without_exceptions = exclude(translations, exceptions)
137
+ filtered_translations = filter(translations_without_exceptions, scope) || {}
138
+
139
+ Utils.deep_merge!(result, filtered_translations)
140
+ end
141
+
142
+ result
143
+ end
144
+
145
+ # Exclude keys from translations listed in the `except:` section in the config file
146
+ def self.exclude(translations, exceptions)
147
+ return translations if exceptions.empty?
148
+
149
+ exceptions.inject(translations) do |memo, exception|
150
+ exception_scopes = exception.to_s.split(".")
151
+ Utils.deep_reject(memo) do |key, value, scopes|
152
+ Utils.scopes_match?(scopes, exception_scopes)
153
+ end
154
+ end
155
+ end
156
+
157
+ # Filter translations according to the specified scope.
158
+ def self.filter(translations, scopes)
159
+ scopes = scopes.split(".") if scopes.is_a?(String)
160
+ scopes = scopes.clone
161
+ scope = scopes.shift
162
+
163
+ if scope == "*"
164
+ results = {}
165
+ translations.each do |scope, translations|
166
+ tmp = scopes.empty? ? translations : filter(translations, scopes)
167
+ results[scope.to_sym] = tmp unless tmp.nil?
168
+ end
169
+ return results
170
+ elsif translations.respond_to?(:key?) && translations.key?(scope.to_sym)
171
+ return {scope.to_sym => scopes.empty? ? translations[scope.to_sym] : filter(translations[scope.to_sym], scopes)}
172
+ end
173
+ nil
174
+ end
175
+
176
+ # Initialize and return translations
177
+ def self.translations
178
+ self.backend.instance_eval do
179
+ init_translations unless initialized?
180
+ # When activesupport is absent,
181
+ # the core extension (`#slice`) from `i18n` gem will be used instead
182
+ # And it's causing errors (at least in test)
183
+ #
184
+ # So the input is wrapped by our class for better `#slice`
185
+ Private::HashWithSymbolKeys.new(translations).
186
+ slice(*::I18n::JS.js_available_locales).
187
+ to_h
188
+ end
189
+ end
190
+
191
+ def self.use_fallbacks?
192
+ fallbacks != false
193
+ end
194
+
195
+ def self.json_only
196
+ config.fetch(:json_only) do
197
+ # default value
198
+ false
199
+ end
200
+ end
201
+
202
+ def self.fallbacks
203
+ config.fetch(:fallbacks) do
204
+ # default value
205
+ true
206
+ end
207
+ end
208
+
209
+ def self.js_extend
210
+ config.fetch(:js_extend) do
211
+ # default value
212
+ true
213
+ end
214
+ end
215
+
216
+ # Get all available locales.
217
+ #
218
+ # @return [Array<Symbol>] the locales.
219
+ def self.js_available_locales
220
+ config.fetch(:js_available_locales) do
221
+ # default value
222
+ I18n.available_locales
223
+ end.map(&:to_sym)
224
+ end
225
+
226
+ def self.sort_translation_keys?
227
+ @sort_translation_keys ||= (config[:sort_translation_keys]) if config.key?(:sort_translation_keys)
228
+ @sort_translation_keys = true if @sort_translation_keys.nil?
229
+ @sort_translation_keys
230
+ end
231
+
232
+ def self.sort_translation_keys=(value)
233
+ @sort_translation_keys = !!value
234
+ end
235
+
236
+ def self.extract_segment_options(options)
237
+ segment_options = Private::HashWithSymbolKeys.new({
238
+ js_extend: js_extend,
239
+ sort_translation_keys: sort_translation_keys?,
240
+ json_only: json_only
241
+ }).freeze
242
+ segment_options.merge(options.slice(*Segment::OPTIONS))
243
+ end
244
+
245
+ ### Export i18n.js
246
+ begin
247
+
248
+ # Copy i18n.js
249
+ def self.export_i18n_js
250
+ return unless export_i18n_js_dir_path.is_a? String
251
+
252
+ FileUtils.mkdir_p(export_i18n_js_dir_path)
253
+
254
+ i18n_js_path = File.expand_path('../../../app/assets/javascripts/i18n.js', __FILE__)
255
+ destination_path = File.expand_path("i18n.js", export_i18n_js_dir_path)
256
+ return if File.exist?(destination_path) && FileUtils.identical?(i18n_js_path, destination_path)
257
+
258
+ FileUtils.cp(i18n_js_path, export_i18n_js_dir_path)
259
+ end
260
+
261
+ def self.export_i18n_js_dir_path
262
+ @export_i18n_js_dir_path ||= (config[:export_i18n_js] || :none) if config.key?(:export_i18n_js)
263
+ @export_i18n_js_dir_path ||= DEFAULT_EXPORT_DIR_PATH
264
+ @export_i18n_js_dir_path
265
+ end
266
+
267
+ # Setting this to nil would disable i18n.js exporting
268
+ def self.export_i18n_js_dir_path=(new_path)
269
+ new_path = :none unless new_path.is_a? String
270
+ @export_i18n_js_dir_path = new_path
271
+ end
272
+ end
273
+ end
274
+ end
data/lib/i18n-js.rb CHANGED
@@ -1,177 +1 @@
1
- require "FileUtils" unless defined?(FileUtils)
2
-
3
- module SimplesIdeias
4
- module I18n
5
- extend self
6
-
7
- require "i18n-js/railtie" if Rails.version >= "3.0"
8
- require "i18n-js/engine" if Rails.version >= "3.1"
9
- require "i18n-js/middleware"
10
-
11
- # deep_merge by Stefan Rusterholz, see http://www.ruby-forum.com/topic/142809
12
- MERGER = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &MERGER) : v2 }
13
-
14
- # Under rails 3.1.1 and higher, perform a check to ensure that the
15
- # full environment will be available during asset compilation.
16
- # This is required to ensure I18n is loaded.
17
- def assert_usable_configuration!
18
- @usable_configuration ||= Rails.version >= "3.1.1" &&
19
- Rails.configuration.assets.initialize_on_precompile ||
20
- raise("Cannot precompile i18n-js translations unless environment is initialized. Please set config.assets.initialize_on_precompile to true.")
21
- end
22
-
23
- def has_asset_pipeline?
24
- Rails.configuration.respond_to?(:assets) && Rails.configuration.assets.enabled
25
- end
26
-
27
- def config_file
28
- Rails.root.join("config/i18n-js.yml")
29
- end
30
-
31
- def export_dir
32
- if has_asset_pipeline?
33
- "app/assets/javascripts/i18n"
34
- else
35
- "public/javascripts"
36
- end
37
- end
38
-
39
- def javascript_file
40
- Rails.root.join(export_dir, "i18n.js")
41
- end
42
-
43
- # Export translations to JavaScript, considering settings
44
- # from configuration file
45
- def export!
46
- translation_segments.each do |filename, translations|
47
- save(translations, filename)
48
- end
49
- end
50
-
51
- def segments_per_locale(pattern,scope)
52
- ::I18n.available_locales.each_with_object({}) do |locale,segments|
53
- result = scoped_translations("#{locale}.#{scope}")
54
- unless result.empty?
55
- segment_name = ::I18n.interpolate(pattern,{:locale => locale})
56
- segments[segment_name] = result
57
- end
58
- end
59
- end
60
-
61
- def segment_for_scope(scope)
62
- if scope == "*"
63
- translations
64
- else
65
- scoped_translations(scope)
66
- end
67
- end
68
-
69
- def configured_segments
70
- config[:translations].each_with_object({}) do |options,segments|
71
- options.reverse_merge!(:only => "*")
72
- if options[:file] =~ ::I18n::INTERPOLATION_PATTERN
73
- segments.merge!(segments_per_locale(options[:file],options[:only]))
74
- else
75
- result = segment_for_scope(options[:only])
76
- segments[options[:file]] = result unless result.empty?
77
- end
78
- end
79
- end
80
-
81
- def translation_segments
82
- if config? && config[:translations]
83
- configured_segments
84
- else
85
- {"#{export_dir}/translations.js" => translations}
86
- end
87
- end
88
-
89
- # Load configuration file for partial exporting and
90
- # custom output directory
91
- def config
92
- if config?
93
- (YAML.load_file(config_file) || {}).with_indifferent_access
94
- else
95
- {}
96
- end
97
- end
98
-
99
- # Check if configuration file exist
100
- def config?
101
- File.file? config_file
102
- end
103
-
104
- # Copy configuration and JavaScript library files to
105
- # <tt>config/i18n-js.yml</tt> and <tt>public/javascripts/i18n.js</tt>.
106
- def setup!
107
- FileUtils.cp(File.dirname(__FILE__) + "/../vendor/assets/javascripts/i18n.js", javascript_file) unless Rails.version >= "3.1"
108
- FileUtils.cp(File.dirname(__FILE__) + "/../config/i18n-js.yml", config_file) unless config?
109
- end
110
-
111
- # Retrieve an updated JavaScript library from Github.
112
- def update!
113
- require "open-uri"
114
- contents = open("https://raw.github.com/fnando/i18n-js/master/vendor/assets/javascripts/i18n.js").read
115
- File.open(javascript_file, "w+") {|f| f << contents}
116
- end
117
-
118
- # Convert translations to JSON string and save file.
119
- def save(translations, file)
120
- file = Rails.root.join(file)
121
- FileUtils.mkdir_p File.dirname(file)
122
-
123
- File.open(file, "w+") do |f|
124
- f << %(var I18n = I18n || {};\n)
125
- f << %(I18n.translations = );
126
- f << translations.to_json
127
- f << %(;)
128
- end
129
- end
130
-
131
- def scoped_translations(scopes) # :nodoc:
132
- result = {}
133
-
134
- [scopes].flatten.each do |scope|
135
- deep_merge! result, filter(translations, scope)
136
- end
137
-
138
- result
139
- end
140
-
141
- # Filter translations according to the specified scope.
142
- def filter(translations, scopes)
143
- scopes = scopes.split(".") if scopes.is_a?(String)
144
- scopes = scopes.clone
145
- scope = scopes.shift
146
-
147
- if scope == "*"
148
- results = {}
149
- translations.each do |scope, translations|
150
- tmp = scopes.empty? ? translations : filter(translations, scopes)
151
- results[scope.to_sym] = tmp unless tmp.nil?
152
- end
153
- return results
154
- elsif translations.has_key?(scope.to_sym)
155
- return {scope.to_sym => scopes.empty? ? translations[scope.to_sym] : filter(translations[scope.to_sym], scopes)}
156
- end
157
- nil
158
- end
159
-
160
- # Initialize and return translations
161
- def translations
162
- ::I18n.backend.instance_eval do
163
- init_translations unless initialized?
164
- translations
165
- end
166
- end
167
-
168
- def deep_merge(target, hash) # :nodoc:
169
- target.merge(hash, &MERGER)
170
- end
171
-
172
- def deep_merge!(target, hash) # :nodoc:
173
- target.merge!(hash, &MERGER)
174
- end
175
- end
176
- end
177
-
1
+ require "i18n/js"
@@ -0,0 +1,19 @@
1
+ module I18n
2
+ module Js
3
+ class ConfigGenerator < Rails::Generators::Base
4
+ # Copied files come from templates folder
5
+ source_root File.expand_path('../templates', __FILE__)
6
+
7
+ # Generator desc
8
+ desc <<-DESC
9
+ Creates a default i18n-js.yml configuration file in your app's config
10
+ folder. This file allows you to customize i18n:js:export rake task
11
+ outputted files.
12
+ DESC
13
+
14
+ def copy_initializer_file
15
+ copy_file "i18n-js.yml", "config/i18n-js.yml"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,4 +1,5 @@
1
1
  # Split context in several files.
2
+ #
2
3
  # By default only one file with all translations is exported and
3
4
  # no configuration is required. Your settings for asset pipeline
4
5
  # are automatically recognized.
@@ -7,16 +8,20 @@
7
8
  # locale contexts that will be exported, just use this file to do
8
9
  # so.
9
10
  #
11
+ # For more informations about the export options with this file, please
12
+ # refer to the README
13
+ #
14
+ #
10
15
  # If you're going to use the Rails 3.1 asset pipeline, change
11
16
  # the following configuration to something like this:
12
17
  #
13
- # translations:
14
- # - file: "app/assets/javascripts/i18n/translations.js"
18
+ # translations:
19
+ # - file: "app/assets/javascripts/i18n/translations.js"
15
20
  #
16
21
  # If you're running an old version, you can use something
17
22
  # like this:
18
23
  #
19
- # translations:
20
- # - file: "public/javascripts/translations.js"
21
- # only: "*"
22
- #
24
+ # translations:
25
+ # - file: "app/assets/javascripts/i18n/translations.js"
26
+ # only: "*"
27
+ #
@@ -0,0 +1,8 @@
1
+ namespace :i18n do
2
+ namespace :js do
3
+ desc "Export translations to JS file(s)"
4
+ task :export => :environment do
5
+ I18n::JS.export
6
+ end
7
+ end
8
+ end