mlanett-i18n-js 2.1.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.
data/lib/i18n-js.rb ADDED
@@ -0,0 +1,192 @@
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 = $.extend\(true, I18n.translations, )
126
+ f << deep_sort(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
+
176
+ def deep_sort(hash)
177
+ case hash
178
+ when Hash
179
+ result = Hash[hash.sort { |a, b| a[0].to_s <=> b[0].to_s } ]
180
+ result.each do |key, value|
181
+ result[key] = deep_sort(value)
182
+ end
183
+ result
184
+ when Array
185
+ hash.map { |value| deep_sort(value) }
186
+ else
187
+ hash
188
+ end
189
+ end
190
+ end
191
+ end
192
+
@@ -0,0 +1,63 @@
1
+ module SimplesIdeias
2
+ module I18n
3
+ class Engine < ::Rails::Engine
4
+ I18N_TRANSLATIONS_ASSET = "i18n/translations"
5
+
6
+ initializer "i18n-js.asset_dependencies", :after => "sprockets.environment",
7
+ :before => "i18n-js.initialize" do
8
+ next unless SimplesIdeias::I18n.has_asset_pipeline?
9
+
10
+ config = I18n.config_file
11
+ cache_file = I18n::Engine.load_path_hash_cache
12
+
13
+ Rails.application.assets.register_preprocessor "application/javascript", :"i18n-js_dependencies" do |context, data|
14
+ if context.logical_path == I18N_TRANSLATIONS_ASSET
15
+ context.depend_on(config) if I18n.config?
16
+ # also set up dependencies on every locale file
17
+ ::I18n.load_path.each {|path| context.depend_on(path)}
18
+
19
+ # Set up a dependency on the contents of the load path
20
+ # itself. In some situations it is possible to get here
21
+ # before the path hash cache file has been written; in
22
+ # this situation, write it now.
23
+ I18n::Engine.write_hash! unless File.exists?(cache_file)
24
+ context.depend_on(cache_file)
25
+ end
26
+
27
+ data
28
+ end
29
+ end
30
+
31
+ # rewrite path cache hash at startup and before each request in development
32
+ config.to_prepare do
33
+ next unless SimplesIdeias::I18n.has_asset_pipeline?
34
+ SimplesIdeias::I18n::Engine.write_hash_if_changed unless Rails.env.production?
35
+ end
36
+
37
+ def self.load_path_hash_cache
38
+ @load_path_hash_cache ||= Rails.root.join("tmp/i18n-js.cache")
39
+ end
40
+
41
+ def self.write_hash_if_changed
42
+ load_path_hash = ::I18n.load_path.hash
43
+
44
+ if load_path_hash != cached_load_path_hash
45
+ self.cached_load_path_hash = load_path_hash
46
+ write_hash!
47
+ end
48
+ end
49
+
50
+ def self.write_hash!
51
+ FileUtils.mkdir_p Rails.root.join("tmp")
52
+
53
+ File.open(load_path_hash_cache, "w+") do |f|
54
+ f.write(cached_load_path_hash)
55
+ end
56
+ end
57
+
58
+ class << self
59
+ attr_accessor :cached_load_path_hash
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,59 @@
1
+ module SimplesIdeias
2
+ module I18n
3
+ class Middleware
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ @cache = nil
10
+ verify_locale_files!
11
+ @app.call(env)
12
+ end
13
+
14
+ private
15
+ def cache_path
16
+ @cache_path ||= Rails.root.join("tmp/cache/i18n-js.yml")
17
+ end
18
+
19
+ def cache
20
+ @cache ||= begin
21
+ if cache_path.exist?
22
+ YAML.load_file(cache_path) || {}
23
+ else
24
+ {}
25
+ end
26
+ end
27
+ end
28
+
29
+ # Check if translations should be regenerated.
30
+ # ONLY REGENERATE when these conditions are met:
31
+ #
32
+ # # Cache file doesn't exist
33
+ # # Translations and cache size are different (files were removed/added)
34
+ # # Translation file has been updated
35
+ #
36
+ def verify_locale_files!
37
+ valid_cache = []
38
+ new_cache = {}
39
+
40
+ valid_cache.push cache_path.exist?
41
+ valid_cache.push ::I18n.load_path.uniq.size == cache.size
42
+
43
+ ::I18n.load_path.each do |path|
44
+ changed_at = File.mtime(path).to_i
45
+ valid_cache.push changed_at == cache[path]
46
+ new_cache[path] = changed_at
47
+ end
48
+
49
+ unless valid_cache.all?
50
+ File.open(cache_path, "w+") do |file|
51
+ file << new_cache.to_yaml
52
+ end
53
+
54
+ SimplesIdeias::I18n.export!
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,13 @@
1
+ module SimplesIdeias
2
+ module I18n
3
+ class Railtie < Rails::Railtie
4
+ rake_tasks do
5
+ require "i18n-js/rake"
6
+ end
7
+
8
+ initializer "i18n-js.initialize" do |app|
9
+ app.config.middleware.use(Middleware) if (Rails.env.development? || Rails.env.test?) && !SimplesIdeias::I18n.has_asset_pipeline?
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,16 @@
1
+ namespace "i18n:js" do
2
+ desc "Copy i18n.js and configuration file"
3
+ task :setup => :environment do
4
+ SimplesIdeias::I18n.setup!
5
+ end
6
+
7
+ desc "Export the messages files"
8
+ task :export => :environment do
9
+ SimplesIdeias::I18n.export!
10
+ end
11
+
12
+ desc "Update the JavaScript library"
13
+ task :update => :environment do
14
+ SimplesIdeias::I18n.update!
15
+ end
16
+ end
@@ -0,0 +1,10 @@
1
+ module SimplesIdeias
2
+ module I18n
3
+ module Version
4
+ MAJOR = 2
5
+ MINOR = 1
6
+ PATCH = 2
7
+ STRING = "#{MAJOR}.#{MINOR}.#{PATCH}"
8
+ end
9
+ end
10
+ end
data/spec/i18n_spec.js ADDED
@@ -0,0 +1,820 @@
1
+ load("vendor/assets/javascripts/i18n.js");
2
+
3
+ describe("I18n.js", function(){
4
+ before(function() {
5
+ I18n.defaultLocale = "en";
6
+ I18n.defaultSeparator = ".";
7
+ I18n.locale = null;
8
+
9
+ I18n.translations = {
10
+ en: {
11
+ hello: "Hello World!",
12
+ greetings: {
13
+ stranger: "Hello stranger!",
14
+ name: "Hello {{name}}!"
15
+ },
16
+ profile: {
17
+ details: "{{name}} is {{age}}-years old"
18
+ },
19
+ inbox: {
20
+ one: "You have {{count}} message",
21
+ other: "You have {{count}} messages",
22
+ zero: "You have no messages"
23
+ },
24
+ unread: {
25
+ one: "You have 1 new message ({{unread}} unread)",
26
+ other: "You have {{count}} new messages ({{unread}} unread)",
27
+ zero: "You have no new messages ({{unread}} unread)"
28
+ },
29
+ number: {
30
+ human: {
31
+ storage_units: {
32
+ units: {
33
+ "byte": {
34
+ one: "Byte",
35
+ other: "Bytes"
36
+ },
37
+
38
+ "kb": "KB",
39
+ "mb": "MB",
40
+ "gb": "GB",
41
+ "tb": "TB"
42
+ }
43
+ }
44
+ }
45
+ }
46
+ },
47
+
48
+ "pt-BR": {
49
+ hello: "Olá Mundo!",
50
+ date: {
51
+ formats: {
52
+ "default": "%d/%m/%Y",
53
+ "short": "%d de %B",
54
+ "long": "%d de %B de %Y"
55
+ },
56
+ day_names: ["Domingo", "Segunda", "Terça", "Quarta", "Quinta", "Sexta", "Sábado"],
57
+ abbr_day_names: ["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb"],
58
+ month_names: [null, "Janeiro", "Fevereiro", "Março", "Abril", "Maio", "Junho", "Julho", "Agosto", "Setembro", "Outubro", "Novembro", "Dezembro"],
59
+ abbr_month_names: [null, "Jan", "Fev", "Mar", "Abr", "Mai", "Jun", "Jul", "Ago", "Set", "Out", "Nov", "Dez"]
60
+ },
61
+ number: {
62
+ percentage: {
63
+ format: {
64
+ delimiter: "",
65
+ separator: ",",
66
+ precision: 2
67
+ }
68
+ }
69
+ },
70
+ time: {
71
+ formats: {
72
+ "default": "%A, %d de %B de %Y, %H:%M h",
73
+ "short": "%d/%m, %H:%M h",
74
+ "long": "%A, %d de %B de %Y, %H:%M h"
75
+ },
76
+ am: "AM",
77
+ pm: "PM"
78
+ }
79
+ },
80
+
81
+ "en-US": {
82
+ date: {
83
+ formats: {
84
+ "default": "%d/%m/%Y",
85
+ "short": "%d de %B",
86
+ "long": "%d de %B de %Y"
87
+ },
88
+ day_names: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
89
+ abbr_day_names: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
90
+ month_names: [null, "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
91
+ abbr_month_names: [null, "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sept", "Oct", "Nov", "Dec"],
92
+ meridian: ["am", "pm"]
93
+ }
94
+ },
95
+
96
+ "de": {
97
+ hello: "Hallo Welt!"
98
+ },
99
+
100
+ "nb": {
101
+ hello: "Hei Verden!"
102
+ }
103
+ };
104
+ });
105
+
106
+ specify("with default options", function(){
107
+ expect(I18n.defaultLocale).toBeEqualTo("en");
108
+ expect(I18n.locale).toBeEqualTo(null);
109
+ expect(I18n.currentLocale()).toBeEqualTo("en");
110
+ });
111
+
112
+ specify("with custom locale", function(){
113
+ I18n.locale = "pt-BR";
114
+ expect(I18n.currentLocale()).toBeEqualTo("pt-BR");
115
+ });
116
+
117
+ specify("aliases", function(){
118
+ expect(I18n.t).toBe(I18n.translate);
119
+ expect(I18n.l).toBe(I18n.localize);
120
+ expect(I18n.p).toBe(I18n.pluralize);
121
+ });
122
+
123
+ specify("translation with single scope", function(){
124
+ expect(I18n.t("hello")).toBeEqualTo("Hello World!");
125
+ });
126
+
127
+ specify("translation as object", function(){
128
+ expect(I18n.t("greetings")).toBeInstanceOf(Object);
129
+ });
130
+
131
+ specify("translation with invalid scope shall not block", function(){
132
+ actual = I18n.t("invalid.scope.shall.not.block");
133
+ expected = '[missing "en.invalid.scope.shall.not.block" translation]';
134
+ expect(actual).toBeEqualTo(expected);
135
+ });
136
+
137
+ specify("translation for single scope on a custom locale", function(){
138
+ I18n.locale = "pt-BR";
139
+ expect(I18n.t("hello")).toBeEqualTo("Olá Mundo!");
140
+ });
141
+
142
+ specify("translation for multiple scopes", function(){
143
+ expect(I18n.t("greetings.stranger")).toBeEqualTo("Hello stranger!");
144
+ });
145
+
146
+ specify("translation with default locale option", function(){
147
+ expect(I18n.t("hello", {locale: "en"})).toBeEqualTo("Hello World!");
148
+ expect(I18n.t("hello", {locale: "pt-BR"})).toBeEqualTo("Olá Mundo!");
149
+ });
150
+
151
+ specify("translation should fall if locale is missing", function(){
152
+ I18n.locale = "pt-BR";
153
+ expect(I18n.t("greetings.stranger")).toBeEqualTo("[missing \"pt-BR.greetings.stranger\" translation]");
154
+ });
155
+
156
+ specify("translation should handle fallback if I18n.fallbacks == true", function(){
157
+ I18n.locale = "pt-BR";
158
+ I18n.fallbacks = true;
159
+ expect(I18n.t("greetings.stranger")).toBeEqualTo("Hello stranger!");
160
+ });
161
+
162
+ specify("translation should handle fallback from unknown locale", function(){
163
+ I18n.locale = "fr";
164
+ I18n.fallbacks = true;
165
+ expect(I18n.t("greetings.stranger")).toBeEqualTo("Hello stranger!");
166
+ });
167
+
168
+ specify("translation should handle fallback to less specific locale", function(){
169
+ I18n.locale = "de-DE";
170
+ I18n.fallbacks = true;
171
+ expect(I18n.t("hello")).toBeEqualTo("Hallo Welt!");
172
+ });
173
+
174
+ specify("translation should handle fallback via custom rules", function(){
175
+ I18n.locale = "no";
176
+ I18n.fallbacks = true;
177
+ I18n.fallbackRules.no = [ "nb" ];
178
+ expect(I18n.t("hello")).toBeEqualTo("Hei Verden!");
179
+ });
180
+
181
+ specify("single interpolation", function(){
182
+ actual = I18n.t("greetings.name", {name: "John Doe"});
183
+ expect(actual).toBeEqualTo("Hello John Doe!");
184
+ });
185
+
186
+ specify("multiple interpolation", function(){
187
+ actual = I18n.t("profile.details", {name: "John Doe", age: 27});
188
+ expect(actual).toBeEqualTo("John Doe is 27-years old");
189
+ });
190
+
191
+ specify("translation with count option", function(){
192
+ expect(I18n.t("inbox", {count: 0})).toBeEqualTo("You have no messages");
193
+ expect(I18n.t("inbox", {count: 1})).toBeEqualTo("You have 1 message");
194
+ expect(I18n.t("inbox", {count: 5})).toBeEqualTo("You have 5 messages");
195
+ });
196
+
197
+ specify("translation with count option and multiple placeholders", function(){
198
+ actual = I18n.t("unread", {unread: 5, count: 1});
199
+ expect(actual).toBeEqualTo("You have 1 new message (5 unread)");
200
+
201
+ actual = I18n.t("unread", {unread: 2, count: 10});
202
+ expect(actual).toBeEqualTo("You have 10 new messages (2 unread)");
203
+
204
+ actual = I18n.t("unread", {unread: 5, count: 0});
205
+ expect(actual).toBeEqualTo("You have no new messages (5 unread)");
206
+ });
207
+
208
+ specify("missing translation with count option", function(){
209
+ actual = I18n.t("invalid", {count: 1});
210
+ expect(actual).toBeEqualTo('[missing "en.invalid" translation]');
211
+
212
+ I18n.translations.en.inbox.one = null;
213
+ actual = I18n.t("inbox", {count: 1});
214
+ expect(actual).toBeEqualTo('[missing "en.inbox.one" translation]');
215
+ });
216
+
217
+ specify("pluralization", function(){
218
+ expect(I18n.p(0, "inbox")).toBeEqualTo("You have no messages");
219
+ expect(I18n.p(1, "inbox")).toBeEqualTo("You have 1 message");
220
+ expect(I18n.p(5, "inbox")).toBeEqualTo("You have 5 messages");
221
+ });
222
+
223
+ specify("pluralize should return 'other' scope", function(){
224
+ I18n.translations["en"]["inbox"]["zero"] = null;
225
+ expect(I18n.p(0, "inbox")).toBeEqualTo("You have 0 messages");
226
+ });
227
+
228
+ specify("pluralize should return 'zero' scope", function(){
229
+ I18n.translations["en"]["inbox"]["zero"] = "No messages (zero)";
230
+ I18n.translations["en"]["inbox"]["none"] = "No messages (none)";
231
+
232
+ expect(I18n.p(0, "inbox")).toBeEqualTo("No messages (zero)");
233
+ });
234
+
235
+ specify("pluralize should return 'none' scope", function(){
236
+ I18n.translations["en"]["inbox"]["zero"] = null;
237
+ I18n.translations["en"]["inbox"]["none"] = "No messages (none)";
238
+
239
+ expect(I18n.p(0, "inbox")).toBeEqualTo("No messages (none)");
240
+ });
241
+
242
+ specify("pluralize with negative values", function(){
243
+ expect(I18n.p(-1, "inbox")).toBeEqualTo("You have -1 message");
244
+ expect(I18n.p(-5, "inbox")).toBeEqualTo("You have -5 messages");
245
+ });
246
+
247
+ specify("pluralize with missing scope", function(){
248
+ expect(I18n.p(-1, "missing")).toBeEqualTo('[missing "en.missing" translation]');
249
+ });
250
+
251
+ specify("pluralize with multiple placeholders", function(){
252
+ actual = I18n.p(1, "unread", {unread: 5});
253
+ expect(actual).toBeEqualTo("You have 1 new message (5 unread)");
254
+
255
+ actual = I18n.p(10, "unread", {unread: 2});
256
+ expect(actual).toBeEqualTo("You have 10 new messages (2 unread)");
257
+
258
+ actual = I18n.p(0, "unread", {unread: 5});
259
+ expect(actual).toBeEqualTo("You have no new messages (5 unread)");
260
+ });
261
+
262
+ specify("pluralize should allow empty strings", function(){
263
+ I18n.translations["en"]["inbox"]["zero"] = "";
264
+
265
+ expect(I18n.p(0, "inbox")).toBeEqualTo("");
266
+ });
267
+
268
+ specify("numbers with default settings", function(){
269
+ expect(I18n.toNumber(1)).toBeEqualTo("1.000");
270
+ expect(I18n.toNumber(12)).toBeEqualTo("12.000");
271
+ expect(I18n.toNumber(123)).toBeEqualTo("123.000");
272
+ expect(I18n.toNumber(1234)).toBeEqualTo("1,234.000");
273
+ expect(I18n.toNumber(12345)).toBeEqualTo("12,345.000");
274
+ expect(I18n.toNumber(123456)).toBeEqualTo("123,456.000");
275
+ expect(I18n.toNumber(1234567)).toBeEqualTo("1,234,567.000");
276
+ expect(I18n.toNumber(12345678)).toBeEqualTo("12,345,678.000");
277
+ expect(I18n.toNumber(123456789)).toBeEqualTo("123,456,789.000");
278
+ });
279
+
280
+ specify("negative numbers with default settings", function(){
281
+ expect(I18n.toNumber(-1)).toBeEqualTo("-1.000");
282
+ expect(I18n.toNumber(-12)).toBeEqualTo("-12.000");
283
+ expect(I18n.toNumber(-123)).toBeEqualTo("-123.000");
284
+ expect(I18n.toNumber(-1234)).toBeEqualTo("-1,234.000");
285
+ expect(I18n.toNumber(-12345)).toBeEqualTo("-12,345.000");
286
+ expect(I18n.toNumber(-123456)).toBeEqualTo("-123,456.000");
287
+ expect(I18n.toNumber(-1234567)).toBeEqualTo("-1,234,567.000");
288
+ expect(I18n.toNumber(-12345678)).toBeEqualTo("-12,345,678.000");
289
+ expect(I18n.toNumber(-123456789)).toBeEqualTo("-123,456,789.000");
290
+ });
291
+
292
+ specify("numbers with partial translation and default options", function(){
293
+ I18n.translations.en.number = {
294
+ format: {
295
+ precision: 2
296
+ }
297
+ };
298
+
299
+ expect(I18n.toNumber(1234)).toBeEqualTo("1,234.00");
300
+ });
301
+
302
+ specify("numbers with full translation and default options", function(){
303
+ I18n.translations.en.number = {
304
+ format: {
305
+ delimiter: ".",
306
+ separator: ",",
307
+ precision: 2
308
+ }
309
+ };
310
+
311
+ expect(I18n.toNumber(1234)).toBeEqualTo("1.234,00");
312
+ });
313
+
314
+ specify("numbers with some custom options that should be merged with default options", function(){
315
+ expect(I18n.toNumber(1234, {precision: 0})).toBeEqualTo("1,234");
316
+ expect(I18n.toNumber(1234, {separator: '-'})).toBeEqualTo("1,234-000");
317
+ expect(I18n.toNumber(1234, {delimiter: '-'})).toBeEqualTo("1-234.000");
318
+ });
319
+
320
+ specify("numbers considering options", function(){
321
+ options = {
322
+ precision: 2,
323
+ separator: ",",
324
+ delimiter: "."
325
+ };
326
+
327
+ expect(I18n.toNumber(1, options)).toBeEqualTo("1,00");
328
+ expect(I18n.toNumber(12, options)).toBeEqualTo("12,00");
329
+ expect(I18n.toNumber(123, options)).toBeEqualTo("123,00");
330
+ expect(I18n.toNumber(1234, options)).toBeEqualTo("1.234,00");
331
+ expect(I18n.toNumber(123456, options)).toBeEqualTo("123.456,00");
332
+ expect(I18n.toNumber(1234567, options)).toBeEqualTo("1.234.567,00");
333
+ expect(I18n.toNumber(12345678, options)).toBeEqualTo("12.345.678,00");
334
+ });
335
+
336
+ specify("numbers with different precisions", function(){
337
+ options = {separator: ".", delimiter: ","};
338
+
339
+ options["precision"] = 2;
340
+ expect(I18n.toNumber(1.98, options)).toBeEqualTo("1.98");
341
+
342
+ options["precision"] = 3;
343
+ expect(I18n.toNumber(1.98, options)).toBeEqualTo("1.980");
344
+
345
+ options["precision"] = 2;
346
+ expect(I18n.toNumber(1.987, options)).toBeEqualTo("1.99");
347
+
348
+ options["precision"] = 1;
349
+ expect(I18n.toNumber(1.98, options)).toBeEqualTo("2.0");
350
+
351
+ options["precision"] = 0;
352
+ expect(I18n.toNumber(1.98, options)).toBeEqualTo("2");
353
+ });
354
+
355
+ specify("currency with default settings", function(){
356
+ expect(I18n.toCurrency(100.99)).toBeEqualTo("$100.99");
357
+ expect(I18n.toCurrency(1000.99)).toBeEqualTo("$1,000.99");
358
+ });
359
+
360
+ specify("currency with custom settings", function(){
361
+ I18n.translations.en.number = {
362
+ currency: {
363
+ format: {
364
+ format: "%n %u",
365
+ unit: "USD",
366
+ delimiter: ".",
367
+ separator: ",",
368
+ precision: 2
369
+ }
370
+ }
371
+ };
372
+
373
+ expect(I18n.toCurrency(12)).toBeEqualTo("12,00 USD");
374
+ expect(I18n.toCurrency(123)).toBeEqualTo("123,00 USD");
375
+ expect(I18n.toCurrency(1234.56)).toBeEqualTo("1.234,56 USD");
376
+ });
377
+
378
+ specify("currency with custom settings and partial overriding", function(){
379
+ I18n.translations.en.number = {
380
+ currency: {
381
+ format: {
382
+ format: "%n %u",
383
+ unit: "USD",
384
+ delimiter: ".",
385
+ separator: ",",
386
+ precision: 2
387
+ }
388
+ }
389
+ };
390
+
391
+ expect(I18n.toCurrency(12, {precision: 0})).toBeEqualTo("12 USD");
392
+ expect(I18n.toCurrency(123, {unit: "bucks"})).toBeEqualTo("123,00 bucks");
393
+ });
394
+
395
+ specify("currency with some custom options that should be merged with default options", function(){
396
+ expect(I18n.toCurrency(1234, {precision: 0})).toBeEqualTo("$1,234");
397
+ expect(I18n.toCurrency(1234, {unit: "º"})).toBeEqualTo("º1,234.00");
398
+ expect(I18n.toCurrency(1234, {separator: "-"})).toBeEqualTo("$1,234-00");
399
+ expect(I18n.toCurrency(1234, {delimiter: "-"})).toBeEqualTo("$1-234.00");
400
+ expect(I18n.toCurrency(1234, {format: "%u %n"})).toBeEqualTo("$ 1,234.00");
401
+ });
402
+
403
+ specify("localize numbers", function(){
404
+ expect(I18n.l("number", 1234567)).toBeEqualTo("1,234,567.000");
405
+ });
406
+
407
+ specify("localize currency", function(){
408
+ expect(I18n.l("currency", 1234567)).toBeEqualTo("$1,234,567.00");
409
+ });
410
+
411
+ specify("parse date", function(){
412
+ expected = new Date(2009, 0, 24, 0, 0, 0);
413
+ actual = I18n.parseDate("2009-01-24");
414
+ expect(actual.toString()).toBeEqualTo(expected.toString());
415
+
416
+ expected = new Date(2009, 0, 24, 0, 15, 0);
417
+ actual = I18n.parseDate("2009-01-24 00:15:00");
418
+ expect(actual.toString()).toBeEqualTo(expected.toString());
419
+
420
+ expected = new Date(2009, 0, 24, 0, 0, 15);
421
+ actual = I18n.parseDate("2009-01-24 00:00:15");
422
+ expect(actual.toString()).toBeEqualTo(expected.toString());
423
+
424
+ expected = new Date(2009, 0, 24, 15, 33, 44);
425
+ actual = I18n.parseDate("2009-01-24 15:33:44");
426
+ expect(actual.toString()).toBeEqualTo(expected.toString());
427
+
428
+ expected = new Date(2009, 0, 24, 0, 0, 0);
429
+ actual = I18n.parseDate(expected.getTime());
430
+ expect(actual.toString()).toBeEqualTo(expected.toString());
431
+
432
+ expected = new Date(2009, 0, 24, 0, 0, 0);
433
+ actual = I18n.parseDate("01/24/2009");
434
+ expect(actual.toString()).toBeEqualTo(expected.toString());
435
+
436
+ expected = new Date(2009, 0, 24, 14, 33, 55);
437
+ actual = I18n.parseDate(expected).toString();
438
+ expect(actual).toBeEqualTo(expected.toString());
439
+
440
+ expected = new Date(2009, 0, 24, 15, 33, 44);
441
+ actual = I18n.parseDate("2009-01-24T15:33:44");
442
+ expect(actual.toString()).toBeEqualTo(expected.toString());
443
+
444
+ expected = new Date(Date.UTC(2011, 6, 20, 12, 51, 55));
445
+ actual = I18n.parseDate("2011-07-20T12:51:55+0000");
446
+ expect(actual.toString()).toBeEqualTo(expected.toString());
447
+
448
+ expected = new Date(Date.UTC(2011, 6, 20, 13, 03, 39));
449
+ actual = I18n.parseDate("Wed Jul 20 13:03:39 +0000 2011");
450
+ expect(actual.toString()).toBeEqualTo(expected.toString());
451
+
452
+ expected = new Date(Date.UTC(2009, 0, 24, 15, 33, 44));
453
+ actual = I18n.parseDate("2009-01-24T15:33:44Z");
454
+ expect(actual.toString()).toBeEqualTo(expected.toString());
455
+ });
456
+
457
+ specify("date formatting", function(){
458
+ I18n.locale = "pt-BR";
459
+
460
+ // 2009-04-26 19:35:44 (Sunday)
461
+ var date = new Date(2009, 3, 26, 19, 35, 44);
462
+
463
+ // short week day
464
+ expect(I18n.strftime(date, "%a")).toBeEqualTo("Dom");
465
+
466
+ // full week day
467
+ expect(I18n.strftime(date, "%A")).toBeEqualTo("Domingo");
468
+
469
+ // short month
470
+ expect(I18n.strftime(date, "%b")).toBeEqualTo("Abr");
471
+
472
+ // full month
473
+ expect(I18n.strftime(date, "%B")).toBeEqualTo("Abril");
474
+
475
+ // day
476
+ expect(I18n.strftime(date, "%d")).toBeEqualTo("26");
477
+
478
+ // 24-hour
479
+ expect(I18n.strftime(date, "%H")).toBeEqualTo("19");
480
+
481
+ // 12-hour
482
+ expect(I18n.strftime(date, "%I")).toBeEqualTo("07");
483
+
484
+ // month
485
+ expect(I18n.strftime(date, "%m")).toBeEqualTo("04");
486
+
487
+ // minutes
488
+ expect(I18n.strftime(date, "%M")).toBeEqualTo("35");
489
+
490
+ // meridian
491
+ expect(I18n.strftime(date, "%p")).toBeEqualTo("PM");
492
+
493
+ // seconds
494
+ expect(I18n.strftime(date, "%S")).toBeEqualTo("44");
495
+
496
+ // week day
497
+ expect(I18n.strftime(date, "%w")).toBeEqualTo("0");
498
+
499
+ // short year
500
+ expect(I18n.strftime(date, "%y")).toBeEqualTo("09");
501
+
502
+ // full year
503
+ expect(I18n.strftime(date, "%Y")).toBeEqualTo("2009");
504
+ });
505
+
506
+ specify("date formatting without padding", function(){
507
+ I18n.locale = "pt-BR";
508
+
509
+ // 2009-04-26 19:35:44 (Sunday)
510
+ var date = new Date(2009, 3, 9, 7, 8, 9);
511
+
512
+ // 24-hour without padding
513
+ expect(I18n.strftime(date, "%-H")).toBeEqualTo("7");
514
+
515
+ // 12-hour without padding
516
+ expect(I18n.strftime(date, "%-I")).toBeEqualTo("7");
517
+
518
+ // minutes without padding
519
+ expect(I18n.strftime(date, "%-M")).toBeEqualTo("8");
520
+
521
+ // seconds without padding
522
+ expect(I18n.strftime(date, "%-S")).toBeEqualTo("9");
523
+
524
+ // short year without padding
525
+ expect(I18n.strftime(date, "%-y")).toBeEqualTo("9");
526
+
527
+ // month without padding
528
+ expect(I18n.strftime(date, "%-m")).toBeEqualTo("4");
529
+
530
+ // day without padding
531
+ expect(I18n.strftime(date, "%-d")).toBeEqualTo("9");
532
+ expect(I18n.strftime(date, "%e")).toBeEqualTo("9");
533
+ });
534
+
535
+ specify("date formatting with padding", function(){
536
+ I18n.locale = "pt-BR";
537
+
538
+ // 2009-04-26 19:35:44 (Sunday)
539
+ var date = new Date(2009, 3, 9, 7, 8, 9);
540
+
541
+ // 24-hour
542
+ expect(I18n.strftime(date, "%H")).toBeEqualTo("07");
543
+
544
+ // 12-hour
545
+ expect(I18n.strftime(date, "%I")).toBeEqualTo("07");
546
+
547
+ // minutes
548
+ expect(I18n.strftime(date, "%M")).toBeEqualTo("08");
549
+
550
+ // seconds
551
+ expect(I18n.strftime(date, "%S")).toBeEqualTo("09");
552
+
553
+ // short year
554
+ expect(I18n.strftime(date, "%y")).toBeEqualTo("09");
555
+
556
+ // month
557
+ expect(I18n.strftime(date, "%m")).toBeEqualTo("04");
558
+
559
+ // day
560
+ expect(I18n.strftime(date, "%d")).toBeEqualTo("09");
561
+ });
562
+
563
+ specify("date formatting with negative time zone", function(){
564
+ I18n.locale = "pt-BR";
565
+ var date = new Date(2009, 3, 26, 19, 35, 44);
566
+ stub(date, "getTimezoneOffset()", 345);
567
+
568
+ expect(I18n.strftime(date, "%z")).toMatch(/^(\+|-)[\d]{4}$/);
569
+ expect(I18n.strftime(date, "%z")).toBeEqualTo("-0545");
570
+ });
571
+
572
+ specify("date formatting with positive time zone", function(){
573
+ I18n.locale = "pt-BR";
574
+ var date = new Date(2009, 3, 26, 19, 35, 44);
575
+ stub(date, "getTimezoneOffset()", -345);
576
+
577
+ expect(I18n.strftime(date, "%z")).toMatch(/^(\+|-)[\d]{4}$/);
578
+ expect(I18n.strftime(date, "%z")).toBeEqualTo("+0545");
579
+ });
580
+
581
+ specify("date formatting with custom meridian", function(){
582
+ I18n.locale = "en-US";
583
+ var date = new Date(2009, 3, 26, 19, 35, 44);
584
+ expect(I18n.strftime(date, "%p")).toBeEqualTo("pm");
585
+ });
586
+
587
+ specify("date formatting meridian boundaries", function(){
588
+ I18n.locale = "en-US";
589
+ var date = new Date(2009, 3, 26, 0, 35, 44);
590
+ expect(I18n.strftime(date, "%p")).toBeEqualTo("am");
591
+
592
+ date = new Date(2009, 3, 26, 12, 35, 44);
593
+ expect(I18n.strftime(date, "%p")).toBeEqualTo("pm");
594
+ });
595
+
596
+ specify("date formatting hour12 values", function(){
597
+ I18n.locale = "pt-BR";
598
+ var date = new Date(2009, 3, 26, 19, 35, 44);
599
+ expect(I18n.strftime(date, "%I")).toBeEqualTo("07");
600
+
601
+ date = new Date(2009, 3, 26, 12, 35, 44);
602
+ expect(I18n.strftime(date, "%I")).toBeEqualTo("12");
603
+
604
+ date = new Date(2009, 3, 26, 0, 35, 44);
605
+ expect(I18n.strftime(date, "%I")).toBeEqualTo("12");
606
+ });
607
+
608
+ specify("localize date strings", function(){
609
+ I18n.locale = "pt-BR";
610
+
611
+ expect(I18n.l("date.formats.default", "2009-11-29")).toBeEqualTo("29/11/2009");
612
+ expect(I18n.l("date.formats.short", "2009-01-07")).toBeEqualTo("07 de Janeiro");
613
+ expect(I18n.l("date.formats.long", "2009-01-07")).toBeEqualTo("07 de Janeiro de 2009");
614
+ });
615
+
616
+ specify("localize time strings", function(){
617
+ I18n.locale = "pt-BR";
618
+
619
+ expect(I18n.l("time.formats.default", "2009-11-29 15:07:59")).toBeEqualTo("Domingo, 29 de Novembro de 2009, 15:07 h");
620
+ expect(I18n.l("time.formats.short", "2009-01-07 09:12:35")).toBeEqualTo("07/01, 09:12 h");
621
+ expect(I18n.l("time.formats.long", "2009-11-29 15:07:59")).toBeEqualTo("Domingo, 29 de Novembro de 2009, 15:07 h");
622
+ });
623
+
624
+ specify("localize percentage", function(){
625
+ I18n.locale = "pt-BR";
626
+ expect(I18n.l("percentage", 123.45)).toBeEqualTo("123,45%");
627
+ });
628
+
629
+ specify("default value for simple translation", function(){
630
+ actual = I18n.t("warning", {defaultValue: "Warning!"});
631
+ expect(actual).toBeEqualTo("Warning!");
632
+ });
633
+
634
+ specify("default value for unknown locale", function(){
635
+ I18n.locale = "fr";
636
+ actual = I18n.t("warning", {defaultValue: "Warning!"});
637
+ expect(actual).toBeEqualTo("Warning!");
638
+ });
639
+
640
+ specify("default value with interpolation", function(){
641
+ actual = I18n.t(
642
+ "alert",
643
+ {defaultValue: "Attention! {{message}}", message: "You're out of quota!"}
644
+ );
645
+
646
+ expect(actual).toBeEqualTo("Attention! You're out of quota!");
647
+ });
648
+
649
+ specify("default value should not be used when scope exist", function(){
650
+ actual = I18n.t("hello", {defaultValue: "What's up?"});
651
+ expect(actual).toBeEqualTo("Hello World!");
652
+ });
653
+
654
+ specify("default value for pluralize", function(){
655
+ options = {defaultValue: {
656
+ none: "No things here!",
657
+ one: "There is {{count}} thing here!",
658
+ other: "There are {{count}} things here!"
659
+ }};
660
+
661
+ expect(I18n.p(0, "things", options)).toBeEqualTo("No things here!");
662
+ expect(I18n.p(1, "things", options)).toBeEqualTo("There is 1 thing here!");
663
+ expect(I18n.p(5, "things", options)).toBeEqualTo("There are 5 things here!");
664
+ });
665
+
666
+ specify("default value for pluralize should not be used when scope exist", function(){
667
+ options = {defaultValue: {
668
+ none: "No things here!",
669
+ one: "There is {{count}} thing here!",
670
+ other: "There are {{count}} things here!"
671
+ }};
672
+
673
+ expect(I18n.pluralize(0, "inbox", options)).toBeEqualTo("You have no messages");
674
+ expect(I18n.pluralize(1, "inbox", options)).toBeEqualTo("You have 1 message");
675
+ expect(I18n.pluralize(5, "inbox", options)).toBeEqualTo("You have 5 messages");
676
+ });
677
+
678
+ specify("prepare options", function(){
679
+ options = I18n.prepareOptions(
680
+ {name: "Mary Doe"},
681
+ {name: "John Doe", role: "user"}
682
+ );
683
+
684
+ expect(options["name"]).toBeEqualTo("Mary Doe");
685
+ expect(options["role"]).toBeEqualTo("user");
686
+ });
687
+
688
+ specify("prepare options with multiple options", function(){
689
+ options = I18n.prepareOptions(
690
+ {name: "Mary Doe"},
691
+ {name: "John Doe", role: "user"},
692
+ {age: 33},
693
+ {email: "mary@doe.com", url: "http://marydoe.com"},
694
+ {role: "admin", email: "john@doe.com"}
695
+ );
696
+
697
+ expect(options["name"]).toBeEqualTo("Mary Doe");
698
+ expect(options["role"]).toBeEqualTo("user");
699
+ expect(options["age"]).toBeEqualTo(33);
700
+ expect(options["email"]).toBeEqualTo("mary@doe.com");
701
+ expect(options["url"]).toBeEqualTo("http://marydoe.com");
702
+ });
703
+
704
+ specify("prepare options should return an empty hash when values are null", function(){
705
+ expect({}).toBeEqualTo(I18n.prepareOptions(null, null));
706
+ });
707
+
708
+ specify("percentage with defaults", function(){
709
+ expect(I18n.toPercentage(1234)).toBeEqualTo("1234.000%");
710
+ });
711
+
712
+ specify("percentage with custom options", function(){
713
+ actual = I18n.toPercentage(1234, {delimiter: "_", precision: 0});
714
+ expect(actual).toBeEqualTo("1_234%");
715
+ });
716
+
717
+ specify("percentage with translation", function(){
718
+ I18n.translations.en.number = {
719
+ percentage: {
720
+ format: {
721
+ precision: 2,
722
+ delimiter: ".",
723
+ separator: ","
724
+ }
725
+ }
726
+ };
727
+
728
+ expect(I18n.toPercentage(1234)).toBeEqualTo("1.234,00%");
729
+ });
730
+
731
+ specify("percentage with translation and custom options", function(){
732
+ I18n.translations.en.number = {
733
+ percentage: {
734
+ format: {
735
+ precision: 2,
736
+ delimiter: ".",
737
+ separator: ","
738
+ }
739
+ }
740
+ };
741
+
742
+ actual = I18n.toPercentage(1234, {precision: 4, delimiter: "-", separator: "+"});
743
+ expect(actual).toBeEqualTo("1-234+0000%");
744
+ });
745
+
746
+ specify("scope option as string", function(){
747
+ actual = I18n.t("stranger", {scope: "greetings"});
748
+ expect(actual).toBeEqualTo("Hello stranger!");
749
+ });
750
+
751
+ specify("scope as array", function(){
752
+ actual = I18n.t(["greetings", "stranger"]);
753
+ expect(actual).toBeEqualTo("Hello stranger!");
754
+ });
755
+
756
+ specify("new placeholder syntax", function(){
757
+ I18n.translations["en"]["new_syntax"] = "Hi %{name}!";
758
+ actual = I18n.t("new_syntax", {name: "John"});
759
+ expect(actual).toBeEqualTo("Hi John!");
760
+ });
761
+
762
+ specify("return translation for custom scope separator", function(){
763
+ I18n.defaultSeparator = "•";
764
+ actual = I18n.t("greetings•stranger");
765
+ expect(actual).toBeEqualTo("Hello stranger!");
766
+ });
767
+
768
+ specify("return number as human size", function(){
769
+ kb = 1024;
770
+
771
+ expect(I18n.toHumanSize(1)).toBeEqualTo("1Byte");
772
+ expect(I18n.toHumanSize(100)).toBeEqualTo("100Bytes");
773
+
774
+ expect(I18n.toHumanSize(kb)).toBeEqualTo("1KB");
775
+ expect(I18n.toHumanSize(kb * 1.5)).toBeEqualTo("1.5KB");
776
+
777
+ expect(I18n.toHumanSize(kb * kb)).toBeEqualTo("1MB");
778
+ expect(I18n.toHumanSize(kb * kb * 1.5)).toBeEqualTo("1.5MB");
779
+
780
+ expect(I18n.toHumanSize(kb * kb * kb)).toBeEqualTo("1GB");
781
+ expect(I18n.toHumanSize(kb * kb * kb * 1.5)).toBeEqualTo("1.5GB");
782
+
783
+ expect(I18n.toHumanSize(kb * kb * kb * kb)).toBeEqualTo("1TB");
784
+ expect(I18n.toHumanSize(kb * kb * kb * kb * 1.5)).toBeEqualTo("1.5TB");
785
+
786
+ expect(I18n.toHumanSize(kb * kb * kb * kb * kb)).toBeEqualTo("1024TB");
787
+ });
788
+
789
+ specify("return number as human size using custom options", function(){
790
+ expect(I18n.toHumanSize(1024 * 1.6, {precision: 0})).toBeEqualTo("2KB");
791
+ });
792
+
793
+ specify("return number without insignificant zeros", function(){
794
+ options = {precision: 4, strip_insignificant_zeros: true};
795
+
796
+ expect(I18n.toNumber(65, options)).toBeEqualTo("65");
797
+ expect(I18n.toNumber(1.2, options)).toBeEqualTo("1.2");
798
+ expect(I18n.toCurrency(1.2, options)).toBeEqualTo("$1.2");
799
+ expect(I18n.toHumanSize(1.2, options)).toBeEqualTo("1.2Bytes");
800
+ });
801
+
802
+ specify("return plural key for given count for english locale", function(){
803
+ expect(I18n.pluralizationRules.en(0)).toBeEqualTo(["zero", "none", "other"]);
804
+ expect(I18n.pluralizationRules.en(1)).toBeEqualTo("one");
805
+ expect(I18n.pluralizationRules.en(2)).toBeEqualTo("other");
806
+ });
807
+
808
+ specify("return given pluralizer", function(){
809
+ I18n.pluralizationRules.en = "en";
810
+ expect(I18n.pluralizer("ru")).toBeEqualTo("en");
811
+ I18n.pluralizationRules.ru = "ru";
812
+ expect(I18n.pluralizer("ru")).toBeEqualTo("ru");
813
+ });
814
+
815
+ specify("find and translate valid node", function(){
816
+ expect(I18n.findAndTranslateValidNode(["one", "other"], {one: "one", other: "other"})).toBeEqualTo("one");
817
+ expect(I18n.findAndTranslateValidNode(["one", "other"], {other: "other"})).toBeEqualTo("other");
818
+ expect(I18n.findAndTranslateValidNode(["one"], {})).toBeEqualTo(null);
819
+ });
820
+ });