i18n-keyless-rails 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.md +21 -0
- data/README.md +237 -0
- data/SKILL.md +106 -0
- data/lib/i18n-keyless-rails.rb +4 -0
- data/lib/i18n_keyless/api_client.rb +355 -0
- data/lib/i18n_keyless/backend.rb +52 -0
- data/lib/i18n_keyless/config.rb +125 -0
- data/lib/i18n_keyless/dictionary_store.rb +151 -0
- data/lib/i18n_keyless/helper.rb +21 -0
- data/lib/i18n_keyless/locale.rb +103 -0
- data/lib/i18n_keyless/middleware.rb +30 -0
- data/lib/i18n_keyless/miss.rb +44 -0
- data/lib/i18n_keyless/railtie.rb +31 -0
- data/lib/i18n_keyless/translate_missing_keys_job.rb +11 -0
- data/lib/i18n_keyless/translator.rb +300 -0
- data/lib/i18n_keyless/version.rb +8 -0
- data/lib/i18n_keyless.rb +129 -0
- data/llms.txt +101 -0
- metadata +95 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 633d0e2996fa5a44ee8797a9ae88800193649f7044e6656c9fcf72d0ea91bb96
|
|
4
|
+
data.tar.gz: 00074f4d1e5cd3e49ec2457f709174aff440b78c4be7e11a18ee5ec842a09b5c
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: a565de147537f2032e4b34f6895dc9700ef8f778eee41d08ff67761fece8887d574a9c15c90b331ac31ac1fc780994906211376b6273e5c0bba444135e7aeffc
|
|
7
|
+
data.tar.gz: 95a91cd608ba7f4097db861ab0d74c89aed212047ddd6b86ec23d5cc8cd67bb00ad0c3b4d47825e9a711f30473f81bde53cc65d506dbfac5738c29620b44261e
|
data/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Arnaud Ambroselli
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# i18n-keyless for Ruby on Rails
|
|
2
|
+
|
|
3
|
+
Keyless translations for Rails. Write the source string where a key would go,
|
|
4
|
+
`t('Welcome to our app')`, and it resolves through the i18n-keyless API: a missing string is
|
|
5
|
+
translated by AI once, for every language, and served from `Rails.cache` from then on. No more
|
|
6
|
+
`config/locales/*.yml` files to maintain by hand.
|
|
7
|
+
|
|
8
|
+
## Quick start
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
bundle add i18n-keyless-rails
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```dotenv
|
|
15
|
+
# .env
|
|
16
|
+
I18N_KEYLESS_API_KEY=your-key # https://i18n-keyless.com/#get-api-key
|
|
17
|
+
I18N_KEYLESS_LANGUAGES=en,fr,es # every language your app serves
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```erb
|
|
21
|
+
<h1><%= t('Welcome to our app') %></h1>
|
|
22
|
+
<p><%= t('Welcome %{name}', name: current_user.name) %></p>
|
|
23
|
+
<%= i18nk('8 heures', context: 'duration') %>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Done. `t('Welcome to our app')` returns the translation for `I18n.locale`.
|
|
27
|
+
|
|
28
|
+
That is the whole integration: one gem, two `.env` lines, and your source strings where the
|
|
29
|
+
keys used to be. Rails 7.0 to 8.x, Ruby >= 3.1.
|
|
30
|
+
|
|
31
|
+
## How it works
|
|
32
|
+
|
|
33
|
+
1. Nothing happens for the primary language: the source string is the translation.
|
|
34
|
+
2. The gem is an `I18n` backend chained **after** your own: `I18n::Backend::Chain.new(your
|
|
35
|
+
backend, I18nKeyless::Backend.new)`. A key found in `config/locales/*.yml` wins and never
|
|
36
|
+
reaches the API. The first `t()` call that misses in another locale loads that locale's
|
|
37
|
+
dictionary from `Rails.cache`. On the very first request ever for that language, it is
|
|
38
|
+
fetched from the API (`GET /translate/{lang}`) and stored.
|
|
39
|
+
3. A string the dictionary does not have is returned as-is (the source text, with `%{name}`
|
|
40
|
+
placeholders replaced by I18n), and recorded. The request is never blocked by a miss.
|
|
41
|
+
4. After the response is sent (a Rack middleware closes the body), the recorded strings are
|
|
42
|
+
sent to `POST /translate` with the configured `languages` (plus the primary), deduplicated
|
|
43
|
+
by key and context, at most 30 in flight at once, and the answers are merged into the
|
|
44
|
+
cache. The next request has them. Without `I18N_KEYLESS_LANGUAGES` nothing is sent: the
|
|
45
|
+
API stores the list it receives as the project's languages, so an incomplete list would
|
|
46
|
+
damage every other client on the same key. One warning is logged per process.
|
|
47
|
+
5. A dictionary older than `cache_ttl` is still served, then revalidated after the response
|
|
48
|
+
with its `ETag` (`If-None-Match`): an unchanged dictionary costs a bodyless `304`.
|
|
49
|
+
6. Usage analytics, like the node SDK: the date each string was last served is recorded and
|
|
50
|
+
the cumulative map is sent to `POST /translate/last-used-translations` after the response,
|
|
51
|
+
at most once every 10 s across all your processes (a lock in the cache). It feeds the
|
|
52
|
+
dashboard's "last used" column so unused strings can be pruned. A failed POST never
|
|
53
|
+
affects the response; the map waits in the cache for a later request.
|
|
54
|
+
`I18N_KEYLESS_USAGE=false` switches it off.
|
|
55
|
+
|
|
56
|
+
Every API call has a 10 s timeout and is retried twice with backoff (500 ms, 1500 ms) on a
|
|
57
|
+
network error, a timeout, a `429` or a `5xx`. Any other `4xx` is not retried. Nothing ever
|
|
58
|
+
raises: on failure the page shows the source text, and the failure is remembered for 60 s so
|
|
59
|
+
the API is not hammered.
|
|
60
|
+
|
|
61
|
+
## Which strings are keyless
|
|
62
|
+
|
|
63
|
+
Rails keys and source strings share one `t()`. The rule (`I18nKeyless.keyless_key?`):
|
|
64
|
+
|
|
65
|
+
- a **Rails key** is a Symbol, a key with a `scope:`, or a lowercase identifier path:
|
|
66
|
+
`t(:hello)`, `t('hello')`, `t('users.index.title')`, `t('.title')` (lazy lookup),
|
|
67
|
+
`t('activerecord.errors.models.user')`. Those go to your YAML files, as before, and are
|
|
68
|
+
never sent to the API. A missing one is still a missing translation.
|
|
69
|
+
- everything else is a **source string**: a space, an uppercase letter or a punctuation mark
|
|
70
|
+
is enough. `t('Welcome to our app')`, `t('Bonjour')`, `t('Bonjour. Ça va ?')` (a dot in a
|
|
71
|
+
source string is not a separator).
|
|
72
|
+
|
|
73
|
+
For a lowercase one-word source string (`close`, `cancel`) use the helper, which never
|
|
74
|
+
treats its argument as a key: `i18nk('close')`. The rule is `config.rails_key_pattern`
|
|
75
|
+
(default `/\A[a-z0-9_]+(\.[a-z0-9_]+)*\z/`); set it to `nil` to make every string keyless.
|
|
76
|
+
|
|
77
|
+
## Configuration
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
# config/initializers/i18n_keyless.rb (optional: every value has an .env counterpart)
|
|
81
|
+
I18nKeyless.configure do |c|
|
|
82
|
+
c.api_key = ENV.fetch("I18N_KEYLESS_API_KEY")
|
|
83
|
+
c.languages = %w[en fr es]
|
|
84
|
+
c.primary = "en"
|
|
85
|
+
c.cache = Rails.cache
|
|
86
|
+
end
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
| Config | Env | Default | What it is |
|
|
90
|
+
| --- | --- | --- | --- |
|
|
91
|
+
| `enabled` | `I18N_KEYLESS_ENABLED` | `true` | `false` switches the gem off: Rails behaves as without it. |
|
|
92
|
+
| `api_key` | `I18N_KEYLESS_API_KEY` | none | Your project's key. Without it the gem stays inactive. |
|
|
93
|
+
| `api_url` | `I18N_KEYLESS_API_URL` | `https://api.i18n-keyless.com` | The official service, or your own backend / proxy that speaks the same wire format. |
|
|
94
|
+
| `primary` | `I18N_KEYLESS_PRIMARY_LANG` | `I18n.default_locale` | The language your source strings are written in (`en`, `fr`, `pt-BR`, `zh-Hans`...). A Rails locale (`pt_BR`, `:"zh-CN"`) is accepted. |
|
|
95
|
+
| `languages` | `I18N_KEYLESS_LANGUAGES` | empty | Required for translation. Every language your app serves, comma separated (`en,fr,es`) or an array. A new string is translated into all of them at once, and the API stores the list as the project's languages. When empty, missing strings are served as their source text and never sent; one warning is logged. |
|
|
96
|
+
| `namespace` | `I18N_KEYLESS_NAMESPACE` | `default` | The i18n-keyless namespace of the `t()` strings. |
|
|
97
|
+
| `cache` | | `Rails.cache` | Any `ActiveSupport::Cache::Store` (memory, file, Redis, Memcached, the database). Outside Rails: a `MemoryStore`. |
|
|
98
|
+
| `cache_ttl` | `I18N_KEYLESS_CACHE_TTL` | `3600` | Seconds a dictionary is served without asking the API. After that it is revalidated with its `ETag` after the response. |
|
|
99
|
+
| `cache_prefix` | | `i18n-keyless` | Prefix of every cache key the gem writes. |
|
|
100
|
+
| `timeout` | | `10` | HTTP timeout in seconds, per attempt. |
|
|
101
|
+
| `retry` | | `[500, 1500]` | Backoff in milliseconds between retries: two entries, two retries. |
|
|
102
|
+
| `concurrency` | | `30` | Maximum `POST /translate` requests in flight at once. |
|
|
103
|
+
| `usage` | `I18N_KEYLESS_USAGE` | `true` | Usage analytics: the date each string was last served, POSTed at most once every 10 s. `false` disables it. |
|
|
104
|
+
| `queue` | `I18N_KEYLESS_QUEUE` | none | An ActiveJob queue name: the misses of a request are enqueued as one `I18nKeyless::TranslateMissingKeysJob` instead of being sent after the response. |
|
|
105
|
+
| `logger` | | `Rails.logger` | Where the `i18n-keyless:` warnings go. |
|
|
106
|
+
| `rails_key_pattern` | | see above | The Rails-key rule. `nil`: every string is keyless. |
|
|
107
|
+
|
|
108
|
+
`I18N_KEYLESS_LANGUAGES` is the list the API translates a new string into, and the list it
|
|
109
|
+
stores as your project's languages (it replaces the previous one, like the `supported` list
|
|
110
|
+
the JavaScript SDKs send). A locale that is not in the list is served, but never translated.
|
|
111
|
+
|
|
112
|
+
Rails validates `I18n.locale` against `I18n.available_locales`, which it derives from the
|
|
113
|
+
YAML files present. An app that serves a locale with no YAML file lists it explicitly:
|
|
114
|
+
`config.i18n.available_locales = %i[en fr es]`.
|
|
115
|
+
|
|
116
|
+
## Locales
|
|
117
|
+
|
|
118
|
+
Rails locales are mapped onto the 48 i18n-keyless codes: `:fr` is `fr`, `:"pt-BR"` and
|
|
119
|
+
`pt_BR` are `pt-BR`, `zh_CN` is `zh-Hans`, `zh_TW` is `zh-Hant`, `en_US` is `en`, `fr_FR` is
|
|
120
|
+
`fr`, `es-419` is `es-MX`. A locale no code matches (`xx`) is left alone: `t()` returns the
|
|
121
|
+
source text and nothing is sent. The full list is `I18nKeyless::Locale::AVAILABLE_LANGS`.
|
|
122
|
+
|
|
123
|
+
## Context: one string, two meanings
|
|
124
|
+
|
|
125
|
+
`8 heures` is "8 hours" in a duration and "8 AM" on a clock. Pass a `context`, through `t()`
|
|
126
|
+
or through the `i18nk` helper (mixed into views, controllers, mailers and jobs):
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
t('8 heures', context: 'duration') # "8 hours"
|
|
130
|
+
i18nk('8 heures', context: 'clock time') # "8 AM"
|
|
131
|
+
i18nk('Bienvenue %{name}', name: user.name, context: 'greeting')
|
|
132
|
+
i18nk('Payer', namespace: 'checkout') # an i18n-keyless namespace
|
|
133
|
+
I18nKeyless.t('Payer', namespace: 'checkout') # the same, outside a view
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The string is stored as `key__context`, exactly like the JavaScript SDKs, so the dashboard
|
|
137
|
+
and every other SDK see the same entry.
|
|
138
|
+
|
|
139
|
+
Placeholders are I18n's job: write `%{name}` in the source string, pass `name:` as an
|
|
140
|
+
option, and I18n replaces it after the translation. The SDKs' `replace` option has no
|
|
141
|
+
equivalent here because I18n already does it.
|
|
142
|
+
|
|
143
|
+
`i18nk(text, values = nil, context: nil, locale: nil, namespace: nil, **values)`.
|
|
144
|
+
|
|
145
|
+
## Cache
|
|
146
|
+
|
|
147
|
+
Dictionaries live in `Rails.cache` under `i18n-keyless:{key-hash}:dict:{namespace}:{lang}`
|
|
148
|
+
(the same layout as the Laravel port), stored without expiry. `cache_ttl` is not their
|
|
149
|
+
lifetime: it is how long they are served without revalidation, so a stale dictionary is
|
|
150
|
+
never thrown away before the API has confirmed a newer one. To force a refetch,
|
|
151
|
+
`Rails.cache.clear` (or delete those keys). `I18n.reload!` forgets the dictionaries a
|
|
152
|
+
process holds and re-reads the cache.
|
|
153
|
+
|
|
154
|
+
Every process (Puma workers, Sidekiq workers) keeps the loaded dictionaries between
|
|
155
|
+
requests; they are refreshed when a revalidation brings a new dictionary, and after each
|
|
156
|
+
`POST /translate`. A dashboard edit reaches a running process at the next revalidation, at
|
|
157
|
+
most `cache_ttl` seconds later. The miss guard and the usage lock live in the cache too:
|
|
158
|
+
with a `:memory_store` they are per process, so use a shared store (Redis, Memcached, the
|
|
159
|
+
database, the file store) in production, as Rails recommends anyway.
|
|
160
|
+
|
|
161
|
+
## Limitations
|
|
162
|
+
|
|
163
|
+
- **Plurals.** `t('Il y a %{count} pommes', count: n)` sends one source string and I18n
|
|
164
|
+
replaces `%{count}`; the pluralisation rules of YAML (`one:` / `other:`) do not apply to a
|
|
165
|
+
source string. Prefer one `i18nk()` per plural form with a `context` (`context: 'one'`,
|
|
166
|
+
`context: 'other'`), or keep plurals in YAML files, which Rails keeps loading.
|
|
167
|
+
- **HTML safety.** A source string is a plain string: ERB escapes it like any `t()` result.
|
|
168
|
+
The `_html` key suffix convention does not apply to a source string.
|
|
169
|
+
- **One key space per locale.** The lines of every namespace are looked up by the same `t()`;
|
|
170
|
+
pass `namespace:` on the call, or set `I18N_KEYLESS_NAMESPACE` for the whole app.
|
|
171
|
+
- **Misses are deduplicated by key AND context** (the SDK queue ignores the context), so two
|
|
172
|
+
contexts of one string on one page are both translated after that page.
|
|
173
|
+
- **Usage analytics** count the strings served by this gem only. A string served from a
|
|
174
|
+
YAML file never reaches it, so it is not counted.
|
|
175
|
+
- **Threads.** One translator per process, shared by every thread. The misses of concurrent
|
|
176
|
+
requests are flushed together by whichever response finishes first (the cache guard keeps
|
|
177
|
+
each one from being POSTed twice).
|
|
178
|
+
|
|
179
|
+
## Outside Rails
|
|
180
|
+
|
|
181
|
+
The gem needs only `i18n` and `activesupport`. In a plain Ruby, Sinatra or Hanami app:
|
|
182
|
+
|
|
183
|
+
```ruby
|
|
184
|
+
require "i18n_keyless"
|
|
185
|
+
|
|
186
|
+
I18nKeyless.configure do |c|
|
|
187
|
+
c.api_key = ENV.fetch("I18N_KEYLESS_API_KEY")
|
|
188
|
+
c.languages = %w[en fr]
|
|
189
|
+
c.cache = ActiveSupport::Cache::MemoryStore.new # or any store
|
|
190
|
+
end
|
|
191
|
+
I18nKeyless.install! # chains the backend after I18n.backend
|
|
192
|
+
use I18nKeyless::Middleware # Rack: flush after each response
|
|
193
|
+
I18nKeyless.t("Bonjour", locale: :en) # without the helper
|
|
194
|
+
I18nKeyless.flush # a script: send the misses before exit
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Self-hosted backend or proxy
|
|
198
|
+
|
|
199
|
+
Point `I18N_KEYLESS_API_URL` at a server that speaks the three-route wire format
|
|
200
|
+
(`GET /translate/{lang}`, `POST /translate`, `POST /translate/last-used-translations`).
|
|
201
|
+
See https://docs.i18n-keyless.com/docs/guides/proxy-mode. Every request carries
|
|
202
|
+
`Authorization: Bearer <api_key>`, `Version: 3.5.0` (the wire dialect: v3 language codes) and
|
|
203
|
+
`sdk: rails` (a server label, counted like `node`: by its connection, not by a device id).
|
|
204
|
+
|
|
205
|
+
## Publishing to RubyGems
|
|
206
|
+
|
|
207
|
+
This directory lives inside the `i18n-keyless` monorepo and is not an npm workspace. The
|
|
208
|
+
gem builds from here:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
cd ports/rails
|
|
212
|
+
gem build i18n-keyless-rails.gemspec
|
|
213
|
+
gem push i18n-keyless-rails-3.5.0.gem
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
The version number follows the SDKs (`3.x`, `lib/i18n_keyless/version.rb`, written by
|
|
217
|
+
`scripts/set-version.mjs`).
|
|
218
|
+
|
|
219
|
+
## Development
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
cd ports/rails
|
|
223
|
+
bundle install
|
|
224
|
+
bundle exec rake test
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Tests run on minitest with WebMock: no network, no key. `test/conformance/vectors_test.rb`
|
|
228
|
+
replays the monorepo's shared protocol vectors (`conformance/vectors/*.json`): language
|
|
229
|
+
codes, locale resolution, storage key, namespace resolution, retry decisions, backoff
|
|
230
|
+
scenarios, dictionary and translate requests and responses, usage requests. The test is
|
|
231
|
+
skipped when the vectors directory is absent (standalone checkout).
|
|
232
|
+
`test/integration/railtie_test.rb` boots a one-file Rails application and drives a request
|
|
233
|
+
through the middleware.
|
|
234
|
+
|
|
235
|
+
One deliberate difference from the JavaScript SDKs: misses are deduplicated by key AND
|
|
236
|
+
context (the SDK queue ignores the context). Usage analytics follow the node SDK
|
|
237
|
+
(`sdk: rails` is a server label with the `node` rules).
|
data/SKILL.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: i18n-keyless-rails
|
|
3
|
+
description: Install and use i18n-keyless in a Ruby on Rails application. t('Welcome to our app') (the source string where a key would go) resolves through the i18n-keyless API with one gem and two .env lines. Use when adding, configuring or debugging translations / localization / multi-language support in a Rails 7 or 8 project, or when the project already depends on `i18n-keyless-rails`.
|
|
4
|
+
license: MIT
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# i18n-keyless for Ruby on Rails
|
|
8
|
+
|
|
9
|
+
Rails' `t()` takes a key and looks it up in `config/locales/*.yml`. This gem chains an I18n
|
|
10
|
+
backend after the application's: a `t('Welcome to our app')` call whose argument is a source
|
|
11
|
+
string, not a key, is translated by AI once, for every language, cached in `Rails.cache`,
|
|
12
|
+
and served from there. No YAML to maintain for those strings.
|
|
13
|
+
|
|
14
|
+
**Version covered: `i18n-keyless-rails` 3.x, Rails 7.0 to 8.x, Ruby >= 3.1.**
|
|
15
|
+
|
|
16
|
+
## Install in one step
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
bundle add i18n-keyless-rails
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```dotenv
|
|
23
|
+
I18N_KEYLESS_API_KEY=... # required: https://i18n-keyless.com/#get-api-key
|
|
24
|
+
I18N_KEYLESS_LANGUAGES=en,fr,es # required for translation: every language the app serves
|
|
25
|
+
I18N_KEYLESS_PRIMARY_LANG=en # the language the source strings are written in (default: I18n.default_locale)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The Railtie is auto-loaded. No code change: `t()`, `I18n.t`, `translate()` keep their
|
|
29
|
+
signatures; the `i18nk` helper is mixed into views, controllers, mailers and jobs.
|
|
30
|
+
|
|
31
|
+
## Rules
|
|
32
|
+
|
|
33
|
+
- Source strings are written in the primary language. `t('Bonjour')` in a French-first app,
|
|
34
|
+
`t('Hello')` in an English-first one. Never invent a key name for a keyless string.
|
|
35
|
+
- What is a Rails key, left to YAML and never sent: a Symbol (`t(:hello)`), a `scope:`, a
|
|
36
|
+
lazy lookup (`t('.title')`), or a lowercase identifier path (`t('hello')`,
|
|
37
|
+
`t('users.index.title')`). What is a source string: anything with a space, an uppercase
|
|
38
|
+
letter or punctuation. For a lowercase one-word source string, use `i18nk('close')`.
|
|
39
|
+
The rule is `config.rails_key_pattern`.
|
|
40
|
+
- Placeholders are I18n's: `t('Bienvenue %{name}', name: name)`. Do not use the SDKs'
|
|
41
|
+
`replace` option or `{{name}}` syntax.
|
|
42
|
+
- Ambiguous strings take a context: `t('8 heures', context: 'duration')` or
|
|
43
|
+
`i18nk('8 heures', context: 'duration')`. Stored as `8 heures__duration`, the same entry
|
|
44
|
+
the other SDKs use. `namespace:` travels the same way.
|
|
45
|
+
- Plurals: YAML `one:` / `other:` rules do not apply to a source string. Use one `i18nk()`
|
|
46
|
+
per form with a `context`, or a YAML file.
|
|
47
|
+
- The current locale is `I18n.locale`. Switching it (`I18n.locale = :fr`, `around_action`,
|
|
48
|
+
`?locale=`) is all that is needed; the gem loads the locale on its first miss. Rails
|
|
49
|
+
locales are mapped: `pt_BR` is `pt-BR`, `zh_CN` is `zh-Hans`. List the served locales in
|
|
50
|
+
`config.i18n.available_locales` when they have no YAML file.
|
|
51
|
+
- Always set `I18N_KEYLESS_LANGUAGES` to the full list the app serves. It is what a new
|
|
52
|
+
string is translated into, and the API stores it as the project's language list (it
|
|
53
|
+
replaces the previous one). Without it, missing strings are never sent: they stay as
|
|
54
|
+
source text and one warning `I18N_KEYLESS_LANGUAGES is required` is logged per process.
|
|
55
|
+
- Nothing blocks a request except the very first fetch of a language's dictionary. Misses
|
|
56
|
+
are sent after the response (a Rack middleware) or, with `I18N_KEYLESS_QUEUE=<queue>`, as
|
|
57
|
+
an `I18nKeyless::TranslateMissingKeysJob`.
|
|
58
|
+
- Never raises. A failed API call shows the source text and is retried at the next request.
|
|
59
|
+
- Every request carries `Authorization: Bearer`, `Version: 3.5.0` and `sdk: rails` (a
|
|
60
|
+
server label: counted by connection, no device id, usage analytics like the node SDK).
|
|
61
|
+
- A line in `config/locales/{locale}.yml` wins over the API. Delete it to let the API serve it.
|
|
62
|
+
- Usage analytics are on by default, like the node SDK: one `POST
|
|
63
|
+
/translate/last-used-translations` at most every 10 s, after the response, never blocking.
|
|
64
|
+
`I18N_KEYLESS_USAGE=false` turns them off.
|
|
65
|
+
- Use a shared cache store in production (Redis, Memcached, the database): the miss guard
|
|
66
|
+
and the usage lock live in `Rails.cache`, and a `:memory_store` is per process.
|
|
67
|
+
- If the app sets `I18n.backend` itself after boot, chain again with `I18nKeyless.install!`.
|
|
68
|
+
- A source string is capped at 2000 characters (`context` and `namespace` at 200). Long-form
|
|
69
|
+
content is allowed, but a blog post is one translation **per Markdown block** of about 1000
|
|
70
|
+
characters: keep the Markdown inside each block, give every block of the document the same
|
|
71
|
+
`context` — one very short summary of it — and one `namespace` per document.
|
|
72
|
+
https://docs.i18n-keyless.com/docs/guides/long-form-content
|
|
73
|
+
|
|
74
|
+
## Configuration
|
|
75
|
+
|
|
76
|
+
`I18nKeyless.configure { |c| ... }` in an initializer, or the environment. Keys: `enabled`,
|
|
77
|
+
`api_key`, `api_url`, `primary`, `languages`, `namespace`, `cache`, `cache_ttl`,
|
|
78
|
+
`cache_prefix`, `timeout` (10 s), `retry` (`[500, 1500]` ms), `concurrency` (30), `usage`
|
|
79
|
+
(true), `queue`, `logger`, `rails_key_pattern`.
|
|
80
|
+
Env: `I18N_KEYLESS_ENABLED`, `I18N_KEYLESS_API_KEY`, `I18N_KEYLESS_API_URL`,
|
|
81
|
+
`I18N_KEYLESS_PRIMARY_LANG`, `I18N_KEYLESS_LANGUAGES`, `I18N_KEYLESS_NAMESPACE`,
|
|
82
|
+
`I18N_KEYLESS_CACHE_TTL`, `I18N_KEYLESS_USAGE`, `I18N_KEYLESS_QUEUE`.
|
|
83
|
+
|
|
84
|
+
## Debug
|
|
85
|
+
|
|
86
|
+
- `t()` returns the source text in a non-primary locale: check `I18N_KEYLESS_API_KEY` and
|
|
87
|
+
`I18N_KEYLESS_LANGUAGES`, that the locale maps to a supported code and is in the list, and
|
|
88
|
+
the log for lines starting with `i18n-keyless:`.
|
|
89
|
+
- `t()` returns `Translation missing`: the argument matched the Rails-key rule (lowercase
|
|
90
|
+
identifier). Use `i18nk()` or capitalise the source string.
|
|
91
|
+
- A translation does not update after a dashboard edit: the dictionary is served for
|
|
92
|
+
`cache_ttl` seconds before revalidation. `Rails.cache.clear` forces a refetch.
|
|
93
|
+
- A string is POSTed on every request: the miss guard lives in the cache; make sure the
|
|
94
|
+
cache store is shared between processes (not `:memory_store` or `:null_store`).
|
|
95
|
+
|
|
96
|
+
## Offline try-out
|
|
97
|
+
|
|
98
|
+
Run `examples/_mock-server` (`node server.mjs`, port 8787) and set
|
|
99
|
+
`I18N_KEYLESS_API_URL=http://localhost:8787`, `I18N_KEYLESS_API_KEY=demo`,
|
|
100
|
+
`I18N_KEYLESS_PRIMARY_LANG=fr`. See `examples/rails/README.md`.
|
|
101
|
+
|
|
102
|
+
## Go deeper
|
|
103
|
+
|
|
104
|
+
- Gem README: `ports/rails/README.md`
|
|
105
|
+
- The whole i18n-keyless documentation as one file: https://docs.i18n-keyless.com/llms.txt
|
|
106
|
+
- Dashboard: https://i18n-keyless.com/dashboard
|