diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..d9765b1 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,58 @@ +name: Build Status + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + ruby: + - '2.3' + - '2.4' + - '2.5' + - '2.6' + - '2.7' + - '3.0' + - '3.1' + - '3.2' + - '3.3.0-preview1' + - 'jruby-9.1.17.0' + gemfile: + - lowest + - latest + exclude: + # Ruby > 2.6 removed BigDecimal.new which was used on Rails 4.1 (removed with Rails 4.2 though) + - ruby: '2.7' + gemfile: lowest + - ruby: '3.0' + gemfile: lowest + - ruby: '3.1' + gemfile: lowest + - ruby: '3.2' + gemfile: lowest + - ruby: '3.3.0-preview1' + gemfile: lowest + env: + BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile + CC_TEST_REPORTER_ID: 945dfb58a832d233a3caeb84e3e6d3be212e8c7abcb48117fce63b9adcb43647 + steps: + - uses: actions/checkout@v2 + - name: Setup Bundler 1.x for Rails 4.x + if: ${{ matrix.gemfile == 'lowest' }} + run: echo "BUNDLER_VERSION=1.17.3" >> $GITHUB_ENV + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true # runs bundle install and caches installed gems automatically + bundler: ${{ env.BUNDLER_VERSION || 'latest' }} + - run: ruby --version + - name: Run specs + run: bundle exec rspec + - name: Publish code coverage + if: ${{ matrix.ruby == '3.1' && matrix.gemfile == 'latest' }} # only one is needed! + uses: paambaati/codeclimate-action@v3.0.0 + with: + debug: true + verifyDownload: true diff --git a/.gitignore b/.gitignore index d1efd75..f1ea31c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ coverage Gemfile.lock .ruby-version .bundle +RELEASE.md diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 9959231..0000000 --- a/.travis.yml +++ /dev/null @@ -1,57 +0,0 @@ -env: - global: - - CC_TEST_REPORTER_ID=1ec0e82ac7a13e879f71d36abbf09c7c691c71ec58a8b7f74beef6086e3b65db - -language: ruby - -# 2.0 has only one test not passing -# (TranslationIO::YAMLConversion#get_yaml_data_from_flat_translations works with weird not-escaped code) -rvm: - - 2.1 - - 2.2 - - 2.3 - - 2.4 - - 2.5 - - 2.6 - - 2.7 - - 3.0 - - jruby - -gemfile: - - gemfiles/lowest.gemfile - - gemfiles/latest.gemfile - -matrix: - exclude: - # Ruby > 2.6 removed BigDecimal.new which was used on Rails 4.1 (removed with Rails 4.2 though) - - rvm: 2.7 - gemfile: gemfiles/lowest.gemfile - - rvm: 3.0 - gemfile: gemfiles/lowest.gemfile - -sudo: false - -# https://docs.travis-ci.com/user/languages/ruby/ -# To stay on bundler 1 for compatibility with rails 4.1 -before_install: - - gem uninstall -v '>= 2' -i $(rvm gemdir)@global -ax bundler || true - - gem install bundler -v '1.17.3' - -# Override to be able to force bundler 1, needed for lowest.gemfile -install: bundle _1.17.3_ install --jobs=3 --retry=3 - -before_script: - - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter - - chmod +x ./cc-test-reporter - - ./cc-test-reporter before-build - -script: - - bundle _1.17.3_ exec rspec - -after_script: - - ./cc-test-reporter after-build -t simplecov --exit-code $TRAVIS_TEST_RESULT - -notifications: - email: - recipients: - - contact@translation.io diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d5f0cf..a28f06b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,109 @@ -# Changelog +# Changelog + +## [v1.38](https://github.com/translation/rails/releases/tag/v1.38) (2023-10-23) + +#### Fixes (bugs & defects): + + * Bump GetText dependency to 3.4.9 (fix `\r` escape and other improvements) + +## [v1.37](https://github.com/translation/rails/releases/tag/v1.37) (2023-06-27) + +#### Fixes (bugs & defects): + + * Prepare compatibility with Ruby 3.3 by bumping GetText dependency + +## [v1.36](https://github.com/translation/rails/releases/tag/v1.36) (2023-05-03) + +#### Fixes (bugs & defects): + + * Keep YAML files and keys from locales that are not in the configuration file (#54) + +## [v1.35](https://github.com/translation/rails/releases/tag/v1.35) (2023-01-12) + +#### Fixes (bugs & defects): + + * Compatibility with Ruby 3.2 (Fix `File.exists?` to `File.exist?`) + +## [v1.34](https://github.com/translation/rails/releases/tag/v1.34) (2022-11-16) + +#### Fixes (bugs & defects): + + * Fix plural rule in source PO file by manually adding it (for GetText >= 3.3.9) + +## [v1.33](https://github.com/translation/rails/releases/tag/v1.33) (2022-11-07) + +#### New features: + + * Match I18n fallbacks in GetText (cf. [#48](https://github.com/translation/rails/issues/48) and [#50](https://github.com/translation/rails/pull/50)). Thanks @ryanb! + +**Important information:** a new directory with PO/MO files for the source language will be created in your `locales/gettext` directory. +Don't worry, it's expected: the purpose is to unify fallback management. + +## [v1.32](https://github.com/translation/rails/releases/tag/v1.32) (2022-05-20) + +#### Fixes (bugs & defects): + + * Fix bad `html.erb` parsing when there is a `case` in it, using new gettext 3.4.3 Erubi parser. (cf. [ruby-gettext/gettext PR #91](https://github.com/ruby-gettext/gettext/pull/91)). + +## [v1.31](https://github.com/translation/rails/releases/tag/v1.31) (2022-04-04) + +#### Fixes (bugs & defects): + + * Fix Psych 4 breaking change when loading YAML files with aliases. See [issue #47](https://github.com/translation/rails/issues/47). + +## [v1.30](https://github.com/translation/rails/releases/tag/v1.30) (2022-03-23) + +#### New features: + + * Don't raise an issue for a conflicted `.translation_io` file anymore, but fix it directly. + +## [v1.29](https://github.com/translation/rails/releases/tag/v1.29) (2022-02-01) + +#### Fixes (bugs & defects): + + * Use `source_locale` as last fallback for GetText (instead of default "en"). + +## [v1.28](https://github.com/translation/rails/releases/tag/v1.28) (2022-01-17) + +#### New features: + + * Bump GetText dependency to 3.4.2 to support: + * Automatic locale fallbacks `fr_BE_Foo` -> `fr_BE` -> `fr` (cf. [ruby-gettext/gettext#89](https://github.com/ruby-gettext/gettext/issues/89)) + * Ruby 3.1 (cf. [ruby-gettext/gettext#92](https://github.com/ruby-gettext/gettext/issues/92)) + +## [v1.27](https://github.com/translation/rails/releases/tag/v1.27) (2021-10-06) + +#### Fixes (bugs & defects): + + * Improve symbol/string consistency of `source_locale` and `target_locales`. + +## [v1.26](https://github.com/translation/rails/releases/tag/v1.26) (2021-07-14) + +#### Fixes (bugs & defects): + + * Fix syntax issue with Ruby 3.0 and release v1.25 (that is now yanked!) + +## [v1.25](https://github.com/translation/rails/releases/tag/v1.25) (2021-07-14) + +#### Fixes (bugs & defects): + + * Don't remove empty keys in `localization.xx.yml` files when `config.yaml_remove_empty_keys = true`, they may be useful for delimiters, etc. + +## [v1.24](https://github.com/translation/rails/releases/tag/v1.24) (2021-06-15) + +#### Fixes (bugs & defects): + + * Force max version of GetText (3.3.7) to avoid dependency to external online request (see [here](https://github.com/ruby-gettext/gettext/issues/85#issuecomment-861462382) for discussion). + +#### New features: + + * Warnings when trying to init/sync with duplicate or empty `target_locales` in the configuration file. + +## [v1.23](https://github.com/translation/rails/releases/tag/v1.23) (2021-01-17) + +#### Fixes (bugs & defects): + + * Fix `ArgumentError: comparison of Pathname with String failed` if `I18n.load_path` contains a Pathname instead of a String ([#41](https://github.com/translation/rails/issues/41)). Thanks @11mdlow! ## [v1.22](https://github.com/translation/rails/releases/tag/v1.22) (2020-07-27) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3f890ed..108230e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,10 +2,6 @@ Feel free to contribute to this repository by creating a Pull Request. -Create an issue or reach us by email (contact@translation.io) before doing -the work if you want to increase the chances for your Pull Request to be -accepted. - ## Pull Request Process 1. Fork the repository. diff --git a/README.md b/README.md index da3dd6a..fe6bd10 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,18 @@ -# [Translation.io](https://translation.io) client for Ruby on Rails +# [Translation.io](https://translation.io/rails) client for Ruby on Rails [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) -[![Build Status](https://travis-ci.org/translation/rails.svg?branch=master)](https://travis-ci.org/translation/rails) +[![Build Status](https://github.com/translation/rails/actions/workflows/test.yml/badge.svg?branch=master)](https://github.com/translation/rails/actions/workflows/test.yml) [![Test Coverage](https://codeclimate.com/github/translation/rails/badges/coverage.svg)](https://codeclimate.com/github/translation/rails/test_coverage) [![Gem Version](https://badgen.net/rubygems/v/translation)](https://rubygems.org/gems/translation) [![Downloads](https://img.shields.io/gem/dt/translation.svg)](https://rubygems.org/gems/translation) -Add this gem to localize your Ruby on Rails application. +Add this gem to localize your **Ruby on Rails** application. Use the official Rails syntax (with [YAML](#i18n-yaml) files) or use the [GetText](#gettext) syntax. -Write only the source text, and keep it synchronized with your translators on [Translation.io](https://translation.io). +Write only the source text, and keep it synchronized with your translators on [Translation.io](https://translation.io/rails). - + @@ -20,10 +20,9 @@ Write only the source text, and keep it synchronized with your translators on [T Need help? [contact@translation.io](mailto:contact@translation.io) -Table of contents -================= +## Table of contents - * [Translation syntaxes](#translation-syntaxes) + * [Localization syntaxes](#localization-syntaxes) * [I18n (YAML)](#i18n-yaml) * [GetText](#gettext) * [Installation](#installation) @@ -39,6 +38,9 @@ Table of contents * [Change the current locale](#change-the-current-locale) * [Globally](#globally) * [Locally](#locally) + * [Frontend Localization](#frontend-localization) + * [Using this Gem](#using-this-gem) + * [Using our official React & JavaScript package](#using-our-official-react--javascript-package) * [Continuous Integration](#continuous-integration) * [Advanced Configuration Options](#advanced-configuration-options) * [Disable GetText or YAML](#disable-gettext-or-yaml) @@ -54,17 +56,18 @@ Table of contents * [List of clients for Translation.io](#list-of-clients-for-translationio) * [Ruby on Rails (Ruby)](#ruby-on-rails-ruby) * [Laravel (PHP)](#laravel-php) - * [React and React-Intl (JavaScript)](#react-and-react-intl-javascript) + * [React, React Native and JavaScript](#react-react-native-and-javascript) + * [Angular](#angular) * [Others](#others) * [License](#license) -## Translation syntaxes +## Localization syntaxes ### I18n (YAML) -The default [Rails Internationalization API](http://guides.rubyonrails.org/i18n.html). +The default [Rails Internationalization API](https://guides.rubyonrails.org/i18n.html). -```ruby +~~~ruby # Regular t('inbox.title') @@ -73,11 +76,11 @@ t('inbox.message', count: n) # Interpolation t('inbox.hello', name: @user.name) -``` +~~~ With the source YAML file: -```yaml +~~~yaml en: inbox: title: 'Title to be translated' @@ -86,7 +89,7 @@ en: one: 'one message' other: '%{count} messages' hello: 'Hello %{name}' -``` +~~~ You can keep your source YAML file automatically updated using [i18n-tasks](https://github.com/glebm/i18n-tasks). @@ -95,10 +98,10 @@ You can keep your source YAML file automatically updated using [i18n-tasks](http This gem adds the GetText support to Rails. We [strongly suggest](https://translation.io/blog/gettext-is-better-than-rails-i18n) that you use GetText to translate your application since it allows an easier and more complete syntax. -Also, you won't need to create and manage any YAML file since your code will be +Moreover, you won't need to create and manage any YAML file since your code will be automatically scanned for any string to translate. -```ruby +~~~ruby # Regular _("Text to be translated") @@ -113,7 +116,7 @@ np_("context", "Singular text", "Plural text", number) # Interpolations _('%{city1} is bigger than %{city2}') % { city1: "NYC", city2: "BXL" } -``` +~~~ More information about GetText syntax [here](https://github.com/ruby-gettext/gettext#usage). @@ -121,30 +124,30 @@ More information about GetText syntax [here](https://github.com/ruby-gettext/get 1. Add the gem to your project's Gemfile: -```ruby +~~~ruby gem 'translation' -``` +~~~ - 2. Create a new translation project [from the UI](https://translation.io). + 2. Create a new translation project [from the UI](https://translation.io/rails). 3. Copy the initializer into your Rails app (`config/initializers/translation.rb`) The initializer looks like this: -```ruby +~~~ruby TranslationIO.configure do |config| config.api_key = 'abcdefghijklmnopqrstuvwxyz012345' config.source_locale = 'en' config.target_locales = ['fr', 'nl', 'de', 'es'] end -``` +~~~ 4. Initialize your project and push existing translations to Translation.io with: -```bash -$ bundle exec rake translation:init -``` +~~~bash +bundle exec rake translation:init +~~~ -If you later need to add/remove target languages, please read our +If you need to add or remove languages in the future, please read our [documentation](https://translation.io/blog/adding-target-languages) about that. ## Usage @@ -153,17 +156,17 @@ If you later need to add/remove target languages, please read our To send new translatable keys/strings and get new translations from Translation.io, simply run: -```bash -$ bundle exec rake translation:sync -``` +~~~bash +bundle exec rake translation:sync +~~~ ### Sync and Show Purgeable If you need to find out what are the unused keys/strings from Translation.io, using the current branch as reference: -```bash -$ bundle exec rake translation:sync_and_show_purgeable -``` +~~~bash +bundle exec rake translation:sync_and_show_purgeable +~~~ As the name says, this operation will also perform a sync at the same time. @@ -171,13 +174,14 @@ As the name says, this operation will also perform a sync at the same time. If you need to remove unused keys/strings from Translation.io, using the current branch as reference: -```bash -$ bundle exec rake translation:sync_and_purge -``` +~~~bash +bundle exec rake translation:sync_and_purge +~~~ As the name says, this operation will also perform a sync at the same time. -Warning: all keys that are not present in the current branch will be **permanently deleted from Translation.io**. +Warning: all keys that are not present in the current local branch +will be **permanently deleted from Translation.io**. ## Manage Languages @@ -211,20 +215,26 @@ or another instance of your application. A custom language is always be derived from an [existing language](https://translation.io/docs/languages). Its structure should be like: -```ruby +~~~ruby "#{existing_language_code}-#{custom_text}" -``` +~~~ -where `custom_text` can only contain alphanumeric characters and `-`. +where `custom_text` can only contain alphabetic characters and `-`. Examples: `en-microsoft` or `fr-BE-custom`. ### Fallbacks -Using [I18n (YAML)](#i18n-yaml) syntax, fallbacks will work as expected for any regional or custom -language. It means that if the `en-microsoft.example` key is missing, -then it will fallback to `en.example`. So you only need to translate keys that -are different from the main language. +If a translation is missing for a regional (`fr-BE`) or custom (`fr-microsoft`) +language, then it will fallback to the main language (`fr`). + +Locale fallbacks will work as expected with both [I18n (YAML)](#i18n-yaml) +and [GetText](#gettext) syntaxes. + +A good way to leverage this feature is to ignore sentences from a regional language +that would have the same translation as the main language (usually most of them). +It's way easier to maintain the project over time if only 10% of the regional sentences +need to be adapted. Note that fallbacks are chained, so `fr-BE-custom` will fallback to `fr-BE` that will fallback to `fr`. @@ -233,23 +243,19 @@ Just make sure to add `config.i18n.fallbacks = true` to your `config/application You can find more information about this [here](https://guides.rubyonrails.org/configuring.html#configuring-i18n). -Using [GetText](#gettext) syntax, it will only fallback to the source language. -So either you create a fallback mechanism by yourself or you avoid fallbacking -by translating everything in Translation.io for the regional or custom language. - ## Change the current locale ### Globally The easiest way to change the current locale is with `set_locale`. -```ruby +~~~ruby class ApplicationController < ActionController::Base before_action :set_locale [...] end -``` +~~~ First time the user will connect, it will automatically set the locale extracted from the user's browser `HTTP_ACCEPT_LANGUAGE` value, and keep it in the session between @@ -258,16 +264,17 @@ requests. 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: -```ruby +~~~ruby scope "/:locale", :constraints => { locale: /[a-z]{2}/ } do resources :pages end -``` +~~~ 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. -Don't forget to define your available locales with [I18n.available_locales](http://guides.rubyonrails.org/i18n.html#setup-the-rails-application-for-internationalization). +Don't forget to define your available locales with +[I18n.available_locales](https://guides.rubyonrails.org/i18n.html#setup-the-rails-application-for-internationalization). More examples here: https://translation.io/blog/set-current-locale-in-your-rails-app @@ -275,25 +282,99 @@ More examples here: https://translation.io/blog/set-current-locale-in-your-rails This command will change the locale for both [I18n (YAML)](#i18n-yaml) and [GetText](#gettext): -```ruby +~~~ruby I18n.locale = 'fr' -``` +~~~ You can call it several times in the same page if you want to switch between languages. More examples here: https://translation.io/blog/rails-i18n-with-locale +## Frontend Localization + +### Using this Gem + +This gem is also able to cover frontend localization (React, Vue, ...). + +There are several ways to pass the translation strings from the backend +to the frontend: JavaScript serialization, `data-` HTML attributes, JSON files etc. + +The easiest strategy when dealing with React/Vue would be to pass the corresponding +translations as props when mounting the components. + +Assuming that you use [reactjs/react-rails](https://github.com/reactjs/react-rails), +it would look like this if you want to use [I18n (YAML)](#i18n-yaml) syntax: + +~~~erb +<%= +react_component("MyComponent", { + :user_id => current_user.id, + :i18n => YAML.load_file("config/locales/#{I18n.locale}.yml")[I18n.locale.to_s]["my_component"] +}) +%> +~~~ + +Your `en.yml` should look like this: + +~~~yaml +en: + my_component: + your_name: Your name + title: Title +~~~ + +You can also directly use the [GetText](#gettext) syntax: + +~~~erb +<%= +react_component("MyComponent", { + :user_id => current_user.id, + :i18n => { + :your_name => _('Your name'), + :title => _('Title') + } +}) +%> +~~~ + +In both case, in your React component, you can simply call +`this.props.i18n.yourName` and your text will be localized with the current locale. + +**Notes:** + + * You can also structure the i18n props with multiple levels of depth and pass the subtree as props to each of your sub-components. + * It also works great with server-side rendering of your components (`:prerender => true`). + +### Using our official React & JavaScript package + +As Translation.io is directly integrated in the great +[Lingui](https://lingui.dev/) internationalization framework, +you can also consider frontend localization as a completely different +localization project. + +Please read more about this on: + + * Website: [https://translation.io/lingui](https://translation.io/lingui) + * GitHub page: [https://github.com/translation/lingui](https://github.com/translation/lingui) + ## Continuous Integration -If you want fresh translations in your Continuous Integration workflow, you may find yourself calling `bundle exec rake translation:sync` very frequently. +If you want fresh translations in your Continuous Integration workflow, you may +find yourself calling `bundle exec rake translation:sync` very frequently. -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: +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: -```bash -$ bundle exec rake translation:sync_readonly -``` +~~~bash +bundle exec rake translation:sync_readonly +~~~ -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. +This task will prevent your CI from failing 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. ## Advanced Configuration Options @@ -305,24 +386,24 @@ Some options are described below but for an exhaustive list, please refer to [co If you want to only use YAML files and totally ignore GetText syntax, use: -```ruby +~~~ruby TranslationIO.configure do |config| ... config.disable_gettext = true ... end -``` +~~~ In contrast, if you only want to synchronize GetText files and leave the YAML files unchanged, use: -```ruby +~~~ruby TranslationIO.configure do |config| ... config.disable_yaml = true ... end -``` +~~~ ### Ignored YAML keys @@ -331,7 +412,7 @@ You can use the `ignored_key_prefixes` for that. For example: -```ruby +~~~ruby TranslationIO.configure do |config| ... config.ignored_key_prefixes = [ @@ -346,7 +427,7 @@ TranslationIO.configure do |config| ] ... end -``` +~~~ ### Custom localization key prefixes @@ -361,18 +442,18 @@ with percent signs or spaces) that he might break. We think localization is part of the configuration of the app and it should not reach the translator UI at all. That's why these localization keys are detected and separated on a dedicated YAML file with Translation.io. -We automatically treat [known localization keys](lib/translation_io/yaml_entry.rb), but if you would like +We automatically treat [known localization keys](https://github.com/translation/rails/blob/master/lib/translation_io/yaml_entry.rb), but if you would like to add some more, use the `localization_key_prefixes` option. For example: -```ruby +~~~ruby TranslationIO.configure do |config| ... config.localization_key_prefixes = ['my_gem.date.formats'] ... end -``` +~~~ ### Source file formats (for GetText) @@ -380,12 +461,12 @@ If you are using GetText and you want to manage other file formats than: * `rb`, `ruby` and `rabl` for Ruby. * `erb` and `inky` for Ruby templating. - * `haml` and `mjmlhaml` for [HAML](http://haml.info/). - * `slim` and `mjmlslim` for [SLIM](http://slim-lang.com/). + * `haml` and `mjmlhaml` for [HAML](https://haml.info/). + * `slim` and `mjmlslim` for [SLIM](https://github.com/slim-template/slim). Just add them in your configuration file like this: -```ruby +~~~ruby TranslationIO.configure do |config| ... config.source_formats << 'rb2' @@ -394,33 +475,33 @@ TranslationIO.configure do |config| config.slim_source_formats << 'slim2' ... end -``` +~~~ ### Gems with GetText strings Public gems usually don't make use of GetText strings, but if you created and localized your own gems with the GetText syntax, you'll want to be able to synchronize them: -```ruby +~~~ruby TranslationIO.configure do |config| ... config.parsed_gems = ['your_gem_name'] ... end -``` +~~~ ### Paths where locales are stored (not recommended) You can specify where your GetText and YAML files are on disk: -```ruby +~~~ruby TranslationIO.configure do |config| ... config.locales_path = 'some/path' # defaults to config/locales/gettext config.yaml_locales_path = 'some/path' # defaults to config/locales ... end -``` +~~~ ### GetText Object Class Monkey-Patching @@ -430,27 +511,27 @@ This is made by extending the global `Object` class. You can disable the built-in `Object` monkey-patching if you prefer a more granular approach: -```ruby +~~~ruby TranslationIO.configure do |config| ... config.gettext_object_delegate = false ... end -``` +~~~ Don't forget to manually include the GetText methods where needed: -```ruby +~~~ruby class Contact < ApplicationRecord extend TranslationIO::Proxy end -``` +~~~ ## Pure Ruby (without Rails) This gem was created specifically for Rails, but you can also use it in a pure Ruby project by making some arrangements: -```ruby +~~~ruby require 'rubygems' require 'active_support/all' require 'yaml' @@ -497,7 +578,7 @@ This gem was created specifically for Rails, but you can also use it in a pure R config.target_locales = ['nl', 'de'] config.metadata_path = 'i18n/.translation_io' end -``` +~~~ (Thanks [@kubaw](https://github.com/kubaw) for this snippet!) @@ -505,9 +586,9 @@ This gem was created specifically for Rails, but you can also use it in a pure R To run the specs: -```bash -$ bundle exec rspec -``` +~~~bash +bundle exec rspec +~~~ ## Contributing @@ -515,15 +596,16 @@ Please read the [CONTRIBUTING](CONTRIBUTING.md) file. ## List of clients for Translation.io -These implementations were usually started by contributors for their own projects. -Some of them are officially supported by [Translation.io](https://translation.io) -and some are not yet supported. However, they are quite well documented. +The following clients are officially supported by [Translation.io](https://translation.io) +and are well documented. -Thanks a lot to these contributors for their hard work! +Some of these implementations (and other non-officially supported ones) +were started by contributors for their own translation projects. +We are thankful to all contributors for their hard work! ### Ruby on Rails (Ruby) -Officially Supported on [https://translation.io/rails](https://translation.io/rails) +Officially supported on [https://translation.io/rails](https://translation.io/rails) * GitHub: https://github.com/translation/rails * RubyGems: https://rubygems.org/gems/translation/ @@ -532,19 +614,31 @@ Credits: [@aurels](https://github.com/aurels), [@michaelhoste](https://github.co ### Laravel (PHP) -Officially Supported on [https://translation.io/laravel](https://translation.io/laravel) +Officially supported on [https://translation.io/laravel](https://translation.io/laravel) * GitHub: https://github.com/translation/laravel * Packagist: https://packagist.org/packages/tio/laravel Credits: [@armandsar](https://github.com/armandsar), [@michaelhoste](https://github.com/michaelhoste) -### React and React-Intl (JavaScript) +### React, React Native and JavaScript + +Officially supported on [https://translation.io/lingui](https://translation.io/lingui) + +Translation.io is directly integrated in the great +[Lingui](https://lingui.dev/) internationalization project. + + * GitHub: https://github.com/translation/lingui + * NPM: https://www.npmjs.com/package/@translation/lingui + +### Angular - * GitHub: https://github.com/deecewan/translation-io - * NPM: https://www.npmjs.com/package/translation-io +Officially supported on [https://translation.io/angular](https://translation.io/angular) -Credits: [@deecewan](https://github.com/deecewan) + * GitHub: https://github.com/translation/angular + * NPM: https://www.npmjs.com/package/@translation/angular + +Credits: [@SimonCorellia](https://github.com/SimonCorellia), [@didier-84](https://github.com/didier-84), [@michaelhoste](https://github.com/michaelhoste) ### Others @@ -556,12 +650,12 @@ guide and use the special You can also use the more [traditional API](https://translation.io/docs/api). -Feel free to contact us on [contact@translation.io](mailto:contact@translation.io) if -you need some help or if you want to share your library. +Feel free to contact us on [contact@translation.io](mailto:contact@translation.io) +if you need some help or if you want to share your library. ## License The [translation gem](https://rubygems.org/gems/translation) in released under MIT license by -[Aurélien Malisart](http://aurelien.malisart.be) and [Michaël Hoste](https://80limit.com) (see [LICENSE](LICENSE) file). +[Aurélien Malisart](https://aurelien.malisart.be) and [Michaël Hoste](https://80limit.com) (see [LICENSE](LICENSE) file). (c) [https://translation.io](https://translation.io) / [contact@translation.io](mailto:contact@translation.io) diff --git a/gemfiles/lowest.gemfile b/gemfiles/lowest.gemfile index 120fb08..68ab8de 100644 --- a/gemfiles/lowest.gemfile +++ b/gemfiles/lowest.gemfile @@ -2,7 +2,7 @@ source "https://rubygems.org" gem 'gettext', '3.2.5' gem 'simplecov', '0.12' -gem 'rspec', '2.14' +gem 'rspec', '3.1' gem 'rails', '4.1' gemspec :path => "../" diff --git a/lib/translation.rb b/lib/translation.rb index b2fb887..ef87d24 100644 --- a/lib/translation.rb +++ b/lib/translation.rb @@ -33,13 +33,16 @@ def configure(&block) if !@config.disable_gettext require_gettext_dependencies - add_missing_locales add_parser_for_erb_source_formats(@config.erb_source_formats) if Rails.env.development? GetText::TextDomainManager.cached = false end + # Set default GetText locale (last fallback) as config.source_locale instead of "en" (default) + gettext_locale = @config.source_locale.to_s.gsub('-', '_').to_sym + Locale.set_default(gettext_locale) + # include is private until Ruby 2.1 Proxy.send(:include, GetText) @@ -69,19 +72,20 @@ def require_gettext_dependencies require 'gettext/tools' require 'gettext/text_domain_manager' require 'gettext/tools/xgettext' - end - - # Missing languages from Locale that are in Translation.io - def add_missing_locales - Locale::Info.three_languages['wee'] = Locale::Info::Language.new('', 'wee', 'I', 'L', 'Lower Sorbian') - Locale::Info.three_languages['wen'] = Locale::Info::Language.new('', 'wen', 'I', 'L', 'Upper Sorbian') + require "gettext/tools/parser/erubi" if Gem::Version.new(GetText::VERSION) >= Gem::Version.new('3.4.3') end def add_parser_for_erb_source_formats(new_erb_formats) - existing_extensions = GetText::ErbParser.instance_variable_get("@config")[:extnames] new_extensions = new_erb_formats.collect { |ext| ".#{ext}" } + existing_extensions = GetText::ErbParser.instance_variable_get("@config")[:extnames] GetText::ErbParser.instance_variable_get("@config")[:extnames] = (existing_extensions + new_extensions).uniq + + # for gettext >= 3.4.3 (erubi compatibility) + if defined?(GetText::ErubiParser) + existing_extensions = GetText::ErubiParser.instance_variable_get("@config")[:extnames] + GetText::ErubiParser.instance_variable_get("@config")[:extnames] = (existing_extensions + new_extensions).uniq + end end def info(message, level = 0, verbose_level = 0) @@ -96,6 +100,15 @@ def normalize_path(relative_or_absolute_path) File.expand_path(relative_or_absolute_path).gsub("#{Dir.pwd}/", '') end + # Cf. https://github.com/translation/rails/issues/47 + def yaml_load(source) + begin + YAML.load(source, :aliases => true) || {} + rescue ArgumentError + YAML.load(source) || {} + end + end + def version Gem::Specification::find_by_name('translation').version.to_s end diff --git a/lib/translation_io/client/base_operation.rb b/lib/translation_io/client/base_operation.rb index 826007b..b45f450 100644 --- a/lib/translation_io/client/base_operation.rb +++ b/lib/translation_io/client/base_operation.rb @@ -18,6 +18,38 @@ def initialize(client) private + def warn_wrong_locales(source_locale, target_locales) + if target_locales.uniq != target_locales + duplicate_locale = target_locales.detect { |locale| target_locales.count(locale) > 1 } + + puts + puts "----------" + puts "Your `config.target_locales` has a duplicate locale (#{duplicate_locale})." + puts "Please clean your configuration file and execute this command again." + puts "----------" + exit(true) + end + + if target_locales.include?(source_locale) + puts + puts "----------" + puts "The `config.source_locale` (#{source_locale}) can't be included in the `config.target_locales`." + puts "If you want to customize your source locale, check this link: https://github.com/translation/rails#custom-languages" + puts "Please clean your configuration file and execute this command again." + puts "----------" + exit(true) + end + + if target_locales.empty? + puts + puts "----------" + puts "Your `config.target_locales` is empty." + puts "Please clean your configuration file and execute this command again." + puts "----------" + exit(true) + end + end + def self.perform_request(uri, params) begin params.merge!({ diff --git a/lib/translation_io/client/base_operation/save_new_po_files_step.rb b/lib/translation_io/client/base_operation/save_new_po_files_step.rb index 7f17675..d68204d 100644 --- a/lib/translation_io/client/base_operation/save_new_po_files_step.rb +++ b/lib/translation_io/client/base_operation/save_new_po_files_step.rb @@ -2,7 +2,8 @@ module TranslationIO class Client class BaseOperation class SaveNewPoFilesStep - def initialize(target_locales, locales_path, parsed_response) + def initialize(source_locale, target_locales, locales_path, parsed_response) + @source_locale = source_locale @target_locales = target_locales @locales_path = locales_path @parsed_response = parsed_response @@ -25,8 +26,46 @@ def run end end + create_source_po(text_domain) + return self end + + # Create source locale PO file, with identical source and target + # => Useful for correct fallbacks (cf. discussion https://github.com/translation/rails/issues/48) + def create_source_po(text_domain) + source_locale = Locale::Tag.parse(@source_locale) + + pot_path = File.join(@locales_path, "#{text_domain}.pot") + po_path = File.join(@locales_path, source_locale.to_s, "#{text_domain}.po") + + FileUtils.mkdir_p(File.dirname(po_path)) + FileUtils.rm(po_path) if File.exist?(po_path) + + # Generate source PO from POT and parse it + GetText::Tools::MsgInit.run('-i', pot_path, '-o', po_path, '-l', source_locale.to_s, '--no-translator') + po_entries = GetText::PO.new + GetText::POParser.new.parse(File.read(po_path), po_entries) + + # With GetText after 3.3.9 and the introduction of optional `red-datasets` dependency, we need to complete the plural forms manually + # Source languages must have only 2 plural forms, and only 2 different pluralization rules exist for these languages + if %w(ak am as bn fa fr gu hi hy kn ln mg nso pa si ti zu).include?(source_locale.language) + po_entries[''].msgstr.gsub!('Plural-Forms: nplurals=; plural=;', 'Plural-Forms: nplurals=2; plural=n > 1;') # 0 and 1 are singular + else + po_entries[''].msgstr.gsub!('Plural-Forms: nplurals=; plural=;', 'Plural-Forms: nplurals=2; plural=n != 1;') # only 1 is singular + end + + # Fill with same target as source and save it + po_entries.each do |po_entry| + if po_entry.msgid != '' # header + po_entry.msgstr = [po_entry.msgid, po_entry.msgid_plural].compact.join("\000") + end + end + + File.open(po_path, 'wb') do |file| + file.write(po_entries.to_s) + end + end end end end diff --git a/lib/translation_io/client/base_operation/save_special_yaml_files_step.rb b/lib/translation_io/client/base_operation/save_special_yaml_files_step.rb index 68303a8..7ae7fbd 100644 --- a/lib/translation_io/client/base_operation/save_special_yaml_files_step.rb +++ b/lib/translation_io/client/base_operation/save_special_yaml_files_step.rb @@ -35,13 +35,15 @@ def run YamlEntry.from_locale?(key, target_locale) && !YamlEntry.ignored?(key) end - yaml_data = YAMLConversion.get_yaml_data_from_flat_translations(target_flat_special_translations) + yaml_data = YAMLConversion.get_yaml_data_from_flat_translations(target_flat_special_translations, **{ + :force_keep_empty_keys => true # We want to keep empty keys from localization.xx.yml files (sometimes needed for delimiters!) + }) params["yaml_data_#{target_locale}"] = yaml_data # To have a localization.xx.yml file during tests (without call to backend) if TranslationIO.config.test - if YAML::load(yaml_data).present? + if TranslationIO.yaml_load(yaml_data).present? File.open(yaml_path, 'wb') do |file| file.write(self.class.top_comment) file.write(yaml_data) @@ -62,7 +64,7 @@ def run yaml_path = File.join(@yaml_locales_path, "localization.#{target_locale}.yml") yaml_data = parsed_response["yaml_data_#{target_locale}"] - if yaml_data.present? && YAML::load(yaml_data).present? + if yaml_data.present? && TranslationIO.yaml_load(yaml_data).present? File.open(yaml_path, 'wb') do |file| file.write(self.class.top_comment) file.write(yaml_data) diff --git a/lib/translation_io/client/base_operation/update_pot_file_step.rb b/lib/translation_io/client/base_operation/update_pot_file_step.rb index c18aab8..4f16bf2 100644 --- a/lib/translation_io/client/base_operation/update_pot_file_step.rb +++ b/lib/translation_io/client/base_operation/update_pot_file_step.rb @@ -16,13 +16,29 @@ def run(params) TranslationIO.info "Updating POT file." FileUtils.mkdir_p(File.dirname(@pot_path)) - TranslationIO.info "source files:" + @source_files.to_s - GetText::Tools::XGetText.run(*@source_files, '-o', @pot_path, - '--msgid-bugs-address', TranslationIO.config.pot_msgid_bugs_address, - '--package-name', TranslationIO.config.pot_package_name, - '--package-version', TranslationIO.config.pot_package_version, - '--copyright-holder', TranslationIO.config.pot_copyright_holder, - '--copyright-year', TranslationIO.config.pot_copyright_year.to_s) + + # for gettext >= 3.4.3 (erubi compatibility) + if defined?(GetText::ErubiParser) + GetText::Tools::XGetText.run( + *@source_files, '-o', @pot_path, + '--msgid-bugs-address', TranslationIO.config.pot_msgid_bugs_address, + '--package-name', TranslationIO.config.pot_package_name, + '--package-version', TranslationIO.config.pot_package_version, + '--copyright-holder', TranslationIO.config.pot_copyright_holder, + '--copyright-year', TranslationIO.config.pot_copyright_year.to_s, + '--require', 'gettext/tools/parser/erubi', # Cf. (1) https://github.com/ruby-gettext/gettext/pull/91 + '--parser', 'GetText::ErubiParser' # (2) https://github.com/ruby-gettext/gettext/commit/0eb06a88323a5cc16065680ffe228d978fb87c88 + ) + else + GetText::Tools::XGetText.run( + *@source_files, '-o', @pot_path, + '--msgid-bugs-address', TranslationIO.config.pot_msgid_bugs_address, + '--package-name', TranslationIO.config.pot_package_name, + '--package-version', TranslationIO.config.pot_package_version, + '--copyright-holder', TranslationIO.config.pot_copyright_holder, + '--copyright-year', TranslationIO.config.pot_copyright_year.to_s + ) + end FileUtils.rm_f(@tmp_empty_file) if @tmp_empty_file.present? diff --git a/lib/translation_io/client/init_operation.rb b/lib/translation_io/client/init_operation.rb index 8c21e77..5eb4645 100644 --- a/lib/translation_io/client/init_operation.rb +++ b/lib/translation_io/client/init_operation.rb @@ -13,12 +13,14 @@ def run haml_source_files = config.haml_source_files slim_source_files = config.slim_source_files pot_path = config.pot_path - source_locale = config.source_locale - target_locales = config.target_locales + source_locale = config.source_locale.to_s + target_locales = config.target_locales.map(&:to_s) locales_path = config.locales_path yaml_locales_path = config.yaml_locales_path yaml_file_paths = config.yaml_file_paths + warn_wrong_locales(source_locale, target_locales) + if !config.disable_gettext BaseOperation::DumpMarkupGettextKeysStep.new(haml_source_files, :haml).run BaseOperation::DumpMarkupGettextKeysStep.new(slim_source_files, :slim).run @@ -44,7 +46,7 @@ def run if !parsed_response.nil? if !config.disable_gettext - BaseOperation::SaveNewPoFilesStep.new(target_locales, locales_path, parsed_response).run + BaseOperation::SaveNewPoFilesStep.new(source_locale, target_locales, locales_path, parsed_response).run BaseOperation::CreateNewMoFilesStep.new(locales_path).run end diff --git a/lib/translation_io/client/init_operation/cleanup_yaml_files_step.rb b/lib/translation_io/client/init_operation/cleanup_yaml_files_step.rb index 5b053e6..ef315a4 100644 --- a/lib/translation_io/client/init_operation/cleanup_yaml_files_step.rb +++ b/lib/translation_io/client/init_operation/cleanup_yaml_files_step.rb @@ -12,30 +12,28 @@ def initialize(source_locale, target_locales, yaml_file_paths, yaml_locales_path def run @yaml_file_paths.each do |locale_file_path| if locale_file_removable?(locale_file_path) - if File.exist?(locale_file_path) - content_hash = YAML::load(File.read(locale_file_path)) || {} - source_content_hash = content_hash.select { |k| k.to_s == @source_locale.to_s } + content_hash = TranslationIO.yaml_load(File.read(locale_file_path)) || {} + source_content_hash = content_hash.reject { |k| k.to_s.in?(@target_locales.collect(&:to_s)) } - if source_content_hash.empty? - TranslationIO.info "Removing #{locale_file_path}", 2, 2 - FileUtils.rm(locale_file_path) - elsif content_hash != source_content_hash # in case of mixed languages in source YAML file - TranslationIO.info "Rewriting #{locale_file_path}", 2, 2 + if source_content_hash.empty? + TranslationIO.info "Removing #{locale_file_path}", 2, 2 + FileUtils.rm(locale_file_path) + elsif content_hash != source_content_hash # in case of mixed languages in source YAML file + TranslationIO.info "Rewriting #{locale_file_path}", 2, 2 - if TranslationIO.config.yaml_line_width - file_content = source_content_hash.to_yaml(:line_width => TranslationIO.config.yaml_line_width) - else - file_content = source_content_hash.to_yaml - end + if TranslationIO.config.yaml_line_width + file_content = source_content_hash.to_yaml(:line_width => TranslationIO.config.yaml_line_width) + else + file_content = source_content_hash.to_yaml + end - file_content = file_content.gsub(/ $/, '') # remove trailing spaces + file_content = file_content.gsub(/ $/, '') # remove trailing spaces - File.open(locale_file_path, 'wb') do |file| - file.write(file_content) - end - else - # don't touch source + File.open(locale_file_path, 'wb') do |file| + file.write(file_content) end + else + # don't touch source end end end @@ -44,6 +42,7 @@ def run private def locale_file_removable?(locale_file_path) + exists = File.exist?(locale_file_path) in_project = locale_file_path_in_project?(locale_file_path) protected_file = @target_locales.any? do |target_locale| @@ -55,7 +54,7 @@ def locale_file_removable?(locale_file_path) paths.include?(TranslationIO.normalize_path(locale_file_path)) end - in_project && !protected_file + exists && in_project && !protected_file end def locale_file_path_in_project?(locale_file_path) diff --git a/lib/translation_io/client/init_operation/create_yaml_po_files_step.rb b/lib/translation_io/client/init_operation/create_yaml_po_files_step.rb index 7a43058..e2ac079 100644 --- a/lib/translation_io/client/init_operation/create_yaml_po_files_step.rb +++ b/lib/translation_io/client/init_operation/create_yaml_po_files_step.rb @@ -18,7 +18,7 @@ def run(params) @yaml_file_paths.each do |file_path| TranslationIO.info file_path, 2, 2 - file_translations = YAML::load(File.read(file_path)) + file_translations = TranslationIO.yaml_load(File.read(file_path)) unless file_translations.blank? all_translations = all_translations.deep_merge(file_translations) diff --git a/lib/translation_io/client/sync_operation.rb b/lib/translation_io/client/sync_operation.rb index 6c161e8..ddcff9f 100644 --- a/lib/translation_io/client/sync_operation.rb +++ b/lib/translation_io/client/sync_operation.rb @@ -16,13 +16,15 @@ def run(options = {}) haml_source_files = config.haml_source_files slim_source_files = config.slim_source_files pot_path = config.pot_path - source_locale = config.source_locale - target_locales = config.target_locales + source_locale = config.source_locale.to_s + target_locales = config.target_locales.map(&:to_s) locales_path = config.locales_path yaml_locales_path = config.yaml_locales_path yaml_file_paths = config.yaml_file_paths db_fields = config.db_fields + warn_wrong_locales(source_locale, target_locales) + if !config.disable_yaml ApplyYamlSourceEditsStep.new(yaml_file_paths, source_locale).run(params) end @@ -57,7 +59,7 @@ def run(options = {}) if !parsed_response.nil? if !config.disable_gettext - BaseOperation::SaveNewPoFilesStep.new(target_locales, locales_path, parsed_response).run + BaseOperation::SaveNewPoFilesStep.new(source_locale, target_locales, locales_path, parsed_response).run BaseOperation::CreateNewMoFilesStep.new(locales_path).run end diff --git a/lib/translation_io/client/sync_operation/apply_yaml_source_edits_step.rb b/lib/translation_io/client/sync_operation/apply_yaml_source_edits_step.rb index 46c2842..d55c024 100644 --- a/lib/translation_io/client/sync_operation/apply_yaml_source_edits_step.rb +++ b/lib/translation_io/client/sync_operation/apply_yaml_source_edits_step.rb @@ -21,7 +21,7 @@ def run(params) reload_or_reuse_yaml_sources - @yaml_sources.each do |yaml_source| + @yaml_sources.to_a.each do |yaml_source| yaml_file_path = yaml_source[:yaml_file_path] yaml_flat_hash = yaml_source[:yaml_flat_hash] @@ -46,7 +46,7 @@ def reload_or_reuse_yaml_sources if yaml_sources_reload_needed? @yaml_sources = sort_by_project_locales_first(@yaml_file_paths).collect do |yaml_file_path| yaml_content = File.read(yaml_file_path) - yaml_hash = YAML::load(yaml_content) + yaml_hash = TranslationIO.yaml_load(yaml_content) yaml_flat_hash = FlatHash.to_flat_hash(yaml_hash) { @@ -55,7 +55,7 @@ def reload_or_reuse_yaml_sources } end else - @yaml_sources + @yaml_source end end @@ -103,7 +103,7 @@ def apply_gem_source_edit(source_edit) # Source yaml file like config/locales/en.yml yaml_file_path = File.expand_path(File.join(TranslationIO.config.yaml_locales_path, "#{@source_locale}.yml")) - if File.exists?(yaml_file_path) + if File.exist?(yaml_file_path) # Complete existing hash if YAML file already exists existing_yaml_source = @yaml_sources.detect { |y_s| normalize_path(y_s[:yaml_file_path]) == normalize_path(yaml_file_path) } yaml_flat_hash = existing_yaml_source[:yaml_flat_hash] @@ -133,11 +133,12 @@ def metadata_timestamp if File.exist?(TranslationIO.config.metadata_path) metadata_content = File.read(TranslationIO.config.metadata_path) + # If any conflicts in file, take the lowest timestamp and potentially reapply some source edits if metadata_content.include?('>>>>') || metadata_content.include?('<<<<') - TranslationIO.info "[Error] #{TranslationIO.config.metadata_path} file is corrupted and seems to have unresolved versioning conflicts. Please resolve them and try again." - exit(false) + timestamps = metadata_content.scan(/timestamp: (\d*)/).flatten.uniq.collect(&:to_i) + return timestamps.min || 0 else - return YAML::load(metadata_content)['timestamp'] rescue 0 + return YAML.load(metadata_content)['timestamp'] rescue 0 end else return 0 @@ -146,6 +147,13 @@ def metadata_timestamp def update_metadata_timestamp File.open(TranslationIO.config.metadata_path, 'w') do |f| + f.puts '# This file is used in the context of Translation.io source editions.' + f.puts '# Please see: https://translation.io/blog/new-feature-copywriting' + f.puts '#' + f.puts '# If you have any git conflicts, either keep the smaller timestamp or' + f.puts '# ignore the conflicts and "sync" again, it will fix this file for you.' + f.puts + f.write({ 'timestamp' => Time.now.utc.to_i }.to_yaml) end end diff --git a/lib/translation_io/client/sync_operation/create_yaml_pot_file_step.rb b/lib/translation_io/client/sync_operation/create_yaml_pot_file_step.rb index 35fefb5..4463858 100644 --- a/lib/translation_io/client/sync_operation/create_yaml_pot_file_step.rb +++ b/lib/translation_io/client/sync_operation/create_yaml_pot_file_step.rb @@ -14,7 +14,7 @@ def run(params) @yaml_file_paths.each do |file_path| TranslationIO.info file_path, 2, 2 - file_translations = YAML::load(File.read(file_path)) + file_translations = TranslationIO.yaml_load(File.read(file_path)) unless file_translations.blank? all_translations = all_translations.deep_merge(file_translations) diff --git a/lib/translation_io/config.rb b/lib/translation_io/config.rb index 77dcd79..0afe917 100644 --- a/lib/translation_io/config.rb +++ b/lib/translation_io/config.rb @@ -126,7 +126,7 @@ def pot_path end def yaml_file_paths - I18n.load_path.select do |p| + I18n.load_path.collect(&:to_s).uniq.select do |p| File.exist?(p) && (File.extname(p) == '.yml' || File.extname(p) == '.yaml') end end diff --git a/lib/translation_io/extractor.rb b/lib/translation_io/extractor.rb index c45a7a7..81eaa2f 100644 --- a/lib/translation_io/extractor.rb +++ b/lib/translation_io/extractor.rb @@ -1,7 +1,7 @@ module TranslationIO module Extractor # visual: https://www.debuggex.com/r/fYSQ-jwQfTjhhE6T - # .*? is non-greedy (lazy) match : http://stackoverflow.com/a/1919995/1243212 + # .*? is non-greedy (lazy) match : https://stackoverflow.com/a/1919995/1243212 REGEXP_INSIDE_1 = '\s*(?:\[?(?:(?:(?:".*?")|(?:\'.*?\'))\s*?,?\s*?){1}\]?)\s*?' REGEXP_INSIDE_2 = '\s*(?:\[?(?:(?:(?:".*?")|(?:\'.*?\'))\s*?,?\s*?){2}\]?),?\s*?.*?\s*' REGEXP_INSIDE_2B = '\s*(?:\[?(?:(?:(?:".*?")|(?:\'.*?\'))\s*?,?\s*?){2}\]?)\s*?' diff --git a/lib/translation_io/railtie.rb b/lib/translation_io/railtie.rb index 341b508..63c2458 100644 --- a/lib/translation_io/railtie.rb +++ b/lib/translation_io/railtie.rb @@ -1,5 +1,6 @@ require 'i18n' require 'i18n/config' +require "i18n/backend/fallbacks" module TranslationIO class Railtie < Rails::Railtie @@ -20,20 +21,32 @@ class Railtie < Rails::Railtie end end -module I18n - class Config - def locale=(locale) - I18n.enforce_available_locales!(locale) if I18n.respond_to?(:enforce_available_locales!) - @locale = locale.to_sym rescue nil +### +# Set GetText/Locale current locale based on I18n.locale +### +module I18nConfigExtension + def locale=(*args) + super - if defined?(GetText) - GetText.set_current_locale(locale.to_s.gsub('-', '_').to_sym) - end + if defined? Locale + # GetText/Locale already uses default fallbacks ("en-us-custom" => "en-us" => "en-custom" => "en") + # But we want to add them custom fallbacks from I18n (ex: "fr" => "nl" => "en") + # cf. https://github.com/translation/rails/issues/48 + fallback_locales = I18n.fallbacks[I18n.locale].collect { |l| l.to_s.gsub('-', '_').to_sym } + + Locale.set_current(*fallback_locales) end end end -if defined?(GetText) +I18n::Config.prepend I18nConfigExtension + +### +# Monkey-Patch GetText to : +# * Ignore GetText warnings +# * Don't stop code parsing if a file is badly formatted + message +### +if defined? GetText module GetText class POParser < Racc::Parser def initialize diff --git a/lib/translation_io/tasks.rb b/lib/translation_io/tasks.rb index 53551c0..e809a0b 100644 --- a/lib/translation_io/tasks.rb +++ b/lib/translation_io/tasks.rb @@ -50,7 +50,7 @@ def client_ready? true else TranslationIO.info("[Error] Can't configure client. Did you set up the initializer?\n"\ - "Read usage instructions here : http://translation.io/usage") + "Read usage instructions here : https://translation.io/rails/usage") false end end diff --git a/lib/translation_io/yaml_conversion.rb b/lib/translation_io/yaml_conversion.rb index f667745..34c9c4f 100644 --- a/lib/translation_io/yaml_conversion.rb +++ b/lib/translation_io/yaml_conversion.rb @@ -26,7 +26,7 @@ def get_flat_translations_for_yaml_file(file_path) end def get_flat_translations_for_yaml_data(yaml_data) - translations = YAML::load(yaml_data) + translations = TranslationIO.yaml_load(yaml_data) if translations return FlatHash.to_flat_hash(translations) @@ -35,9 +35,14 @@ def get_flat_translations_for_yaml_data(yaml_data) end end - def get_yaml_data_from_flat_translations(flat_translations) - remove_empty_keys = TranslationIO.config.yaml_remove_empty_keys - translations = FlatHash.to_hash(flat_translations, remove_empty_keys) + def get_yaml_data_from_flat_translations(flat_translations, force_keep_empty_keys: false) + if force_keep_empty_keys + remove_empty_keys = false + else + remove_empty_keys = TranslationIO.config.yaml_remove_empty_keys + end + + translations = FlatHash.to_hash(flat_translations, remove_empty_keys) if TranslationIO.config.yaml_line_width data = translations.to_yaml(:line_width => TranslationIO.config.yaml_line_width) diff --git a/lib/translation_io/yaml_entry.rb b/lib/translation_io/yaml_entry.rb index 5c99b34..873b239 100644 --- a/lib/translation_io/yaml_entry.rb +++ b/lib/translation_io/yaml_entry.rb @@ -39,9 +39,7 @@ def localization?(key, value) end def localization_prefix?(key) - localization_key_prefixes.any? do |prefix| - key_without_locale(key).match(/^#{Regexp.escape(prefix)}\b/) != nil - end + localization_key_prefixes.any? { |prefix| key_without_locale(key).match(/^#{Regexp.escape(prefix)}\b/) != nil } end private diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 4fe2930..9d45724 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -6,7 +6,13 @@ require 'translation' RSpec.configure do |config| - config.expect_with(:rspec) { |c| c.syntax = [:should, :expect] } + config.expect_with :rspec do |c| + c.syntax = [:should, :expect] + end + + config.mock_with :rspec do |c| + c.syntax = [:should, :expect] + end config.before :each do TranslationIO.configure do |config| diff --git a/spec/support/data/en.yml b/spec/support/data/en.yml index f26b417..2e16054 100644 --- a/spec/support/data/en.yml +++ b/spec/support/data/en.yml @@ -1,6 +1,6 @@ en: hello: "Hello world" - main: + main: &main_alias menu: stuff: "This is stuff" bye: "Good bye world" @@ -21,3 +21,5 @@ en: - sub_test: hello: hello no: "N°" + other_main: + *main_alias diff --git a/spec/support/rails_app/app/views/layouts/application.html.erb b/spec/support/rails_app/app/views/layouts/application.html.erb index 646d31e..7f1172a 100644 --- a/spec/support/rails_app/app/views/layouts/application.html.erb +++ b/spec/support/rails_app/app/views/layouts/application.html.erb @@ -1,2 +1,9 @@ <%= _("Let's get ready to rumble!") %> -<%= p_("contexte", "salut") %> +<%= p_("Eminem", "I'm Slim Shady, yes, I'm the real Shady") %> + +