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 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
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ # `require "i18n-keyless-rails"` (the gem name) and `require "i18n_keyless"` are the same thing.
4
+ require_relative "i18n_keyless"