i18n-js 4.0.0 → 4.2.3

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/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img width="250" height="58" src="https://github.com/fnando/i18n-js/raw/main/i18njs.png" alt="i18n.js">
2
+ <img width="250" height="58" src="https://github.com/fnando/i18n-js/raw/main/images/i18njs.png" alt="i18n.js">
3
3
  </p>
4
4
 
5
5
  <p align="center">
@@ -12,14 +12,14 @@
12
12
  <small>
13
13
  Oh, you don't use Ruby? No problem! You can still use i18n-js
14
14
  <br>
15
- and the
16
- <a href="https://www.npmjs.com/package/i18n-js/v/next">companion JavaScript package</a>.
15
+ and the
16
+ <a href="https://www.npmjs.com/package/i18n-js/v/latest">companion JavaScript package</a>.
17
17
  </small>
18
18
  </p>
19
19
 
20
20
  <p align="center">
21
21
  <a href="https://github.com/fnando/i18n-js"><img src="https://github.com/fnando/i18n-js/workflows/ruby-tests/badge.svg" alt="Tests"></a>
22
- <a href="https://rubygems.org/gems/i18n-js"><img src="https://img.shields.io/gem/v/i18n-js.svg?include_prereleases" alt="Gem"></a>
22
+ <a href="https://rubygems.org/gems/i18n-js"><img src="https://img.shields.io/gem/v/i18n-js.svg" alt="Gem"></a>
23
23
  <a href="https://rubygems.org/gems/i18n-js"><img src="https://img.shields.io/gem/dt/i18n-js.svg" alt="Gem"></a>
24
24
  <a href="https://tldrlegal.com/license/mit-license"><img src="https://img.shields.io/:License-MIT-blue.svg" alt="MIT License"></a>
25
25
  </p>
@@ -45,6 +45,14 @@ About patterns:
45
45
  - `*.messages.*`
46
46
  - Patterns starting with `!` are excluded.
47
47
  - `!*.activerecord.*` will exclude all ActiveRecord translations.
48
+ - You can use groups:
49
+ - `{pt-BR,en}.js.*` will include only `pt-BR` and `en` translations, even if
50
+ more languages are available.
51
+
52
+ > **Note**:
53
+ >
54
+ > Patterns use [glob](https://rubygems.org/gems/glob), so check it out for the
55
+ > most up-to-date documentation about what's available.
48
56
 
49
57
  The config file:
50
58
 
@@ -68,6 +76,22 @@ The output path can use the following placeholders:
68
76
  - `:locale`: the language that's being exported.
69
77
  - `:digest`: the MD5 hex digest of the exported file.
70
78
 
79
+ The config file is processed as erb, so you can have dynamic content on it if
80
+ you want. The following example shows how to use groups from a variable.
81
+
82
+ ```yml
83
+ ---
84
+ <% group = "{en,pt}" %>
85
+
86
+ translations:
87
+ - file: app/frontend/translations.json
88
+ patterns:
89
+ - "<%= group %>.*"
90
+ - "!<%= group %>.activerecord"
91
+ - "!<%= group %>.errors"
92
+ - "!<%= group %>.number.nth"
93
+ ```
94
+
71
95
  The Ruby API:
72
96
 
73
97
  ```ruby
@@ -88,7 +112,9 @@ Commands:
88
112
  - init: Initialize a project
89
113
  - export: Export translations as JSON files
90
114
  - version: Show package version
91
- - check: Check for missing translations
115
+ - plugins: List plugins that will be activated
116
+ - lint:translations: Check for missing translations
117
+ - lint:scripts: Lint files using TypeScript
92
118
 
93
119
  Run `i18n COMMAND --help` for more information on specific commands.
94
120
  ```
@@ -97,14 +123,181 @@ By default, `i18n` will use `config/i18n.yml` and `config/environment.rb` as the
97
123
  configuration files. If you don't have these files, then you'll need to specify
98
124
  both `--config` and `--require`.
99
125
 
126
+ ### Plugins
127
+
128
+ #### Built-in plugins:
129
+
130
+ ##### `embed_fallback_translations`:
131
+
132
+ Embed fallback translations inferred from the default locale. This can be useful
133
+ in cases where you have multiple large translation files and don't want to load
134
+ the default locale together with the target locale.
135
+
136
+ To use it, add the following to your configuration file:
137
+
138
+ ```yaml
139
+ embed_fallback_translations:
140
+ enabled: true
141
+ ```
142
+
143
+ ##### `export_files`:
144
+
145
+ By default, i18n-js will export only JSON files out of your translations. This
146
+ plugin allows exporting other file formats. To use it, add the following to your
147
+ configuration file:
148
+
149
+ ```yaml
150
+ export_files:
151
+ enabled: true
152
+ files:
153
+ - template: path/to/template.erb
154
+ output: "%{dir}/%{base_name}.ts"
155
+ ```
156
+
157
+ You can export multiple files by define more entries.
158
+
159
+ The output name can use the following placeholders:
160
+
161
+ - `%{dir}`: the directory where the translation file is.
162
+ - `%{name}`: file name with extension.
163
+ - `%{base_name}`: file name without extension.
164
+ - `%{digest}`: MD5 hexdigest from the generated file.
165
+
166
+ The template file must be a valid eRB template. You can execute arbitrary Ruby
167
+ code, so be careful. An example of how you can generate a file can be seen
168
+ below:
169
+
170
+ ```erb
171
+ /* eslint-disable */
172
+ <%= banner %>
173
+
174
+ import { i18n } from "config/i18n";
175
+
176
+ i18n.store(<%= JSON.pretty_generate(translations) %>);
177
+ ```
178
+
179
+ This template is loading the instance from `config/i18n` and storing the
180
+ translations that have been loaded. The
181
+ `banner(comment: "// ", include_time: true)` method is built-in. The generated
182
+ file will look something like this:
183
+
184
+ ```typescript
185
+ /* eslint-disable */
186
+ // File generated by i18n-js on 2022-12-10 15:37:00 +0000
187
+
188
+ import { i18n } from "config/i18n";
189
+
190
+ i18n.store({
191
+ en: {
192
+ "bunny rabbit adventure": "bunny rabbit adventure",
193
+ "hello sunshine!": "hello sunshine!",
194
+ "time for bed!": "time for bed!",
195
+ },
196
+ es: {
197
+ "bunny rabbit adventure": "conejito conejo aventura",
198
+ bye: "adios",
199
+ "time for bed!": "hora de acostarse!",
200
+ },
201
+ pt: {
202
+ "bunny rabbit adventure": "a aventura da coelhinha",
203
+ bye: "tchau",
204
+ "time for bed!": "hora de dormir!",
205
+ },
206
+ });
207
+ ```
208
+
209
+ #### Plugin API
210
+
211
+ You can transform the exported translations by adding plugins. A plugin must
212
+ inherit from `I18nJS::Plugin` and can have 4 class methods (they're all optional
213
+ and will default to a noop implementation). For real examples, see [lib/i18n-js/embed_fallback_translations_plugin.rb](https://github.com/fnando/i18n-js/blob/main/lib/i18n-js/embed_fallback_translations_plugin.rb) and [lib/i18n-js/export_files_plugin.rb](https://github.com/fnando/i18n-js/blob/main/lib/i18n-js/export_files_plugin.rb)
214
+
215
+ ```ruby
216
+ # frozen_string_literal: true
217
+
218
+ module I18nJS
219
+ class SamplePlugin < I18nJS::Plugin
220
+ # This method is responsible for transforming the translations. The
221
+ # translations you'll receive may be already be filtered by other plugins
222
+ # and by the default filtering itself. If you need to access the original
223
+ # translations, use `I18nJS.translations`.
224
+ def transform(translations:)
225
+ # transform `translations` here…
226
+
227
+ translations
228
+ end
229
+
230
+ # In case your plugin accepts configuration, this is where you must validate
231
+ # the configuration, making sure only valid keys and type is provided.
232
+ # If the configuration contains invalid data, then you must raise an
233
+ # exception using something like
234
+ # `raise I18nJS::Schema::InvalidError, error_message`.
235
+ #
236
+ # Notice the validation will only happen when the plugin configuration is
237
+ # set (i.e. the configuration contains your config key).
238
+ def validate_schema
239
+ # validate plugin schema here…
240
+ end
241
+
242
+ # This method must set up the basic plugin configuration, like adding the
243
+ # config's root key in case your plugin accepts configuration (defined via
244
+ # the config file).
245
+ #
246
+ # If you don't add this key, the linter will prevent non-default keys from
247
+ # being added to the configuration file.
248
+ def setup
249
+ # If you plugin has configuration, uncomment the line below
250
+ # I18nJS::Schema.root_keys << config_key
251
+ end
252
+
253
+ # This method is called whenever `I18nJS.call(**kwargs)` finishes exporting
254
+ # JSON files based on your configuration.
255
+ #
256
+ # You can use it to further process exported files, or generate new files
257
+ # based on the translations that have been exported.
258
+ def after_export(files:)
259
+ # process exported files here…
260
+ end
261
+ end
262
+ end
263
+ ```
264
+
265
+ The class `I18nJS::Plugin` implements some helper methods that you can use:
266
+
267
+ - `I18nJS::Plugin#config_key`: the configuration key that was inferred out of
268
+ your plugin's class name.
269
+ - `I18nJS::Plugin#config`: the plugin configuration.
270
+ - `I18nJS::Plugin#enabled?`: whether the plugin is enabled or not based on the
271
+ plugin's configuration.
272
+
273
+ To distribute this plugin, you need to create a gem package that matches the
274
+ pattern `i18n-js/*_plugin.rb`. You can test whether your plugin will be found by
275
+ installing your gem, opening a iRB session and running
276
+ `Gem.find_files("i18n-js/*_plugin.rb")`. If your plugin is not listed, then you
277
+ need to double check your gem load path and see why the file is not being
278
+ loaded.
279
+
100
280
  ### Listing missing translations
101
281
 
102
- To list missing and extraneous translations, you can use `i18n check`. This
103
- command will load your translations similarly to how `i18n export` does, but
104
- will output the list of keys that don't have a matching translation against the
105
- default locale. Here's an example:
282
+ To list missing and extraneous translations, you can use
283
+ `i18n lint:translations`. This command will load your translations similarly to
284
+ how `i18n export` does, but will output the list of keys that don't have a
285
+ matching translation against the default locale. Here's an example:
106
286
 
107
- ![`i18n check` command in action](https://github.com/fnando/i18n-js/raw/main/images/i18njs-check.gif)
287
+ ```console
288
+ $ i18n lint:translations
289
+ => Config file: "./config/i18n.yml"
290
+ => Require file: "./config/environment.rb"
291
+ => Check "./config/i18n.yml" for ignored keys.
292
+ => en: 232 translations
293
+ => pt-BR: 5 missing, 1 extraneous, 1 ignored
294
+ - pt-BR.actors.github.metrics (missing)
295
+ - pt-BR.actors.github.metrics_hint (missing)
296
+ - pt-BR.actors.github.repo_metrics (missing)
297
+ - pt-BR.actors.github.repository (missing)
298
+ - pt-BR.actors.github.user_metrics (missing)
299
+ - pt-BR.github.repository (extraneous)
300
+ ```
108
301
 
109
302
  This command will exist with status 1 whenever there are missing translations.
110
303
  This way you can use it as a CI linting.
@@ -125,19 +318,97 @@ translations:
125
318
  patterns:
126
319
  - "*"
127
320
 
128
- check:
321
+ lint_translations:
129
322
  ignore:
130
323
  - en.mailer.login.subject
131
324
  - en.mailer.login.body
132
325
  ```
133
326
 
134
- > **Note**: In order to avoid mistakenly ignoring keys, this configuration
135
- > option only accepts the full translation scope, rather than accepting a
136
- > pattern like `pt.ignored.scope.*`.
327
+ > **Note**:
328
+ >
329
+ > In order to avoid mistakenly ignoring keys, this configuration option only
330
+ > accepts the full translation scope, rather than accepting a pattern like
331
+ > `pt.ignored.scope.*`.
332
+
333
+ ### Linting your JavaScript/TypeScript files
334
+
335
+ To lint your script files and check for missing translations (which can signal
336
+ that you're either using wrong scopes or forgot to add the translation), use
337
+ `i18n lint:scripts`. This command will parse your JavaScript/TypeScript files
338
+ and extract all scopes being used. This command requires a Node.js runtime. You
339
+ can either specify one via `--node-path`, or let the plugin infer a binary from
340
+ your `$PATH`.
341
+
342
+ The comparison will be made against the export JSON files, which means it'll
343
+ consider transformations performed by plugins (e.g. the output files may be
344
+ affected by `embed_fallback_translations` plugin).
345
+
346
+ The translations that will be extract must be called as one of the following
347
+ ways:
348
+
349
+ - `i18n.t(scope, options)`
350
+ - `i18n.translate(scope, options)`
351
+ - `t(scope, options)`
352
+
353
+ Notice that only literal strings can be used, as in `i18n.t("message")`. If
354
+ you're using dynamic scoping through variables (e.g.
355
+ `const scope = "message"; i18n.t(scope)`), they will be skipped.
356
+
357
+ ```console
358
+ $ i18n lint:scripts
359
+ => Config file: "./config/i18n.yml"
360
+ => Require file: "./config/environment.rb"
361
+ => Node: "/Users/fnando/.asdf/shims/node"
362
+ => Available locales: [:en, :es, :pt]
363
+ => Patterns: ["!(node_modules)/**/*.js", "!(node_modules)/**/*.ts", "!(node_modules)/**/*.jsx", "!(node_modules)/**/*.tsx"]
364
+ => 9 translations, 11 missing, 4 ignored
365
+ - test/scripts/lint/file.js:1:1: en.js.missing
366
+ - test/scripts/lint/file.js:1:1: es.js.missing
367
+ - test/scripts/lint/file.js:1:1: pt.js.missing
368
+ - test/scripts/lint/file.js:2:8: en.base.js.missing
369
+ - test/scripts/lint/file.js:2:8: es.base.js.missing
370
+ - test/scripts/lint/file.js:2:8: pt.base.js.missing
371
+ - test/scripts/lint/file.js:4:8: en.js.missing
372
+ - test/scripts/lint/file.js:4:8: es.js.missing
373
+ - test/scripts/lint/file.js:4:8: pt.js.missing
374
+ - test/scripts/lint/file.js:6:1: en.another_ignore_scope
375
+ - test/scripts/lint/file.js:6:1: es.another_ignore_scope
376
+ ```
377
+
378
+ This command will list all locales and their missing translations. Avoid listing
379
+ a particular translation, you can set `lint.ignore` on your config file.
380
+
381
+ ```yaml
382
+ ---
383
+ translations:
384
+ - file: app/frontend/translations.json
385
+ patterns:
386
+ - "*"
387
+
388
+ lint_scripts:
389
+ ignore:
390
+ - ignore_scope # will ignore this scope on all languages
391
+ - pt.another_ignore_scope # will ignore this scope only on `pt`
392
+ ```
393
+
394
+ You can also set the patterns that will be looked up. By default, it scans all
395
+ JavaScript and TypeScript files that don't live on `node_modules`.
396
+
397
+ ```yaml
398
+ ---
399
+ translations:
400
+ - file: app/frontend/translations.json
401
+ patterns:
402
+ - "*"
403
+
404
+ lint:
405
+ patterns:
406
+ - "app/assets/**/*.ts"
407
+ ```
137
408
 
138
409
  ## Automatically export translations
139
410
 
140
- ### Using watchman
411
+ ### Using [watchman](https://facebook.github.io/watchman/)
141
412
 
142
413
  Create a script at `bin/i18n-watch`.
143
414
 
@@ -182,11 +453,11 @@ line to your Procfile:
182
453
  i18n: ./bin/i18n-watch
183
454
  ```
184
455
 
185
- ### Using guard
456
+ ### Using [guard](https://rubygems.org/gems/guard)
186
457
 
187
- Install [guard](https://rubygems.org/packages/guard) and
188
- [guard-compat](https://rubygems.org/packages/guard-compat). Then create a
189
- Guardfile with the following configuration:
458
+ Install [guard](https://rubygems.org/gems/guard) and
459
+ [guard-compat](https://rubygems.org/gems/guard-compat). Then create a Guardfile
460
+ with the following configuration:
190
461
 
191
462
  ```ruby
192
463
  guard(:"i18n-js",
@@ -204,7 +475,7 @@ accordingly.
204
475
 
205
476
  Now you can run `guard start -i`.
206
477
 
207
- ### Using listen
478
+ ### Using [listen](https://rubygems.org/gems/listen)
208
479
 
209
480
  Create a file under `config/initializers/i18n.rb` with the following content:
210
481
 
@@ -216,8 +487,25 @@ end
216
487
  ```
217
488
 
218
489
  The code above will watch for changes based on `config/i18n.yml` and
219
- `config/locales`. You can customize these options with
220
- `I18nJS.listen(config_file: "config/i18n.yml", locales_dir: "config/locales")`.
490
+ `config/locales`. You can customize these options:
491
+
492
+ - `config_file` - i18n-js configuration file
493
+ - `locales_dir` - one or multiple directories to watch for locales changes
494
+ - `options` - passed directly to
495
+ [listen](https://github.com/guard/listen/#options)
496
+ - `run_on_start` - export files on start. Defaults to `true`. When disabled,
497
+ files will be exported only when there are file changes.
498
+
499
+ Example:
500
+
501
+ ```ruby
502
+ I18nJS.listen(
503
+ config_file: "config/i18n.yml",
504
+ locales_dir: ["config/locales", "app/views"],
505
+ options: {only: %r{.yml$}},
506
+ run_on_start: false
507
+ )
508
+ ```
221
509
 
222
510
  ### Integrating with your frontend
223
511
 
@@ -227,6 +515,13 @@ that loads all the exported translation.
227
515
 
228
516
  ### FAQ
229
517
 
518
+ #### I'm running v3. Is there a migration plan?
519
+
520
+ [There's a document](https://github.com/fnando/i18n-js/tree/main/MIGRATING_FROM_V3_TO_V4.md)
521
+ outlining some of the things you need to do to migrate from v3 to v4. It may not
522
+ be as complete as we'd like it to be, so let us know if you face any issues
523
+ during the migration is not outline is that document.
524
+
230
525
  #### How can I export translations without having a database around?
231
526
 
232
527
  Some people may have a build process using something like Docker that don't
@@ -246,9 +541,11 @@ require "action_view/railtie"
246
541
  I18n.load_path += Dir["./config/locales/**/*.yml"]
247
542
  ```
248
543
 
249
- Notice that you may not need to load ActiveSupport and ActionView lines, or even
250
- may need to add additional requires for other libs. With this approach you have
251
- full control on what's going to be loaded.
544
+ > **Note**:
545
+ >
546
+ > You may not need to load ActiveSupport and ActionView lines, or even may need
547
+ > to add additional requires for other libs. With this approach you have full
548
+ > control on what's going to be loaded.
252
549
 
253
550
  ## Maintainer
254
551
 
data/bin/release ADDED
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "optparse"
5
+ require_relative "../lib/i18n-js/version"
6
+
7
+ def write_file(path, contents)
8
+ File.open(File.expand_path(path), "w") do |io|
9
+ io << contents
10
+ end
11
+ end
12
+
13
+ changelog_path = "./CHANGELOG.md"
14
+ version_path = "./lib/i18n-js/version.rb"
15
+
16
+ version = nil
17
+ segments = I18nJS::VERSION.split(".")
18
+ major, minor, patch = *segments.take(3).map(&:to_i)
19
+ pre = segments[4].to_s
20
+ pre_version = pre.gsub(/[^\d]/m, "").to_i
21
+ date = Time.now.strftime("%b %d, %Y")
22
+ dry_run = false
23
+ alpha = false
24
+
25
+ OptionParser.new do |opts|
26
+ opts.on("--major") do
27
+ version = "#{major + 1}.0.0"
28
+ end
29
+
30
+ opts.on("--minor") do
31
+ version = "#{major}.#{minor + 1}.0"
32
+ end
33
+
34
+ opts.on("--patch") do
35
+ version = "#{major}.#{minor}.#{patch + 1}"
36
+ end
37
+
38
+ opts.on("--alpha") do
39
+ alpha = true
40
+ end
41
+
42
+ opts.on("--dry-run") do
43
+ dry_run = true
44
+ end
45
+ end.parse!
46
+
47
+ version = "#{version}.alpha#{pre_version + 1}" if alpha
48
+
49
+ unless version
50
+ puts "ERROR: You need to use either one of: --major, --minor, --patch"
51
+ exit 1
52
+ end
53
+
54
+ puts "=> Current version: #{I18nJS::VERSION}"
55
+ puts "=> Next version: #{version}"
56
+
57
+ system "yarn", "install"
58
+ system "yarn", "compile"
59
+
60
+ write_file changelog_path,
61
+ File.read(changelog_path)
62
+ .gsub("Unreleased", "v#{version} - #{date}")
63
+
64
+ puts "=> Updated #{changelog_path}"
65
+
66
+ write_file version_path,
67
+ File.read(version_path)
68
+ .gsub(/VERSION = ".*?"/, %[VERSION = "#{version}"])
69
+
70
+ puts "=> Updated #{version_path}"
71
+
72
+ unless dry_run
73
+ system "git", "add", changelog_path, version_path
74
+ system "git", "commit", "-m", "Bump up version (v#{version})"
75
+ system "rake", "release"
76
+ end
77
+
78
+ if dry_run
79
+ system "rake", "build"
80
+ system "git", "checkout", changelog_path, version_path
81
+ end
data/i18n-js.gemspec CHANGED
@@ -28,19 +28,22 @@ Gem::Specification.new do |spec|
28
28
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
29
29
  `git ls-files -z`
30
30
  .split("\x0")
31
- .reject {|f| f.match(%r{^(test|spec|features)/}) }
31
+ .reject {|f| f.match(%r{^(test|spec|features|images)/}) }
32
32
  end
33
33
 
34
+ spec.files << "lib/i18n-js/lint.js"
35
+
34
36
  spec.bindir = "exe"
35
37
  spec.executables = spec.files.grep(%r{^exe/}) {|f| File.basename(f) }
36
38
  spec.require_paths = ["lib"]
37
39
 
38
- spec.add_dependency "glob"
40
+ spec.add_dependency "glob", ">= 0.4.0"
39
41
  spec.add_dependency "i18n"
40
42
 
41
43
  spec.add_development_dependency "activesupport"
42
44
  spec.add_development_dependency "minitest"
43
45
  spec.add_development_dependency "minitest-utils"
46
+ spec.add_development_dependency "mocha"
44
47
  spec.add_development_dependency "pry-meta"
45
48
  spec.add_development_dependency "rake"
46
49
  spec.add_development_dependency "rubocop"
data/lib/guard/i18n-js.rb CHANGED
@@ -53,13 +53,30 @@ module Guard
53
53
  info("Changes detected: #{changes.join(', ')}") if changes
54
54
 
55
55
  @current_thread = Thread.new do
56
- require @require_file
57
- ::I18nJS.call(config_file: @config_file)
56
+ capture do
57
+ system "i18n",
58
+ "export",
59
+ "--config",
60
+ config_file.to_s,
61
+ "--require",
62
+ require_file.to_s,
63
+ "--quiet"
64
+ end
58
65
  end
59
66
 
60
67
  current_thread.join
61
68
  end
62
69
 
70
+ def capture
71
+ original = $stdout
72
+ $stdout = StringIO.new
73
+ yield
74
+ rescue StandardError
75
+ # noop
76
+ ensure
77
+ $stdout = original
78
+ end
79
+
63
80
  def validate_file(key, file)
64
81
  return true if file && File.file?(file)
65
82
 
@@ -68,11 +85,11 @@ module Guard
68
85
  end
69
86
 
70
87
  def error(message)
71
- ::Guard::UI.error "[guard-i18n-js] #{message}"
88
+ ::Guard::UI.error "[i18n-js] #{message}"
72
89
  end
73
90
 
74
91
  def info(message)
75
- ::Guard::UI.info "[guard-i18n-js] #{message}"
92
+ ::Guard::UI.info "[i18n-js] #{message}"
76
93
  end
77
94
  end
78
95
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module I18nJS
4
+ def self.clean_hash(hash)
5
+ hash.keys.each_with_object({}) do |key, buffer|
6
+ value = hash[key]
7
+
8
+ next if value.is_a?(Proc)
9
+
10
+ buffer[key] = value.is_a?(Hash) ? clean_hash(value) : value
11
+ end
12
+ end
13
+ end