translation 1.19 → 1.32

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ff2388659bd454e1ff1cbcad7133161a66902007d08e4735b80c628289665c15
4
- data.tar.gz: d9a4222b6014f21dc2b29cc9de1afb60d725301d6d93a9cd7f0ba5b0cbc8d2e8
3
+ metadata.gz: 7a34c57ba0de5df8e9d6e302e24d1d959db4b2a697678d7cfc67e08b36702ef6
4
+ data.tar.gz: 5e024618fe204efef1a26294ec9e8bb6ab5fe357d9a32d826623a7ed4a5b508b
5
5
  SHA512:
6
- metadata.gz: 917f60afd4befcabb144943a3b9ebd26d2d4cd372ef286432d0bc0c03aafc9aff6bae7e4b956abcc11506f35be33e752cd011ff10f1b8a7b77e84038a0bf783f
7
- data.tar.gz: 8a9b12a2c66a7f060f2861150ad171ab4ebeb94b87fb18bc4763c2529871f8b58963c221cd9844844a3ef56dd9e4e32c262cf85b5df751a650f19e97e0de2a7e
6
+ metadata.gz: 59e48c919c27811c8d8ba6f1b29104d9adf2b661ce69a156acaea8ea669d137d87c9a07f0cb2e9e7f8175ca1930419d0ee6f1752fb4abb045fbfea9795185ea9
7
+ data.tar.gz: c2e556ccaf8f3bec9c833a51ba181c996e76f8f549cb1a180779ba287a9ac56ad992cf8b8fddb40bdb574cb63a3b6130145fe6a09a7d6f93af18dfbad43239af
data/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # [Translation.io](https://translation.io) client for Ruby on Rails
2
2
 
3
- [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md)
4
- [![Build Status](https://img.shields.io/travis/translation/rails/master.svg?style=flat-square)](https://travis-ci.org/translation/rails)
5
- [![Test Coverage](https://codeclimate.com/github/translation/rails/badges/coverage.svg)](https://codeclimate.com/github/translation/rails/coverage)
6
- [![Gem Version](https://badge.fury.io/rb/translation.svg)](https://badge.fury.io/rb/translation)
3
+ [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE)
4
+ [![Build Status](https://github.com/translation/rails/actions/workflows/test.yml/badge.svg?branch=master)](https://github.com/translation/rails/actions/workflows/test.yml)
5
+ [![Test Coverage](https://codeclimate.com/github/translation/rails/badges/coverage.svg)](https://codeclimate.com/github/translation/rails/test_coverage)
6
+ [![Gem Version](https://badgen.net/rubygems/v/translation)](https://rubygems.org/gems/translation)
7
7
  [![Downloads](https://img.shields.io/gem/dt/translation.svg)](https://rubygems.org/gems/translation)
8
8
 
9
- Add this gem to localize your Ruby on Rails application.
9
+ Add this gem to localize your **Ruby on Rails** application.
10
10
 
11
11
  Use the official Rails syntax (with [YAML](#i18n-yaml) files) or use the [GetText](#gettext) syntax.
12
12
 
@@ -35,25 +35,30 @@ Table of contents
35
35
  * [Add or Remove Language](#add-or-remove-language)
36
36
  * [Edit Language](#edit-language)
37
37
  * [Custom Languages](#custom-languages)
38
+ * [Fallbacks](#fallbacks)
38
39
  * [Change the current locale](#change-the-current-locale)
39
40
  * [Globally](#globally)
40
41
  * [Locally](#locally)
42
+ * [Frontend Localization](#frontend-localization)
43
+ * [Using this Gem](#using-this-gem)
44
+ * [Using our official React & JavaScript package](#using-our-official-react--javascript-package)
41
45
  * [Continuous Integration](#continuous-integration)
42
46
  * [Advanced Configuration Options](#advanced-configuration-options)
43
47
  * [Disable GetText or YAML](#disable-gettext-or-yaml)
44
48
  * [Ignored YAML keys](#ignored-yaml-keys)
49
+ * [Custom localization key prefixes](#custom-localization-key-prefixes)
45
50
  * [Source file formats (for GetText)](#source-file-formats-for-gettext)
46
51
  * [Gems with GetText strings](#gems-with-gettext-strings)
47
- * [Custom localization key prefixes](#custom-localization-key-prefixes)
48
52
  * [Paths where locales are stored (not recommended)](#paths-where-locales-are-stored-not-recommended)
49
53
  * [GetText Object Class Monkey-Patching](#gettext-object-class-monkey-patching)
50
54
  * [Pure Ruby (without Rails)](#pure-ruby-without-rails)
55
+ * [Limitations](#limitations)
51
56
  * [Testing](#testing)
52
57
  * [Contributing](#contributing)
53
58
  * [List of clients for Translation.io](#list-of-clients-for-translationio)
54
59
  * [Ruby on Rails (Ruby)](#ruby-on-rails-ruby)
55
60
  * [Laravel (PHP)](#laravel-php)
56
- * [React and React-Intl (JavaScript)](#react-and-react-intl-javascript)
61
+ * [React, React Native and JavaScript](#react-react-native-and-javascript)
57
62
  * [Others](#others)
58
63
  * [License](#license)
59
64
 
@@ -94,7 +99,7 @@ You can keep your source YAML file automatically updated using [i18n-tasks](http
94
99
  This gem adds the GetText support to Rails. We [strongly suggest](https://translation.io/blog/gettext-is-better-than-rails-i18n)
95
100
  that you use GetText to translate your application since it allows an easier and more complete syntax.
96
101
 
97
- Also, you won't need to create and manage any YAML file since your code will be
102
+ Moreover, you won't need to create and manage any YAML file since your code will be
98
103
  automatically scanned for any string to translate.
99
104
 
100
105
  ```ruby
@@ -143,7 +148,7 @@ end
143
148
  $ bundle exec rake translation:init
144
149
  ```
145
150
 
146
- If you later need to add/remove target languages, please read our
151
+ If you need to add or remove languages in the future, please read our
147
152
  [documentation](https://translation.io/blog/adding-target-languages) about that.
148
153
 
149
154
  ## Usage
@@ -176,7 +181,8 @@ $ bundle exec rake translation:sync_and_purge
176
181
 
177
182
  As the name says, this operation will also perform a sync at the same time.
178
183
 
179
- Warning: all keys that are not present in the current branch will be **permanently deleted from Translation.io**.
184
+ Warning: all keys that are not present in the current local branch
185
+ will be **permanently deleted from Translation.io**.
180
186
 
181
187
  ## Manage Languages
182
188
 
@@ -204,24 +210,39 @@ Since you created a new project, the translation history and tags will unfortuna
204
210
 
205
211
  ### Custom Languages
206
212
 
207
- You may want to add a custom language that is derived from an existing language.
208
- It's useful if you want to change some translations for another instance of the
209
- application or for a specific customer.
213
+ Custom languages are convenient if you want to customize translations for a specific customer
214
+ or another instance of your application.
215
+
216
+ A custom language is always be derived from an [existing language](https://translation.io/docs/languages).
217
+ Its structure should be like:
218
+
219
+ ```ruby
220
+ "#{existing_language_code}-#{custom_text}"
221
+ ```
210
222
 
211
- The structure of a custom language is : existing language code + "-" + custom text.
223
+ where `custom_text` can only contain alphabetic characters and `-`.
212
224
 
213
225
  Examples: `en-microsoft` or `fr-BE-custom`.
214
226
 
215
- Custom languages can be added like any other language and fallbacks work as expected.
216
- It means that if the `en-microsoft.some_key` is missing, then it will fallback to
217
- `en.some_key`. So you only need to translate keys that should be customized.
227
+ ### Fallbacks
218
228
 
219
- Note that fallback are chained, so `fr-be-custom` will fallback to `fr-be` that will
220
- itself fallback to `fr`.
229
+ If a translation is missing for a regional (`fr-BE`) or custom (`fr-microsoft`)
230
+ language, then it will fallback to the main language (`fr`).
221
231
 
222
- Using GetText syntax, it will only fallback to the source language. So either you
223
- create a fallback mechanism by yourself or you avoid fallbacking
224
- by translating everything in Translation.io for that custom language.
232
+ Locale fallbacks will work as expected with both [I18n (YAML)](#i18n-yaml)
233
+ and [GetText](#gettext) syntaxes.
234
+
235
+ A good way to leverage this feature is to ignore sentences from a regional language
236
+ that would have the same translation as the main language (usually most of them).
237
+ It's way easier to maintain the project over time if only 10% of the regional sentences
238
+ need to be adapted.
239
+
240
+ Note that fallbacks are chained, so `fr-BE-custom` will fallback to `fr-BE` that will
241
+ fallback to `fr`.
242
+
243
+ Just make sure to add `config.i18n.fallbacks = true` to your `config/application.rb` file.
244
+ You can find more information about this
245
+ [here](https://guides.rubyonrails.org/configuring.html#configuring-i18n).
225
246
 
226
247
  ## Change the current locale
227
248
 
@@ -237,9 +258,12 @@ class ApplicationController < ActionController::Base
237
258
  end
238
259
  ```
239
260
 
240
- It will automatically set the locale extracted from the user's browser `HTTP_ACCEPT_LANGUAGE` value, and keep it in the session between requests.
261
+ First time the user will connect, it will automatically set the locale extracted
262
+ from the user's browser `HTTP_ACCEPT_LANGUAGE` value, and keep it in the session between
263
+ requests.
241
264
 
242
- Update the current locale by redirecting the user to https://yourdomain.com?locale=fr or even https://yourdomain.com/fr if you scoped your routes like this:
265
+ Update the current locale by redirecting the user to https://yourdomain.com?locale=fr
266
+ or even https://yourdomain.com/fr if you scoped your routes like this:
243
267
 
244
268
  ```ruby
245
269
  scope "/:locale", :constraints => { locale: /[a-z]{2}/ } do
@@ -247,9 +271,11 @@ scope "/:locale", :constraints => { locale: /[a-z]{2}/ } do
247
271
  end
248
272
  ```
249
273
 
250
- The `set_locale` code is [here](https://github.com/translation/rails/blob/master/lib/translation_io/controller.rb#L3), feel free to override it with your own locale management.
274
+ The `set_locale` code is [here](https://github.com/translation/rails/blob/master/lib/translation_io/controller.rb#L3),
275
+ feel free to override it with your own locale management.
251
276
 
252
- Don't forget to define your available locales with [I18n.available_locales](http://guides.rubyonrails.org/i18n.html#setup-the-rails-application-for-internationalization).
277
+ Don't forget to define your available locales with
278
+ [I18n.available_locales](http://guides.rubyonrails.org/i18n.html#setup-the-rails-application-for-internationalization).
253
279
 
254
280
  More examples here: https://translation.io/blog/set-current-locale-in-your-rails-app
255
281
 
@@ -265,17 +291,91 @@ You can call it several times in the same page if you want to switch between lan
265
291
 
266
292
  More examples here: https://translation.io/blog/rails-i18n-with-locale
267
293
 
294
+ ## Frontend Localization
295
+
296
+ ### Using this Gem
297
+
298
+ This gem is also able to cover frontend localization (React, Vue, ...).
299
+
300
+ There are several ways to pass the translation strings from the backend
301
+ to the frontend: JavaScript serialization, `data-` HTML attributes, JSON files etc.
302
+
303
+ The easiest strategy when dealing with React/Vue would be to pass the corresponding
304
+ translations as props when mounting the components.
305
+
306
+ Assuming that you use [reactjs/react-rails](https://github.com/reactjs/react-rails),
307
+ it would look like this if you want to use [I18n (YAML)](#i18n-yaml) syntax:
308
+
309
+ ```erb
310
+ <%=
311
+ react_component('MyComponent", {
312
+ :user_id => current_user.id,
313
+ :i18n => YAML.load_file("config/locales/#{I18n.locale}.yml")[I18n.locale.to_s]["my_component"]
314
+ })
315
+ %>
316
+ ```
317
+
318
+ Your `en.yml` should look like this:
319
+
320
+ ```yaml
321
+ en:
322
+ my_component:
323
+ your_name: Your name
324
+ title: Title
325
+ ```
326
+
327
+ You can also directly use the [GetText](#gettext) syntax:
328
+
329
+ ```erb
330
+ <%=
331
+ react_component('MyComponent", {
332
+ :user_id => current_user.id,
333
+ :i18n => {
334
+ :your_name => _('Your name'),
335
+ :title => _('Title')
336
+ }
337
+ })
338
+ %>
339
+ ```
340
+
341
+ In both case, in your React component, you can simply call
342
+ `this.props.i18n.yourName` and your text will be localized with the current locale.
343
+
344
+ **Notes:**
345
+
346
+ * You can also structure the i18n props with multiple levels of depth and pass the subtree as props to each of your sub-components.
347
+ * It also works great with server-side rendering of your components (`:prerender => true`).
348
+
349
+ ### Using our official React & JavaScript package
350
+
351
+ As Translation.io is directly integrated in the great
352
+ [Lingui](https://lingui.js.org/) internationalization framework,
353
+ you can also consider frontend localization as a completely different
354
+ localization project.
355
+
356
+ Please read more about this on:
357
+
358
+ * Website: [https://translation.io/lingui](https://translation.io/lingui)
359
+ * GitHub page: [https://github.com/translation/lingui](https://github.com/translation/lingui)
360
+
268
361
  ## Continuous Integration
269
362
 
270
- If you want fresh translations in your Continuous Integration workflow, you may find yourself calling `bundle exec rake translation:sync` very frequently.
363
+ If you want fresh translations in your Continuous Integration workflow, you may
364
+ find yourself calling `bundle exec rake translation:sync` very frequently.
271
365
 
272
- Since this task can't be concurrently executed (we have a [mutex](https://en.wikipedia.org/wiki/Mutual_exclusion) strategy with a queue but it returns an error under heavy load), we implemented this threadsafe readonly task:
366
+ Since this task can't be concurrently executed
367
+ (we have a [mutex](https://en.wikipedia.org/wiki/Mutual_exclusion) strategy with
368
+ a queue but it returns an error under heavy load), we implemented this
369
+ threadsafe readonly task:
273
370
 
274
371
  ```bash
275
372
  $ bundle exec rake translation:sync_readonly
276
373
  ```
277
374
 
278
- This task will prevent your CI to fail and still provide new translations. But be aware that it won't send new keys from your code to Translation.io so you still need to call `bundle exec rake translation:sync` at some point during development.
375
+ This task will prevent your CI to fail and still provide new translations. But
376
+ be aware that it won't send new keys from your code to Translation.io so you
377
+ still need to call `bundle exec rake translation:sync` at some point during
378
+ development.
279
379
 
280
380
  ## Advanced Configuration Options
281
381
 
@@ -330,6 +430,32 @@ TranslationIO.configure do |config|
330
430
  end
331
431
  ```
332
432
 
433
+ ### Custom localization key prefixes
434
+
435
+ Rails YAML files contain not only translation strings but also localization values (integers, arrays, booleans)
436
+ in the same place and that's bad. For example: date formats, number separators, default
437
+ currency or measure units, etc.
438
+
439
+ A translator is supposed to translate, not localize. That's not his role to choose how you want your dates or
440
+ numbers to be displayed, right? Moreover, this special keys often contain special constructions (e.g.,
441
+ with percent signs or spaces) that he might break.
442
+
443
+ We think localization is part of the configuration of the app and it should not reach the translator UI at all.
444
+ That's why these localization keys are detected and separated on a dedicated YAML file with Translation.io.
445
+
446
+ We automatically treat [known localization keys](lib/translation_io/yaml_entry.rb), but if you would like
447
+ to add some more, use the `localization_key_prefixes` option.
448
+
449
+ For example:
450
+
451
+ ```ruby
452
+ TranslationIO.configure do |config|
453
+ ...
454
+ config.localization_key_prefixes = ['my_gem.date.formats']
455
+ ...
456
+ end
457
+ ```
458
+
333
459
  ### Source file formats (for GetText)
334
460
 
335
461
  If you are using GetText and you want to manage other file formats than:
@@ -365,32 +491,6 @@ TranslationIO.configure do |config|
365
491
  end
366
492
  ```
367
493
 
368
- ### Custom localization key prefixes
369
-
370
- Rails YAML files contain not only translation strings but also localization values (integers, arrays, booleans)
371
- in the same place and that's bad. For example: date formats, number separators, default
372
- currency or measure units, etc.
373
-
374
- A translator is supposed to translate, not localize. That's not his role to choose how you want your dates or
375
- numbers to be displayed, right? Moreover, this special keys often contain special constructions (e.g.,
376
- with percent signs or spaces) that he might break.
377
-
378
- We think localization is part of the configuration of the app and it should not reach the translator UI at all.
379
- That's why these localization keys are detected and separated on a dedicated YAML file with Translation.io.
380
-
381
- We automatically treat [known localization keys](lib/translation_io/yaml_entry.rb), but if you would like
382
- to add some more, use the `localization_key_prefixes` option.
383
-
384
- For example:
385
-
386
- ```ruby
387
- TranslationIO.configure do |config|
388
- ...
389
- config.localization_key_prefixes = ['my_gem.date.formats']
390
- ...
391
- end
392
- ```
393
-
394
494
  ### Paths where locales are stored (not recommended)
395
495
 
396
496
  You can specify where your GetText and YAML files are on disk:
@@ -483,6 +583,33 @@ This gem was created specifically for Rails, but you can also use it in a pure R
483
583
 
484
584
  (Thanks [@kubaw](https://github.com/kubaw) for this snippet!)
485
585
 
586
+ ## Limitations
587
+
588
+ If you localize `.erb` files with the [GetText syntax](#gettext), please avoid
589
+ the use of `case` and `when` that are not correctly parsed by ERB.
590
+
591
+ This syntax will break and your file will be ignored:
592
+
593
+ ```erb
594
+ <% case @state %>
595
+ <% when 'received' %>
596
+ ```
597
+
598
+ Instead, use `if`/`elsif` or this `case` syntax:
599
+
600
+ ```erb
601
+ <%
602
+ case @state
603
+ when 'received'
604
+ %>
605
+ ```
606
+
607
+ These are the related discussions: [ruby/erb#4](https://github.com/ruby/erb/issues/4) and [translation/rails#44](https://github.com/translation/rails/issues/44).
608
+
609
+ There is currently an open PR ([ruby-gettext/gettext#91](https://github.com/ruby-gettext/gettext/pull/91)), switching ERB for [Erubi](https://github.com/jeremyevans/erubi),
610
+ that is waiting to be merged into GetText, but it may have unknown undesirable
611
+ side effects.
612
+
486
613
  ## Testing
487
614
 
488
615
  To run the specs:
@@ -521,12 +648,15 @@ Officially Supported on [https://translation.io/laravel](https://translation.io/
521
648
 
522
649
  Credits: [@armandsar](https://github.com/armandsar), [@michaelhoste](https://github.com/michaelhoste)
523
650
 
524
- ### React and React-Intl (JavaScript)
651
+ ### React, React Native and JavaScript
652
+
653
+ Officially Supported on [https://translation.io/lingui](https://translation.io/lingui)
525
654
 
526
- * GitHub: https://github.com/deecewan/translation-io
527
- * NPM: https://www.npmjs.com/package/translation-io
655
+ Translation.io is directly integrated in the great
656
+ [Lingui](https://lingui.js.org/) internationalization project.
528
657
 
529
- Credits: [@deecewan](https://github.com/deecewan)
658
+ * GitHub: https://github.com/translation/lingui
659
+ * NPM: https://www.npmjs.com/package/@translation/lingui
530
660
 
531
661
  ### Others
532
662
 
data/lib/translation.rb CHANGED
@@ -33,13 +33,16 @@ module TranslationIO
33
33
 
34
34
  if !@config.disable_gettext
35
35
  require_gettext_dependencies
36
- add_missing_locales
37
36
  add_parser_for_erb_source_formats(@config.erb_source_formats)
38
37
 
39
38
  if Rails.env.development?
40
39
  GetText::TextDomainManager.cached = false
41
40
  end
42
41
 
42
+ # Set default GetText locale (last fallback) as config.source_locale instead of "en" (default)
43
+ gettext_locale = @config.source_locale.to_s.gsub('-', '_').to_sym
44
+ Locale.set_default(gettext_locale)
45
+
43
46
  # include is private until Ruby 2.1
44
47
  Proxy.send(:include, GetText)
45
48
 
@@ -69,19 +72,20 @@ module TranslationIO
69
72
  require 'gettext/tools'
70
73
  require 'gettext/text_domain_manager'
71
74
  require 'gettext/tools/xgettext'
72
- end
73
-
74
- # Missing languages from Locale that are in Translation.io
75
- def add_missing_locales
76
- Locale::Info.three_languages['wee'] = Locale::Info::Language.new('', 'wee', 'I', 'L', 'Lower Sorbian')
77
- Locale::Info.three_languages['wen'] = Locale::Info::Language.new('', 'wen', 'I', 'L', 'Upper Sorbian')
75
+ require "gettext/tools/parser/erubi" if Gem::Version.new(GetText::VERSION) >= Gem::Version.new('3.4.3')
78
76
  end
79
77
 
80
78
  def add_parser_for_erb_source_formats(new_erb_formats)
81
- existing_extensions = GetText::ErbParser.instance_variable_get("@config")[:extnames]
82
79
  new_extensions = new_erb_formats.collect { |ext| ".#{ext}" }
83
80
 
81
+ existing_extensions = GetText::ErbParser.instance_variable_get("@config")[:extnames]
84
82
  GetText::ErbParser.instance_variable_get("@config")[:extnames] = (existing_extensions + new_extensions).uniq
83
+
84
+ # for gettext >= 3.4.3 (erubi compatibility)
85
+ if defined?(GetText::ErubiParser)
86
+ existing_extensions = GetText::ErubiParser.instance_variable_get("@config")[:extnames]
87
+ GetText::ErubiParser.instance_variable_get("@config")[:extnames] = (existing_extensions + new_extensions).uniq
88
+ end
85
89
  end
86
90
 
87
91
  def info(message, level = 0, verbose_level = 0)
@@ -96,6 +100,15 @@ module TranslationIO
96
100
  File.expand_path(relative_or_absolute_path).gsub("#{Dir.pwd}/", '')
97
101
  end
98
102
 
103
+ # Cf. https://github.com/translation/rails/issues/47
104
+ def yaml_load(source)
105
+ begin
106
+ YAML.load(source, :aliases => true) || {}
107
+ rescue ArgumentError
108
+ YAML.load(source) || {}
109
+ end
110
+ end
111
+
99
112
  def version
100
113
  Gem::Specification::find_by_name('translation').version.to_s
101
114
  end
@@ -35,13 +35,15 @@ module TranslationIO
35
35
  YamlEntry.from_locale?(key, target_locale) && !YamlEntry.ignored?(key)
36
36
  end
37
37
 
38
- yaml_data = YAMLConversion.get_yaml_data_from_flat_translations(target_flat_special_translations)
38
+ yaml_data = YAMLConversion.get_yaml_data_from_flat_translations(target_flat_special_translations, **{
39
+ :force_keep_empty_keys => true # We want to keep empty keys from localization.xx.yml files (sometimes needed for delimiters!)
40
+ })
39
41
 
40
42
  params["yaml_data_#{target_locale}"] = yaml_data
41
43
 
42
44
  # To have a localization.xx.yml file during tests (without call to backend)
43
45
  if TranslationIO.config.test
44
- if YAML::load(yaml_data).present?
46
+ if TranslationIO.yaml_load(yaml_data).present?
45
47
  File.open(yaml_path, 'wb') do |file|
46
48
  file.write(self.class.top_comment)
47
49
  file.write(yaml_data)
@@ -62,7 +64,7 @@ module TranslationIO
62
64
  yaml_path = File.join(@yaml_locales_path, "localization.#{target_locale}.yml")
63
65
  yaml_data = parsed_response["yaml_data_#{target_locale}"]
64
66
 
65
- if yaml_data.present? && YAML::load(yaml_data).present?
67
+ if yaml_data.present? && TranslationIO.yaml_load(yaml_data).present?
66
68
  File.open(yaml_path, 'wb') do |file|
67
69
  file.write(self.class.top_comment)
68
70
  file.write(yaml_data)
@@ -16,12 +16,29 @@ module TranslationIO
16
16
  TranslationIO.info "Updating POT file."
17
17
 
18
18
  FileUtils.mkdir_p(File.dirname(@pot_path))
19
- GetText::Tools::XGetText.run(*@source_files, '-o', @pot_path,
20
- '--msgid-bugs-address', TranslationIO.config.pot_msgid_bugs_address,
21
- '--package-name', TranslationIO.config.pot_package_name,
22
- '--package-version', TranslationIO.config.pot_package_version,
23
- '--copyright-holder', TranslationIO.config.pot_copyright_holder,
24
- '--copyright-year', TranslationIO.config.pot_copyright_year.to_s)
19
+
20
+ # for gettext >= 3.4.3 (erubi compatibility)
21
+ if defined?(GetText::ErubiParser)
22
+ GetText::Tools::XGetText.run(
23
+ *@source_files, '-o', @pot_path,
24
+ '--msgid-bugs-address', TranslationIO.config.pot_msgid_bugs_address,
25
+ '--package-name', TranslationIO.config.pot_package_name,
26
+ '--package-version', TranslationIO.config.pot_package_version,
27
+ '--copyright-holder', TranslationIO.config.pot_copyright_holder,
28
+ '--copyright-year', TranslationIO.config.pot_copyright_year.to_s,
29
+ '--require', 'gettext/tools/parser/erubi', # Cf. (1) https://github.com/ruby-gettext/gettext/pull/91
30
+ '--parser', 'GetText::ErubiParser' # (2) https://github.com/ruby-gettext/gettext/commit/0eb06a88323a5cc16065680ffe228d978fb87c88
31
+ )
32
+ else
33
+ GetText::Tools::XGetText.run(
34
+ *@source_files, '-o', @pot_path,
35
+ '--msgid-bugs-address', TranslationIO.config.pot_msgid_bugs_address,
36
+ '--package-name', TranslationIO.config.pot_package_name,
37
+ '--package-version', TranslationIO.config.pot_package_version,
38
+ '--copyright-holder', TranslationIO.config.pot_copyright_holder,
39
+ '--copyright-year', TranslationIO.config.pot_copyright_year.to_s
40
+ )
41
+ end
25
42
 
26
43
  FileUtils.rm_f(@tmp_empty_file) if @tmp_empty_file.present?
27
44
 
@@ -17,6 +17,38 @@ module TranslationIO
17
17
 
18
18
  private
19
19
 
20
+ def warn_wrong_locales(source_locale, target_locales)
21
+ if target_locales.uniq != target_locales
22
+ duplicate_locale = target_locales.detect { |locale| target_locales.count(locale) > 1 }
23
+
24
+ puts
25
+ puts "----------"
26
+ puts "Your `config.target_locales` has a duplicate locale (#{duplicate_locale})."
27
+ puts "Please clean your configuration file and execute this command again."
28
+ puts "----------"
29
+ exit(true)
30
+ end
31
+
32
+ if target_locales.include?(source_locale)
33
+ puts
34
+ puts "----------"
35
+ puts "The `config.source_locale` (#{source_locale}) can't be included in the `config.target_locales`."
36
+ puts "If you want to customize your source locale, check this link: https://github.com/translation/rails#custom-languages"
37
+ puts "Please clean your configuration file and execute this command again."
38
+ puts "----------"
39
+ exit(true)
40
+ end
41
+
42
+ if target_locales.empty?
43
+ puts
44
+ puts "----------"
45
+ puts "Your `config.target_locales` is empty."
46
+ puts "Please clean your configuration file and execute this command again."
47
+ puts "----------"
48
+ exit(true)
49
+ end
50
+ end
51
+
20
52
  def self.perform_request(uri, params)
21
53
  begin
22
54
  params.merge!({
@@ -13,7 +13,7 @@ module TranslationIO
13
13
  @yaml_file_paths.each do |locale_file_path|
14
14
  if locale_file_removable?(locale_file_path)
15
15
  if File.exist?(locale_file_path)
16
- content_hash = YAML::load(File.read(locale_file_path)) || {}
16
+ content_hash = TranslationIO.yaml_load(File.read(locale_file_path)) || {}
17
17
  source_content_hash = content_hash.select { |k| k.to_s == @source_locale.to_s }
18
18
 
19
19
  if source_content_hash.empty?
@@ -18,7 +18,7 @@ module TranslationIO
18
18
 
19
19
  @yaml_file_paths.each do |file_path|
20
20
  TranslationIO.info file_path, 2, 2
21
- file_translations = YAML::load(File.read(file_path))
21
+ file_translations = TranslationIO.yaml_load(File.read(file_path))
22
22
 
23
23
  unless file_translations.blank?
24
24
  all_translations = all_translations.deep_merge(file_translations)
@@ -13,12 +13,14 @@ module TranslationIO
13
13
  haml_source_files = config.haml_source_files
14
14
  slim_source_files = config.slim_source_files
15
15
  pot_path = config.pot_path
16
- source_locale = config.source_locale
17
- target_locales = config.target_locales
16
+ source_locale = config.source_locale.to_s
17
+ target_locales = config.target_locales.map(&:to_s)
18
18
  locales_path = config.locales_path
19
19
  yaml_locales_path = config.yaml_locales_path
20
20
  yaml_file_paths = config.yaml_file_paths
21
21
 
22
+ warn_wrong_locales(source_locale, target_locales)
23
+
22
24
  if !config.disable_gettext
23
25
  BaseOperation::DumpMarkupGettextKeysStep.new(haml_source_files, :haml).run
24
26
  BaseOperation::DumpMarkupGettextKeysStep.new(slim_source_files, :slim).run
@@ -12,72 +12,113 @@ module TranslationIO
12
12
 
13
13
  params.merge!({ :timestamp => metadata_timestamp })
14
14
  parsed_response = perform_source_edits_request(params)
15
+ source_edits = parsed_response['source_edits'].to_a
15
16
 
16
- unless parsed_response.nil?
17
- TranslationIO.info "Applying YAML source editions."
17
+ TranslationIO.info "Applying YAML source editions."
18
18
 
19
- parsed_response['source_edits'].each do |source_edit|
20
- inserted = false
19
+ source_edits.each do |source_edit|
20
+ applied = false
21
21
 
22
- sort_by_project_locales_first(@yaml_file_paths).each do |file_path|
23
- yaml_hash = YAML::load(File.read(file_path))
24
- flat_yaml_hash = FlatHash.to_flat_hash(yaml_hash)
22
+ reload_or_reuse_yaml_sources
25
23
 
26
- flat_yaml_hash.each do |key, value|
27
- if key == "#{@source_locale}.#{source_edit['key']}"
28
- if value == source_edit['old_text']
29
- TranslationIO.info "#{source_edit['key']} | #{source_edit['old_text']} -> #{source_edit['new_text']}", 2, 2
24
+ @yaml_sources.to_a.each do |yaml_source|
25
+ yaml_file_path = yaml_source[:yaml_file_path]
26
+ yaml_flat_hash = yaml_source[:yaml_flat_hash]
30
27
 
31
- if locale_file_path_in_project?(file_path)
32
- flat_yaml_hash[key] = source_edit['new_text']
28
+ yaml_flat_hash.each do |full_key, value|
29
+ if full_key == "#{@source_locale}.#{source_edit['key']}"
30
+ apply_source_edit(source_edit, yaml_file_path, yaml_flat_hash)
31
+ applied = true
32
+ break
33
+ end
34
+ end
33
35
 
34
- file_content = to_hash_to_yaml(flat_yaml_hash)
36
+ break if applied
37
+ end
38
+ end
35
39
 
36
- File.open(file_path, 'w') do |f|
37
- f.write(file_content)
38
- end
39
- else # override source text of gem
40
- yaml_path = File.join(TranslationIO.config.yaml_locales_path, "#{@source_locale}.yml")
40
+ update_metadata_timestamp
41
+ end
41
42
 
42
- if File.exists?(yaml_path) # source yaml file
43
- yaml_hash = YAML::load(File.read(yaml_path))
44
- flat_yaml_hash = FlatHash.to_flat_hash(yaml_hash)
45
- else
46
- FileUtils::mkdir_p File.dirname(yaml_path)
47
- flat_yaml_hash = {}
48
- end
43
+ private
49
44
 
50
- flat_yaml_hash["#{@source_locale}.#{source_edit['key']}"] = source_edit['new_text']
45
+ def reload_or_reuse_yaml_sources
46
+ if yaml_sources_reload_needed?
47
+ @yaml_sources = sort_by_project_locales_first(@yaml_file_paths).collect do |yaml_file_path|
48
+ yaml_content = File.read(yaml_file_path)
49
+ yaml_hash = TranslationIO.yaml_load(yaml_content)
50
+ yaml_flat_hash = FlatHash.to_flat_hash(yaml_hash)
51
+
52
+ {
53
+ :yaml_file_path => yaml_file_path,
54
+ :yaml_flat_hash => yaml_flat_hash
55
+ }
56
+ end
57
+ else
58
+ @yaml_source
59
+ end
60
+ end
51
61
 
52
- file_content = to_hash_to_yaml(flat_yaml_hash)
62
+ def yaml_sources_reload_needed?
63
+ @yaml_file_paths.sort != @yaml_sources.to_a.collect { |y_s| y_s[:yaml_file_path] }.sort
64
+ end
65
+
66
+ # Sort YAML file paths by project locales first, gem locales after
67
+ # (to replace "overridden" source first)
68
+ def sort_by_project_locales_first(yaml_file_paths)
69
+ yaml_file_paths.sort do |x, y|
70
+ a = locale_file_path_in_project?(x)
71
+ b = locale_file_path_in_project?(y)
72
+ (!a && b) ? 1 : ((a && !b) ? -1 : 0)
73
+ end
74
+ end
53
75
 
54
- File.open(yaml_path, 'w') do |f|
55
- f.write(file_content)
56
- end
57
- end
76
+ def apply_source_edit(source_edit, yaml_file_path, yaml_flat_hash)
77
+ full_key = "#{@source_locale}.#{source_edit['key']}"
58
78
 
59
- inserted = true
60
- break
61
- else
62
- TranslationIO.info "#{source_edit['key']} | Ignored because translation was also updated in source YAML file", 2, 2
63
- end
64
- end
65
- end
79
+ if yaml_flat_hash[full_key] == source_edit['old_text']
80
+ TranslationIO.info "#{source_edit['key']} | #{source_edit['old_text']} -> #{source_edit['new_text']}", 2, 2
66
81
 
67
- break if inserted
68
- end
82
+ if locale_file_path_in_project?(yaml_file_path)
83
+ apply_application_source_edit(source_edit, yaml_file_path, yaml_flat_hash)
84
+ else # Override source text of gem inside the app
85
+ apply_gem_source_edit(source_edit)
69
86
  end
87
+ else
88
+ TranslationIO.info "#{source_edit['key']} | #{source_edit['old_text']} -> #{source_edit['new_text']} | Ignored because translation was also updated in source YAML file", 2, 2
70
89
  end
90
+ end
71
91
 
72
- File.open(TranslationIO.config.metadata_path, 'w') do |f|
73
- f.write({ 'timestamp' => Time.now.utc.to_i }.to_yaml)
92
+ def apply_application_source_edit(source_edit, yaml_file_path, yaml_flat_hash)
93
+ full_key = "#{@source_locale}.#{source_edit['key']}"
94
+ yaml_flat_hash[full_key] = source_edit['new_text']
95
+ file_content = to_hash_to_yaml(yaml_flat_hash)
96
+
97
+ File.open(yaml_file_path, 'w') do |f|
98
+ f.write(file_content)
74
99
  end
75
100
  end
76
101
 
77
- private
102
+ def apply_gem_source_edit(source_edit)
103
+ # Source yaml file like config/locales/en.yml
104
+ yaml_file_path = File.expand_path(File.join(TranslationIO.config.yaml_locales_path, "#{@source_locale}.yml"))
78
105
 
79
- def to_hash_to_yaml(flat_yaml_hash)
80
- yaml_hash = FlatHash.to_hash(flat_yaml_hash)
106
+ if File.exists?(yaml_file_path)
107
+ # Complete existing hash if YAML file already exists
108
+ existing_yaml_source = @yaml_sources.detect { |y_s| normalize_path(y_s[:yaml_file_path]) == normalize_path(yaml_file_path) }
109
+ yaml_flat_hash = existing_yaml_source[:yaml_flat_hash]
110
+ else
111
+ # Create new hash if YAML file doesn't exist yet
112
+ FileUtils::mkdir_p File.dirname(yaml_file_path)
113
+ yaml_flat_hash = {}
114
+ @yaml_file_paths.push(yaml_file_path) # Application YAML are at the end of the list
115
+ end
116
+
117
+ apply_application_source_edit(source_edit, yaml_file_path, yaml_flat_hash)
118
+ end
119
+
120
+ def to_hash_to_yaml(yaml_flat_hash)
121
+ yaml_hash = FlatHash.to_hash(yaml_flat_hash)
81
122
 
82
123
  if TranslationIO.config.yaml_line_width
83
124
  content = yaml_hash.to_yaml(:line_width => TranslationIO.config.yaml_line_width)
@@ -92,37 +133,45 @@ module TranslationIO
92
133
  if File.exist?(TranslationIO.config.metadata_path)
93
134
  metadata_content = File.read(TranslationIO.config.metadata_path)
94
135
 
136
+ # If any conflicts in file, take the lowest timestamp and potentially reapply some source edits
95
137
  if metadata_content.include?('>>>>') || metadata_content.include?('<<<<')
96
- TranslationIO.info "[Error] #{TranslationIO.config.metadata_path} file is corrupted and seems to have unresolved versioning conflicts. Please resolve them and try again."
97
- exit(false)
138
+ timestamps = metadata_content.scan(/timestamp: (\d*)/).flatten.uniq.collect(&:to_i)
139
+ return timestamps.min || 0
98
140
  else
99
- return YAML::load(metadata_content)['timestamp'] rescue 0
141
+ return YAML.load(metadata_content)['timestamp'] rescue 0
100
142
  end
101
143
  else
102
144
  return 0
103
145
  end
104
146
  end
105
147
 
148
+ def update_metadata_timestamp
149
+ File.open(TranslationIO.config.metadata_path, 'w') do |f|
150
+ f.puts '# This file is used in the context of Translation.io source editions.'
151
+ f.puts '# Please see: https://translation.io/blog/new-feature-copywriting'
152
+ f.puts '#'
153
+ f.puts '# If you have any git conflicts, either keep the smaller timestamp or'
154
+ f.puts '# ignore the conflicts and "sync" again, it will fix this file for you.'
155
+ f.puts
156
+
157
+ f.write({ 'timestamp' => Time.now.utc.to_i }.to_yaml)
158
+ end
159
+ end
160
+
106
161
  def perform_source_edits_request(params)
107
162
  uri = URI("#{TranslationIO.client.endpoint}/projects/#{TranslationIO.client.api_key}/source_edits")
108
163
  parsed_response = BaseOperation.perform_request(uri, params)
109
164
  end
110
165
 
111
- # Sort YAML file paths by project locales first, gem locales after
112
- # (to replace "overridden" source first)
113
- def sort_by_project_locales_first(yaml_file_paths)
114
- yaml_file_paths.sort do |x, y|
115
- a = locale_file_path_in_project?(x)
116
- b = locale_file_path_in_project?(y)
117
- (!a && b) ? 1 : ((a && !b) ? -1 : 0)
118
- end
119
- end
120
-
121
166
  def locale_file_path_in_project?(locale_file_path)
122
- TranslationIO.normalize_path(locale_file_path).start_with?(
123
- TranslationIO.normalize_path(TranslationIO.config.yaml_locales_path)
167
+ normalize_path(locale_file_path).start_with?(
168
+ normalize_path(TranslationIO.config.yaml_locales_path)
124
169
  )
125
170
  end
171
+
172
+ def normalize_path(path)
173
+ TranslationIO.normalize_path(path)
174
+ end
126
175
  end
127
176
  end
128
177
  end
@@ -14,7 +14,7 @@ module TranslationIO
14
14
 
15
15
  @yaml_file_paths.each do |file_path|
16
16
  TranslationIO.info file_path, 2, 2
17
- file_translations = YAML::load(File.read(file_path))
17
+ file_translations = TranslationIO.yaml_load(File.read(file_path))
18
18
 
19
19
  unless file_translations.blank?
20
20
  all_translations = all_translations.deep_merge(file_translations)
@@ -16,12 +16,14 @@ module TranslationIO
16
16
  haml_source_files = config.haml_source_files
17
17
  slim_source_files = config.slim_source_files
18
18
  pot_path = config.pot_path
19
- source_locale = config.source_locale
20
- target_locales = config.target_locales
19
+ source_locale = config.source_locale.to_s
20
+ target_locales = config.target_locales.map(&:to_s)
21
21
  locales_path = config.locales_path
22
22
  yaml_locales_path = config.yaml_locales_path
23
23
  yaml_file_paths = config.yaml_file_paths
24
24
 
25
+ warn_wrong_locales(source_locale, target_locales)
26
+
25
27
  if !config.disable_yaml
26
28
  ApplyYamlSourceEditsStep.new(yaml_file_paths, source_locale).run(params)
27
29
  end
@@ -14,6 +14,7 @@ module TranslationIO
14
14
  attr_accessor :ignored_key_prefixes
15
15
  attr_accessor :localization_key_prefixes
16
16
  attr_accessor :yaml_line_width
17
+ attr_accessor :yaml_remove_empty_keys
17
18
 
18
19
  attr_accessor :disable_gettext
19
20
 
@@ -75,6 +76,10 @@ module TranslationIO
75
76
  # Cf. https://github.com/translation/rails/issues/19
76
77
  self.yaml_line_width = nil
77
78
 
79
+ # Remove empty keys from translated YAML files
80
+ # Cf. https://github.com/translation/rails/pull/37
81
+ self.yaml_remove_empty_keys = false
82
+
78
83
  #######
79
84
  # GetText options
80
85
  #######
@@ -119,7 +124,7 @@ module TranslationIO
119
124
  end
120
125
 
121
126
  def yaml_file_paths
122
- I18n.load_path.select do |p|
127
+ I18n.load_path.collect(&:to_s).uniq.select do |p|
123
128
  File.exist?(p) && (File.extname(p) == '.yml' || File.extname(p) == '.yaml')
124
129
  end
125
130
  end
@@ -11,7 +11,7 @@ module TranslationIO
11
11
  hash = {}
12
12
 
13
13
  if remove_empty_keys
14
- flat_hash = flat_hash.reject { |k, v| v.nil? && !k.end_with?(']') }
14
+ flat_hash = flat_hash.reject { |k, v| v.blank? && !k.end_with?(']') }
15
15
  end
16
16
 
17
17
  flat_hash.each_pair do |key, value|
@@ -7,8 +7,10 @@ module TranslationIO
7
7
  require 'translation_io/tasks'
8
8
  end
9
9
 
10
- initializer 'translation.rails_extensions' do
11
- ActionController::Base.send(:include, TranslationIO::Controller)
10
+ initializer 'translation.controller_helper' do
11
+ ActiveSupport.on_load :action_controller do
12
+ ActionController::Base.send(:include, TranslationIO::Controller)
13
+ end
12
14
  end
13
15
 
14
16
  config.after_initialize do
@@ -26,7 +26,7 @@ module TranslationIO
26
26
  end
27
27
 
28
28
  def get_flat_translations_for_yaml_data(yaml_data)
29
- translations = YAML::load(yaml_data)
29
+ translations = TranslationIO.yaml_load(yaml_data)
30
30
 
31
31
  if translations
32
32
  return FlatHash.to_flat_hash(translations)
@@ -35,8 +35,14 @@ module TranslationIO
35
35
  end
36
36
  end
37
37
 
38
- def get_yaml_data_from_flat_translations(flat_translations)
39
- translations = FlatHash.to_hash(flat_translations)
38
+ def get_yaml_data_from_flat_translations(flat_translations, force_keep_empty_keys: false)
39
+ if force_keep_empty_keys
40
+ remove_empty_keys = false
41
+ else
42
+ remove_empty_keys = TranslationIO.config.yaml_remove_empty_keys
43
+ end
44
+
45
+ translations = FlatHash.to_hash(flat_translations, remove_empty_keys)
40
46
 
41
47
  if TranslationIO.config.yaml_line_width
42
48
  data = translations.to_yaml(:line_width => TranslationIO.config.yaml_line_width)
@@ -31,7 +31,7 @@ module TranslationIO
31
31
  end
32
32
 
33
33
  def ignored?(key)
34
- key.present? && ignored_key_prefixes.any? { |p| key_without_locale(key).match(/^#{p}\b/) != nil }
34
+ key.present? && ignored_key_prefixes.any? { |prefix| key_without_locale(key).match(/^#{Regexp.escape(prefix)}\b/) != nil }
35
35
  end
36
36
 
37
37
  def localization?(key, value)
@@ -40,7 +40,7 @@ module TranslationIO
40
40
 
41
41
  def localization_prefix?(key)
42
42
  localization_key_prefixes.any? do |prefix|
43
- key_without_locale(key).match(/^#{prefix}\b/) != nil
43
+ key_without_locale(key).match(/^#{Regexp.escape(prefix)}\b/) != nil
44
44
  end
45
45
  end
46
46
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: translation
3
3
  version: !ruby/object:Gem::Version
4
- version: '1.19'
4
+ version: '1.32'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Hoste
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2019-09-13 00:00:00.000000000 Z
12
+ date: 2022-05-20 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: gettext
@@ -21,6 +21,9 @@ dependencies:
21
21
  - - ">="
22
22
  - !ruby/object:Gem::Version
23
23
  version: 3.2.5
24
+ - - "<="
25
+ - !ruby/object:Gem::Version
26
+ version: 3.4.3
24
27
  type: :runtime
25
28
  prerelease: false
26
29
  version_requirements: !ruby/object:Gem::Requirement
@@ -31,6 +34,9 @@ dependencies:
31
34
  - - ">="
32
35
  - !ruby/object:Gem::Version
33
36
  version: 3.2.5
37
+ - - "<="
38
+ - !ruby/object:Gem::Version
39
+ version: 3.4.3
34
40
  - !ruby/object:Gem::Dependency
35
41
  name: rake
36
42
  requirement: !ruby/object:Gem::Requirement
@@ -65,14 +71,14 @@ dependencies:
65
71
  requirements:
66
72
  - - "~>"
67
73
  - !ruby/object:Gem::Version
68
- version: '2.14'
74
+ version: '3.0'
69
75
  type: :development
70
76
  prerelease: false
71
77
  version_requirements: !ruby/object:Gem::Requirement
72
78
  requirements:
73
79
  - - "~>"
74
80
  - !ruby/object:Gem::Version
75
- version: '2.14'
81
+ version: '3.0'
76
82
  - !ruby/object:Gem::Dependency
77
83
  name: rails
78
84
  requirement: !ruby/object:Gem::Requirement
@@ -143,7 +149,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
143
149
  - !ruby/object:Gem::Version
144
150
  version: '0'
145
151
  requirements: []
146
- rubygems_version: 3.0.3
152
+ rubygems_version: 3.1.4
147
153
  signing_key:
148
154
  specification_version: 4
149
155
  summary: Localize your app with YAML or GetText. Synchronize with your translators