zlocalize 6.1.1 → 7.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5bdb63373ea3f28f85783a225ebd23994ca94471002daa8b807a45cb0bedea09
4
- data.tar.gz: 7c437e2684b8bb3e9849fac615360fd8b905c01f0672d0a8ba4be08ff5ae43b6
3
+ metadata.gz: 87f72c65f42afb37957fea37b99f9ff4ed4f1b5c80158644b5e333213796f551
4
+ data.tar.gz: c2f63f596d3f50a34c5672337124a196106b7246c1e2cb013eb0d46a5e12a5f1
5
5
  SHA512:
6
- metadata.gz: 594c2d4bede9156ba7b889dd14037f3a0884740cf46c0a8e22630e0a3e106ad8ece94acc822041cf4817d9a783f216c0c8afcb9b7fb6a3e166ce55d2a12e3451
7
- data.tar.gz: 1c938f2f00cbeab214cf20aea705004190bfe74afc434d9409e1395d6bedd9fc5dd3ce221f6eeab93de218a3eb60cb9581729b24e378127fb986f17300ab66a9
6
+ metadata.gz: 4f13ccb6f5dbdc0b0fb166f3ad36f948e72fa2097bdab4a47c67f5d83de9331c68597c02079705c63c3e8b7502218e31d4efb6b49ccbc86ba47791dff84321ef
7
+ data.tar.gz: d2572db0055d54d5f7c10bae01b5eb54e517a5bc810aa0f2ac3ad580a71bf145bc803978e2598437bbf1da2c57aaa2e3c751dddfb9587285cb0b66bfbe733c2a
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008-2020 Charles Bedard (zzeligg@gmail.com)
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,584 @@
1
+ # ZLocalize - A translation engine for Rails 7.1+ applications
2
+
3
+ `ZLocalize` provides string translation through YAML-defined dictionaries.
4
+
5
+ What it does:
6
+
7
+ * It uses strings in the source code as keys for lookups with interpolation
8
+ of values.
9
+ * It features a harvester rake task which scans the application source code
10
+ (controllers, models, helpers and views) and to extract all the strings used
11
+ in calls to translation methods.
12
+ * The harvester also *manages* the translation entries, e.g. each time it
13
+ runs, it will update the translation dictionaries without destroying
14
+ existing entries or creating duplicate entries.
15
+ * Translation dictionaries are stored as YAML files, containing metadata (such
16
+ as unique entry ID's and a list of references where an entry is used, much
17
+ like gettext .po files).
18
+ * Provides a simple way to manage used-generated content in multiple languages
19
+ by including modules an ActiveRecord.
20
+ * Provides conversion of locale-formatted decimal values assigned to
21
+ ActiveRecord attributes by users (through forms, console, etc.).
22
+
23
+ What it does not do:
24
+
25
+ * Localization of date/time or other regional-dependent data (except for
26
+ input of decimal values). For localization, you must use `I18n#localize`.
27
+ * Provide an interface to edit and manage translations. You have to edit the
28
+ YAML files by hand.
29
+
30
+ Within a Rails application, `ZLocalize` does not replace the `I18n` backend. Its
31
+ operation is (almost) entirely independent from `I18n`. The only tie to `I18n`
32
+ is the internal use of the `I18n#locale` and `I18n#default_locale` methods to
33
+ get/set its own active and default locales. Setting the active locale through
34
+ `ZLocalize` will set `I18n`'s locale and vice-versa.
35
+
36
+ Other than that, `ZLocalize` keeps its translation data isolated from the
37
+ `I18n` translation data.
38
+
39
+ The reason for this architecture is that the Rails framework itself contains a
40
+ finite set of strings, and as such `I18n` is already doing a fine job with
41
+ translation and localization of the framework internal strings and helpers,
42
+ using a hierarchical data store with Ruby symbols as keys.
43
+
44
+ One of the main reasons `ZLocalize` was created was to avoid having to deal
45
+ with symbols as keys while developing. It is much easier and straightforward
46
+ to start coding your application using a base locale, and then run the
47
+ harvester to collect all the strings used in the translation calls throughout
48
+ the source code.
49
+
50
+ ## Requirements
51
+
52
+ * Ruby 3.4 or later
53
+ * Rails 7.1 or later
54
+
55
+ The harvester parses application source with [Prism](https://github.com/ruby/prism),
56
+ which ships in Ruby's standard library from Ruby 3.4 onwards. Earlier Ruby and
57
+ Rails releases are no longer supported: the `parser` gem used by ZLocalize 6.x has
58
+ no Ruby 4 target, and Rails 5.2/6.x are end-of-life.
59
+
60
+ If you really must use this gem with a previous version of Ruby (< 2.0) or Rails (< 6.0),
61
+ install [ZLocalize 4.2.3](https://github.com/zzeligg/zlocalize/releases/tag/4.2.3).
62
+
63
+ ## Installation
64
+
65
+ Add to your Gemfile:
66
+
67
+ gem 'zlocalize'
68
+
69
+ and run +bundle install+
70
+
71
+ ## Basic configuration
72
+
73
+ 1. Create an initializer:
74
+
75
+ bin/rails generate zlocalize:initializer
76
+
77
+ This will create `config/initializers/zlocalize.rb` with some default
78
+ configuration values.
79
+
80
+ You can configure I18n and ZLocalize in each their own initializer files,
81
+ or you can configure both in the same initializer.
82
+
83
+ First, set the default locale:
84
+
85
+ I18n.default_locale = :en
86
+
87
+ Calling `I18n.default_locale` and `I18n.locale` is the same as calling
88
+ `ZLocalize.default_locale` and `ZLocalize.locale`. The two are bound together.
89
+
90
+ 2. Edit the `ZLocalize.config.locales` structure, by specifying a Hash for
91
+ each locale you want to support. Each locale definition is a key/value pair.
92
+ For example, the French locale definition could be:
93
+
94
+ ZLocalize.config.locales = {
95
+ :fr => {
96
+ :plural_select => -> (n) { n <= 0 ? 0 : (n > 1 ? 2 : 1) },
97
+ :translations => File.join(Rails.root,'config/translations/fr.strings.yml'),
98
+ :titleize => -> (s) { s.capitalize.to_s },
99
+ :convert_float => -> (s) { s.to_s.gsub(' ','').gsub(',','.') }
100
+ }
101
+ }
102
+
103
+ * `:plural_select` is a proc evaluating the value of `n` for
104
+ translation of plural strings (in calls to `ZLocalize#pluralize` or `n_`). It returns
105
+ the index of the Array element to use as the translation for plural
106
+ expressions.
107
+
108
+ * `:translations` is an Array (or a single entry) of file names where the
109
+ translations are stored.
110
+
111
+ * `:titleize` is a proc to be used instead of ActiveSupport's `titleize`
112
+ helper. Call `ZLocalize.titleize` instead of the builtin titleize (helper or
113
+ `ActiveSupport::Inflector` method) in your application.
114
+
115
+ * `:convert_float` is a proc to be used to convert ActiveRecord
116
+ attributes containing decimal values. For example, in French, if the String
117
+ `"1 234,23"` is assigned to a AR instance, it will be converted to `"1234.23"`
118
+ so that Rails can then correctly convert it to `BigDecimal` internally.
119
+
120
+ Note that you can also call `ZLocalize#convert_float` anywhere in your
121
+ application.
122
+
123
+ 3. Set the other configuration values:
124
+
125
+ * `define_gettext_methods` : define `_()` and `n_()` (on `Object` class).
126
+ Defaults to `true`. If you do not define the gettext methods, you will
127
+ need to call `ZLocalize.translate` and `ZLocalize.pluralize` in your
128
+ application.
129
+
130
+ * `return_source_on_missing`: Hash with a key/value pair for each of your
131
+ application environments. This indicates if missing translations in a
132
+ given locale should raise a `ZLocalize::MissingTranslationDataError`
133
+ exception. Defaults to `{ :development => true, :test => false,
134
+ :production => false, :staging => false }`
135
+
136
+ * `harvest_paths` : Array of path patterns (same as `Dir.glob`) relative to
137
+ `Rails.root` that the ZLocalizer Harvester (see Harvester section below) will
138
+ scan to collect all calls to `_()`, `n_()`, `ZLocalize.translate` and
139
+ `ZLocalize.pluralize`. Defaults to
140
+ `["app/channels/**/*.rb", "app/controllers/**/*.rb", "app/helpers/**/*.rb",
141
+ "app/models/**/*.rb", "app/views/**/*.erb", "app/mailers/**/*.rb",
142
+ "app/jobs/**/*.rb", "lib/**/*.rb" ]`
143
+
144
+
145
+ ## In your application (Controllers, Helpers, Views, Models, etc.)
146
+
147
+ The idea is to simply start coding your application without thinking too much
148
+ about how to translate it. The only thing to worry about is wrapping any String
149
+ you will eventually want translated in a call to `_()` or `n_()` (or
150
+ `ZLocalize.translate` and `ZLocalize.pluralize`). Make sure you use parenthesis
151
+ for the parameter list to those methods (see Harvester section below for an
152
+ explanation).
153
+
154
+ For example, in a view:
155
+
156
+ <%= _("Dear user") %>
157
+ <%= n_(["No messages","One message", "{{count}} messages"], @user.messages.count) %>
158
+
159
+ Or in a controller:
160
+
161
+ def create
162
+ @post = Post.new(params[:post])
163
+ if @post.save
164
+ flash[:notice] = _("Your post has been added.")
165
+ redirect_to [@post] and return
166
+ else
167
+ flash[:error] = _("There was an error adding your post")
168
+ end
169
+ end
170
+
171
+ There are 2 methods to translate content:
172
+
173
+ * `ZLocalize.translate(key, options = {})` (and its gettext-style alias `_(key, options = {})`).
174
+ This method looks up the String `key` with the following `options`:
175
+
176
+ * `:locale` : Lookup key in this locale. Defaults to `I18n.current_locale`.
177
+ * `:default` : Return this String if key is not found.
178
+ * `:return_source_on_missing` : Override the global `ZLocalize.config.return_source_on_missing` value.
179
+
180
+ Any other option key passed is meant to be an interpolated named value. See Interpolation section below.
181
+
182
+ * `ZLocalize.pluralize(key, count, options = {})` (and its gettext-style alias `n_(key, count, options = {})`).
183
+ This methods looks up the Array `key` (which is an Array of String), and computes the index of the String to
184
+ return based on the value of `count`. The index is computed using the `:plural_select` Proc from
185
+ `ZLocalize.config` (see Basic Configuration section above).
186
+
187
+ The `options` are the same as the `translate` method. Also, the `count` parameter is also automatically
188
+ treated as a value to be interpolated (when `{{count}}` token is present in translation string).
189
+
190
+
191
+ ## Interpolation
192
+
193
+ ZLocalize supports interpolation of values in source strings. Simply enclose
194
+ the values in double curly braces (`{{` and `}}`), and pass the actual values
195
+ in a Hash as the 2nd parameter to `ZLocalize.translate` and
196
+ `ZLocalize.pluralize`. For example:
197
+
198
+ <%= _("Hello, {{username}}! How are you {{moment}}?",
199
+ :username => @user.name,
200
+ :moment => Time.now.hour > 18 ? _("tonight") : _("today") ) %>
201
+
202
+ If you need to output actual double curly braces, simply double-escape them:
203
+
204
+ <%= _("Hello, \\{{username}}") %>
205
+
206
+ The above will return "Hello, {{username}}" without interpolation of
207
+ `{{username}}`.
208
+
209
+ ## Namespacing Source Strings
210
+
211
+ It is possible to namespace (to give a scope to) a source string by simply
212
+ prefixing it with a name that ends with `::` . The namespace is used
213
+ when looking up the key in the current locale, but the prefix will always be
214
+ stripped in any output (even in the default locale).
215
+
216
+ Let's say your default locale is 'en':
217
+
218
+ <%= _('btn::Edit') %>
219
+
220
+ The above returns "Edit" if the current locale is the base locale (en in this
221
+ case), and will lookup `"btn::Edit"` (e.g. including namespace) for all other
222
+ locales and return the corresponding translation. The translation may include a
223
+ scope, but it will be removed too.
224
+
225
+ The idea behind such scopes is that some languages have different spelling or
226
+ even words for a given word in another locale. For example, "Update" in English
227
+ can be used both as a noun and a verb. But it is not always the case in other
228
+ languages. By adding scopes such as `"btn::Update"` or `"title::Update"`, we
229
+ infer context to the string, so other languages would translate differently
230
+ depending on the context (scope). And in the base locale, only the part
231
+ after `::` would be shown (the scope would be stripped).
232
+
233
+ Should you need to output the scope delimiter `'::'` as is, escape it with a
234
+ backslash:
235
+
236
+ <%= _("MyWebSite.com\\::HomePage") %>
237
+
238
+ The above will lookup `"MyWebSite.com\\::HomePage"`, but will not treat it as a
239
+ namespace prefix and will return the string with only the backslash stripped
240
+ (i.e. `"MyWebSite.com::HomePage"`).
241
+
242
+ ## Harvester
243
+
244
+ Once you're ready to translate any work you've done:
245
+
246
+ bin/rake zlocalize:harvest output=config/translations/fr.strings.yml
247
+
248
+ The harvester will scan your application (models, controllers, helpers, views
249
+ and any other path you might add). This will create a YAML file containing a
250
+ list of entries corresponding to all the strings used in the calls to
251
+ `_()` and `n_()` in your source code.
252
+
253
+ However, you have complete control over the location of your `ZLocalize`
254
+ translation files and can store them in any subdirectory of your Rails
255
+ application. See Basic Configuration section above.
256
+
257
+ Use `rake -D zlocalize:harvest` for a list of options.
258
+
259
+ #### HARVESTER LIMITATIONS
260
+
261
+ The parser used by the harvester module does have some limitations:
262
+
263
+ 1. You *must* use parenthesis in the calls to `_()` and `n_()` (or
264
+ `ZLocalize.translate` and `ZLocalize.pluralize`). While the Ruby language
265
+ allows to omit parenthesis for method parameters, the Harvester parser does
266
+ require them. The rule is simple: the parameter list to a translation
267
+ method (and the parameters of any nested calls) must use parenthesis.
268
+
269
+ This will not be harvested:
270
+
271
+ _ "Hello"
272
+
273
+ _ "Hello, {{name}}", :name => get_user_name @user
274
+
275
+ But this will:
276
+
277
+ _("Hello")
278
+
279
+ _("Hello, {{name}}", :name => get_user_name(@user))
280
+
281
+
282
+ 2. Translation calls inside interpolated double-quoted strings are not
283
+ supported.
284
+
285
+ While you can do something like:
286
+
287
+ "This is #{_('embedded')} in a string"
288
+
289
+ The Harvester parser will not detect the call. Avoid doing so.
290
+
291
+ ## Translation file format
292
+
293
+ Entries in the YAML file have the following format:
294
+
295
+ entry_00001:
296
+ id: 1
297
+ plural: false
298
+ ignore: false
299
+ references:
300
+ - /app/views/users/show.html.erb:12
301
+ source: "Dear user"
302
+ translation: "Cher utilisateur"
303
+
304
+ entry_00002:
305
+ id: 2
306
+ plural: true
307
+ ignore: false
308
+ references:
309
+ - /app/views/users/show.html.erb:21
310
+ source:
311
+ - "No messages"
312
+ - "One message"
313
+ - "{{count}} messages"
314
+ translation:
315
+ - "Aucun message"
316
+ - "Un message"
317
+ - "{{count}} messages"
318
+
319
+ Note that for plural entries, the source is the Array of strings passed to
320
+ `n_()` (or `ZLocalize.pluralize()` ) in the source file. It can have any number
321
+ of elements, as required by your base locale. The translation is also an Array,
322
+ and it too can have any number of elements as required by the target locale.
323
+ It is up to the `:plural_selector` proc to compute the correct index to use
324
+ in the translation array.
325
+
326
+ ## Updating translations when the application changes
327
+
328
+ Simply run `rake zlocalize:harvest` again, specifying as `output` the file that
329
+ already contains for the target language.
330
+
331
+ The Harvester will scan the source code and add/remove/modify the references to
332
+ all strings already present (if a string is not used anymore, it will be removed
333
+ only if you also add the `purge=true` parameter). It will of course also add any
334
+ missing string to the existing translation file.
335
+
336
+ ## Translation of user-generated content (ActiveRecord)
337
+
338
+ Any ActiveRecord model can be made to support multiple languages for its
339
+ attributes. `ZLocalize` provides 2 mechanisms to achieve this:
340
+
341
+ ### 1. Attached Translations
342
+
343
+ This method stores the translation of values in a separate model (judiciously)
344
+ called `Translation`.
345
+
346
+ Any model can have multiple translated values in multiple locales attached to
347
+ it. These values essentially become attributes of the model.
348
+
349
+ To use attached translations for a given model, declare `has_translations` for
350
+ that model. For example:
351
+
352
+ class Page < ActiveRecord::Base
353
+ ...
354
+ has_translations
355
+ ...
356
+ validates :title, translation: { required_locales: [:fr, :en] }
357
+ # `required_locales` can be a Symbol, in which case it refers to a method on the instance (no parameters)
358
+ # that will be called when the validation is performed.
359
+ # It can also be a Proc or lambda to be called, with (record, attribute, value) as parameters
360
+ # validates :title, translation: { required_locales: :get_required_locales }
361
+ # validates :title, translation: { required_locales: -> (record, attribute, value) { [:es, :fr] } }
362
+ ...
363
+ # return an Array of locale identifiers
364
+ def get_required_locales
365
+ [ :de, :en ]
366
+ end
367
+ end
368
+
369
+ From then on, any instance of Page will have the following methods:
370
+
371
+ * `#translate(attr_name,locale = nil)`
372
+ * `#add_translation(attr_name,locale,value)`
373
+ * `#insert_translations(locales = {})`
374
+
375
+ So, a typical use with our example would be:
376
+
377
+ In the controller:
378
+
379
+ def create
380
+ @page = Page.new(params[:page])
381
+ @page.add_translation('title',:fr,'Mon titre')
382
+ @page.save
383
+ end
384
+
385
+ And in a view (where the current locale is +:fr+):
386
+
387
+ <h3><%= @page.translate(title) %></h3> # => "Mon titre"
388
+
389
+ If you have some kind of administration interface, you can also mass-assign the
390
+ translations for a number of attributes and locales with the `insert_translations`
391
+ method.
392
+
393
+ `#insert_translations` accepts a Hash containing the translations for multiple
394
+ columns and locales. The Hash must have the locales as keys and its value is
395
+ another Hash of name-value pairs. As in:
396
+
397
+ @article.insert_translations(
398
+ { 'en' => { 'title' => "What's new this week",
399
+ 'synopsis' => "Learn what has happened this week"},
400
+ 'fr' => { 'title' => "Quoi de neuf cette semaine",
401
+ 'synopsis' => "Tout sur ce qui s'est passé cette semaine" }
402
+ })
403
+
404
+ Alternatively, you can use ActiveRecord's builtin `accepts_nested_attributes_for`
405
+ mechanism to mass-assign translations:
406
+
407
+ class Article < ActiveRecord::Base
408
+ has_translations
409
+ accepts_nested_attributes_for :translations, :allow_destroy => true
410
+ ...
411
+ end
412
+
413
+ And use `form.fields_for :translations` inside a view to have the translations
414
+ assigned directly...
415
+
416
+ The Translation model is loaded by `ZLocalize` itself, whenever a model
417
+ declares `has_translations`. The only thing missing is the translations table,
418
+ which you can generate with:
419
+
420
+ rails generate zlocalize:translations_migration
421
+
422
+ And run `rake db:migrate`.
423
+
424
+ Attached translations are very flexible, because the values are not stored in
425
+ the model itself (as opposed to translated columns, explained in the next
426
+ section below). This allows to easily add new locales with minimal changes
427
+ to the models that use them. All this at the cost of having an association with
428
+ the `Translation` model.
429
+
430
+ ### Validation of attached translations
431
+
432
+ `ZLocalize` also adds a Validator to ActiveRecord::Base, to check the presence
433
+ of attached validations:
434
+
435
+ class Page < ActiveRecord::Base
436
+
437
+ has_translations
438
+ validates_translation_of :content, :required_locales => [:fr, :en]
439
+ # or #
440
+ validates :content, :translation => { :required_locales => [:fr, :en] }
441
+ # or #
442
+ validates :content, :translation => { :required_locales => :get_required_locales }
443
+ ...
444
+
445
+ def get_required_locales
446
+ [:es, :en]
447
+ end
448
+
449
+ end
450
+
451
+ This will generate an error message for each missing translation:
452
+
453
+ p = @page.create
454
+
455
+ p.errors[:content] # => ["content is missing its translation in fr, en"]
456
+
457
+ The `required_locales` option can also be a Proc/lambda:
458
+
459
+ validates_translation_of [:title, :content], :required_locales => -> (record,attribute,value) {
460
+ if attribute == 'content'
461
+ [:fr,:en]
462
+ else
463
+ [:fr]
464
+ end }
465
+
466
+ The other standard validation options, such as `:message`, `:on`, `:if` and
467
+ `:unless` are also supported. `:message` defaults to `:missing_translations` and this value should be
468
+ added to the I18n translations for Rails, with the other ActiveRecord validation messages:
469
+
470
+ en:
471
+ errors:
472
+ messages: &errors_messages
473
+ inclusion: "n'est pas inclus(e) dans la liste"
474
+ exclusion: "n'est pas disponible"
475
+ ...
476
+ missing_translations: "doit être traduit en %{locales}"
477
+
478
+
479
+ and the `%{locales}` token will be interpolated to the actual missing locales.
480
+
481
+ ### 2. Translated columns
482
+
483
+ This second attribute translation mechanism works a bit differently. It is
484
+ basically a wrapper around column names, allowing to use different attribute
485
+ values for each locale.
486
+
487
+ First, you need to create the columns in the model table. You need one column
488
+ for each attribute and locale pair. For example, a `title` attribute with
489
+ values in French and English would require 2 columns:
490
+
491
+ create_table 'pages' do |t|
492
+ t.string :title_en
493
+ t.string :title_fr
494
+ ...
495
+ end
496
+
497
+ In your model:
498
+
499
+ class Page < ActiveRecord::Base
500
+ translates_columns [:title]
501
+ end
502
+
503
+ Then whenever you want to access the value of +title+ in the current locale:
504
+
505
+ <%= @page.title %>
506
+
507
+ The wrapper method can take a Hash of options:
508
+
509
+ * `:locale` to force a given locale
510
+ * `:fetch_default` (`true|false`) to retrieve the the value of the attribute
511
+ in the `default_locale`. Defaults to `true`
512
+
513
+ For example:
514
+
515
+ <%= @page.title(:locale => :fr, :fetch_default => false) %>
516
+
517
+ This will automatically call the wrapper method to read the value of the column
518
+ in the `:fr` locale (the value of `title_fr`). If `title_fr` is nil,
519
+ then the value in the `default_locale` will not be fetched and `nil` will be
520
+ returned. If `fetch_default` is `true`, then the value of `title_en` (given
521
+ that the `default_locale` is `:en`) would be fetched.
522
+
523
+ Translated columns are more efficient in terms of fetching translated content,
524
+ because they are always part of the underlying table columns. If you know in
525
+ advance the exact locales you are going support in your application, and that
526
+ only 2-3 locales will be present, then having multiple columns for each
527
+ attribute is probably the way to go. If you have many locales and/or the
528
+ attributes to be translated are large (text columns and such), then maybe
529
+ Attached Translations would be a better strategy.
530
+
531
+ ## Localized Decimal Attributes
532
+
533
+ `ZLocalize` provides a way to declare some ActiveRecord attributes as being
534
+ localized decimal values. This ensures the assignment of decimal values from
535
+ their string-based representation will not fail. For example, in French, the
536
+ decimal separator is a period ('.') and the thousands separator is a space.
537
+ Assigning the string `"1 234,56"` to a decimal attribute will cause the value
538
+ to become 1.0 (the `BigDecimal` class will parse the string, but stop at the
539
+ space).
540
+
541
+ To enable correct decimal attribute assignment for different locales, declare
542
+ the attributes as such:
543
+
544
+ class Account < ActiveRecord::Base
545
+ localize_decimal_attributes [:balance, :variation]
546
+ end
547
+
548
+ Then, you can safely assign a string representing a decimal value in the
549
+ current locale:
550
+
551
+ Zlocalize.locale = :fr
552
+ @account.balance = "8 765,43"
553
+ => 8765.43
554
+
555
+ Conversion from a locale-specific decimal value to `BigDecimal` is done by
556
+ the `:convert_float` Proc declared in `ZLocalize.locales` (in the
557
+ initializer). For example, in English:
558
+
559
+ ZLocalize.config.locales = {
560
+ :en => {
561
+ :plural_select => -> (n) { n <= 0 ? 0 : (n > 1 ? 2 : 1) },
562
+ :convert_float => -> (s) { s.to_s.gsub(',','') }
563
+ }
564
+ ...
565
+ }
566
+
567
+ ## License
568
+
569
+ `ZLocalize` is released under the MIT license.
570
+
571
+ ## Support
572
+
573
+ Source code, documentation, bug reports, feature requests or anything else is
574
+ at
575
+
576
+ * http://github.com/zzeligg/zlocalize
577
+
578
+ ## Credits
579
+
580
+ This plugin is based on original work by Thomas Fuchs (A Rails 1.X plugin named
581
+ Localization), but it has been extended in many ways to make it answer our needs.
582
+
583
+ Many thanks to Stephane Volet (https://github.com/schmlblk) for his contributions and ideas
584
+ to this gem.
@@ -1,11 +1,9 @@
1
- require 'parser/all'
1
+ require 'prism'
2
2
  # NOTE: This prevents Rails test environment to complain about this:
3
3
  # `ActionView::Template::Error: undefined method 'new' for module Erubi`
4
4
  # somehow Bundler or the autoload mechanism gets confused about which Erubi
5
5
  # implementation is defined
6
- unless defined?(ActionView::Template::Handlers::ERB::Erubi)
7
- require 'action_view/template/handlers/erb/erubi'
8
- end
6
+ require 'action_view'
9
7
  require 'action_view/template/handlers/erb/erubi'
10
8
  require File.join(File.dirname(__FILE__),'translation_file')
11
9
  require File.join(File.dirname(__FILE__),'harvester')
@@ -14,102 +12,101 @@ module ZLocalize
14
12
 
15
13
  class SourceProcessor
16
14
 
15
+ # Methods recognized as translation calls when called without an explicit
16
+ # receiver (bare calls), mapped to whether they yield plural entries.
17
+ BARE_TRANSLATE_METHODS = { '_' => false, 'n_' => true }.freeze
18
+
19
+ # Methods recognized as translation calls when called on the ZLocalize
20
+ # constant, mapped to whether they yield plural entries.
21
+ QUALIFIED_TRANSLATE_METHODS = { 'translate' => false, 'pluralize' => true }.freeze
22
+
17
23
  def initialize(filename, root, is_erb = false)
18
24
  @in_hash = 0
19
25
  @translate_calls = []
20
26
  @root = File.join(File.expand_path(root).downcase,'/') # add a trailing /
21
27
  @filename = File.expand_path(filename).downcase
22
28
  @relative_filename = @filename.gsub(@root,'')
29
+ @line_offset = 0
23
30
  content = File.open(filename, "r") { |f| f.read }
24
31
  if is_erb
25
32
  content = ActionView::Template::Handlers::ERB::Erubi.new(content, escape: true, trim: true).src
33
+ # Erubi compiles ERB to bare Ruby with top-level `yield`, which Prism
34
+ # rejects (yield is only valid inside a method). Wrap in a method and
35
+ # track the offset so reported line numbers still match the template.
36
+ content = "def __zlocalize_erb__\n#{content}\nend"
37
+ @line_offset = 1
26
38
  end
27
- @parser = create_parser_for_ruby_version
28
39
  begin
29
- @stree = @parser.parse(content)
30
- process(@stree)
40
+ process(content)
31
41
  rescue ArgumentError => ae
32
42
  raise ArgumentError.new("In #{filename} #{ae.message}")
33
43
  end
34
44
  end
35
45
 
36
- def create_parser_for_ruby_version
37
- md = RUBY_VERSION.match(/^(\d)\.(\d)/)
38
- begin
39
- kls = Object.const_get('Parser').const_get("Ruby#{md[1]}#{md[2]}")
40
- rescue
41
- raise "Unsupported Ruby version #{RUBY_VERSION}"
42
- end
46
+ def process(content)
47
+ parse_result = Prism.parse(content)
48
+ raise ArgumentError.new("Syntax error: #{format_parse_errors(parse_result)}") unless parse_result.success?
49
+ walk(parse_result.value)
43
50
  end
44
51
 
45
- def process(node)
46
- return unless node.is_a?(AST::Node)
47
- if node.type == :send
48
- if node.children[0] == nil
49
- if node.children[1] == :_
50
- process_translate_call(node) and return
51
- elsif node.children[1] == :n_
52
- process_pluralize_call(node) and return
52
+ def walk(node)
53
+ return unless node.is_a?(Prism::Node)
54
+
55
+ if node.is_a?(Prism::CallNode)
56
+ if node.receiver.nil?
57
+ plural = BARE_TRANSLATE_METHODS[node.name.to_s]
58
+ if !plural.nil? && node.arguments && node.arguments.arguments.size >= 1
59
+ record_call(node, plural) and return
53
60
  end
54
- elsif is_zlocalize_const?(node)
55
- if node.children[1] == :translate
56
- process_translate_call(node) and return
57
- elsif node.children[1] == :pluralize
58
- process_pluralize_call(node) and return
61
+ elsif zlocalize_const?(node.receiver)
62
+ plural = QUALIFIED_TRANSLATE_METHODS[node.name.to_s]
63
+ if !plural.nil? && node.arguments && node.arguments.arguments.size >= 1
64
+ record_call(node, plural) and return
59
65
  end
60
66
  end
61
67
  end
62
- node.children.each do |n|
63
- process(n)
64
- end
68
+
69
+ node.compact_child_nodes.each { |child| walk(child) }
65
70
  end
66
71
 
67
- def is_zlocalize_const?(node)
68
- return node.is_a?(AST::Node) && node.type == :const && node.children[1] == :ZLocalize
72
+ def record_call(node, plural)
73
+ argument_nodes = node.arguments.arguments
74
+ source = plural ? get_string_array_node_value(argument_nodes[0]) : get_string_node_value(argument_nodes[0])
75
+ @translate_calls << { name: node.name.to_s,
76
+ line_no: node.message_loc.start_line - @line_offset,
77
+ char_no: node.message_loc.start_column + 1,
78
+ parameter: source }
79
+ # keep harvesting inside any nested arguments (e.g. options hashes containing
80
+ # further translation calls)
81
+ argument_nodes[1..].each { |child| walk(child) } if argument_nodes.size > 1
69
82
  end
70
83
 
71
- def get_string_node_value(node)
72
- unless node.is_a?(AST::Node) && node.type == :str
73
- raise ArgumentError.new("On line #{node.loc.selector.line} at column #{node.loc.selector.column+1} : String Expected but got: #{node.inspect}")
74
- end
75
- return node.children[0]
84
+ def zlocalize_const?(node)
85
+ node.is_a?(Prism::ConstantReadNode) && node.name.to_s == 'ZLocalize'
76
86
  end
77
87
 
78
- def get_string_array_node_value(node)
79
- unless node.is_a?(AST::Node) || node.type != :array
80
- raise ArgumentError.new("On line #{node.loc.selector.line} at column #{node.loc.selector.column+1} : Array expected but got: #{node.inspect}")
81
- end
82
- a = []
83
- for i in 0..node.children.size - 1
84
- a << get_string_node_value(node.children[i])
88
+ def get_string_node_value(node)
89
+ unless string_node?(node)
90
+ raise ArgumentError.new("On line #{node.location.start_line} at column #{node.location.start_column+1} : String Expected but got: #{node.inspect}")
85
91
  end
86
- a
92
+ return node.unescaped
87
93
  end
88
94
 
89
- def process_translate_call(node)
90
- @translate_calls << { name: node.children[1].to_s,
91
- line_no: node.loc.selector.line,
92
- char_no: node.loc.selector.column+1,
93
- parameter: get_string_node_value(node.children[2]) }
94
- for i in 3..node.children.size-1
95
- process(node.children[i])
95
+ def get_string_array_node_value(node)
96
+ unless node.is_a?(Prism::ArrayNode)
97
+ raise ArgumentError.new("On line #{node.location.start_line} at column #{node.location.start_column+1} : Array expected but got: #{node.inspect}")
96
98
  end
99
+ node.elements.map { |element| get_string_node_value(element) }
97
100
  end
98
101
 
99
- def process_pluralize_call(node)
100
- @translate_calls << { name: node.children[1].to_s,
101
- line_no: node.loc.selector.line,
102
- char_no: node.loc.selector.column+1,
103
- parameter: get_string_array_node_value(node.children[2]) }
104
- for i in 3..node.children.size-1
105
- process(node.children[i])
106
- end
102
+ def string_node?(node)
103
+ node.is_a?(Prism::StringNode)
107
104
  end
108
105
 
109
- def make_translation_entry(h)
110
- TranslationEntry.new('plural' => h[:name] == 'n_' || h[:name] == 'pluralize',
111
- 'source' => h[:parameter],
112
- 'references' => [ "#{@relative_filename}:#{h[:line_no]}" ])
106
+ def format_parse_errors(parse_result)
107
+ parse_result.errors.map do |error|
108
+ "line #{error.location.start_line}: #{error.message}"
109
+ end.join("\n")
113
110
  end
114
111
 
115
112
  # return a Hash of all translation entries we collected
@@ -126,6 +123,12 @@ module ZLocalize
126
123
  entries
127
124
  end
128
125
 
126
+ def make_translation_entry(h)
127
+ TranslationEntry.new('plural' => h[:name] == 'n_' || h[:name] == 'pluralize',
128
+ 'source' => h[:parameter],
129
+ 'references' => [ "#{@relative_filename}:#{h[:line_no]}" ])
130
+ end
131
+
129
132
  end
130
133
 
131
134
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zlocalize
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.1.1
4
+ version: 7.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Charles Bedard
8
8
  - Stephane Volet
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-03-06 00:00:00.000000000 Z
11
+ date: 1980-01-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -16,42 +16,42 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '5.2'
19
+ version: '7.1'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '5.2'
26
+ version: '7.1'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: activesupport
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '5.2'
33
+ version: '7.1'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '5.2'
40
+ version: '7.1'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: actionpack
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: '5.2'
47
+ version: '7.1'
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
- version: '5.2'
54
+ version: '7.1'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: i18n
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -73,19 +73,19 @@ dependencies:
73
73
  - !ruby/object:Gem::Version
74
74
  version: '2'
75
75
  - !ruby/object:Gem::Dependency
76
- name: parser
76
+ name: prism
77
77
  requirement: !ruby/object:Gem::Requirement
78
78
  requirements:
79
79
  - - ">="
80
80
  - !ruby/object:Gem::Version
81
- version: '2.7'
81
+ version: '1.2'
82
82
  type: :runtime
83
83
  prerelease: false
84
84
  version_requirements: !ruby/object:Gem::Requirement
85
85
  requirements:
86
86
  - - ">="
87
87
  - !ruby/object:Gem::Version
88
- version: '2.7'
88
+ version: '1.2'
89
89
  email:
90
90
  - zzeligg@gmail.com
91
91
  - steph@zboing.ca
@@ -93,6 +93,8 @@ executables: []
93
93
  extensions: []
94
94
  extra_rdoc_files: []
95
95
  files:
96
+ - MIT-LICENSE
97
+ - README.md
96
98
  - lib/zlocalize.rb
97
99
  - lib/zlocalize/backend.rb
98
100
  - lib/zlocalize/config.rb
@@ -123,7 +125,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
123
125
  requirements:
124
126
  - - ">="
125
127
  - !ruby/object:Gem::Version
126
- version: 2.5.0
128
+ version: 3.4.0
127
129
  required_rubygems_version: !ruby/object:Gem::Requirement
128
130
  requirements:
129
131
  - - ">="