smartloc 2.0.10 → 2.0.11
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.
- package/AGENTS.md +69 -0
- package/README.md +55 -206
- package/docs/api.md +112 -0
- package/docs/cli.md +85 -0
- package/docs/express.md +36 -0
- package/docs/graphql.md +37 -0
- package/docs/react.md +52 -0
- package/docs/storage.md +57 -0
- package/package.json +12 -4
- package/skills/smartloc/SKILL.md +27 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# smartloc: instructions for AI coding agents
|
|
2
|
+
|
|
3
|
+
This file is for agents writing code that **uses** the `smartloc` package. It is shipped in the npm tarball, so it is readable from `node_modules/smartloc/AGENTS.md`. Detailed docs are in `node_modules/smartloc/docs/`.
|
|
4
|
+
|
|
5
|
+
## What it is
|
|
6
|
+
|
|
7
|
+
i18n via tagged template strings. `loc`...`` returns a `LocStr`: an immutable object that is translated lazily, when `.toString()`/`JSON.stringify` run inside a locale context, or when React renders it. Server (Express, Apollo GraphQL) and browser (React 19) are supported.
|
|
8
|
+
|
|
9
|
+
## Entry points
|
|
10
|
+
|
|
11
|
+
| Import | Exports |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `smartloc` | `loc`, `setDefaultLocale`, `addLocale`, `changeLocale`, `removeLocale`, `clearLocales`, `getDefaultLocale`, `withLocales`, `singleLoc`, `multiLoc`, `joinArray`, `useAsString`, `isLocStr`, `loadJsonLocale`, `toJsonStorable`, `toLocalizable`, `jsonParseLocalized`, `translateObject`, `translateInContext`, `withSerializationContext`, types `LocStr`, `TranslationOf<T>`, `StorableOf<T>` |
|
|
14
|
+
| `smartloc/express` | default export: middleware factory `(options?) => RequestHandler` |
|
|
15
|
+
| `smartloc/graphql` | `GLocString`, `localizeSchema`, `localizedContext`, `localizedContextObject` |
|
|
16
|
+
| `smartloc/node` | `loadAllLocales(dir, merge?)`, `loadLocale(file, merge?)` (Node only, uses fs) |
|
|
17
|
+
|
|
18
|
+
## Rules
|
|
19
|
+
|
|
20
|
+
1. **Never build translated strings by hand.** Return `loc('id')`text ${value}`` and let the boundary translate. Do not call `.toString()` in business code unless you need a string for a log or a non-smartloc API.
|
|
21
|
+
2. **Always give an explicit ID** for anything a user sees: `loc('area.name')`...``. Auto IDs (``loc`...``) change when the text changes and lose translations. Use `.` to namespace IDs; the CLI groups files by the prefix before the first dot.
|
|
22
|
+
3. **Call `setDefaultLocale('xx')` once at startup**, before any translation. Use a literal string: the CLI reads it from the source.
|
|
23
|
+
4. **Placeholders**: strings, numbers, `Date`, other `LocStr`, React nodes. Numbers and dates are locale-formatted automatically, do not pre-format them. Do not concatenate `LocStr` with `+`; nest them as placeholders or use `joinArray`.
|
|
24
|
+
5. **Plurals**: `loc.plural(count)('id')`${count} items``. Count 1 uses the `singular` form from translation files. The default language singular is only in the default-locale file, generated with `--defaultLocale`.
|
|
25
|
+
6. **Express**: `app.use(smartloc())` before routes. `res.json()`, `res.jsonp()`, `res.send()` are then translated from `Accept-Language`. Pass `{ waitFor: promise }` when locales load asynchronously.
|
|
26
|
+
7. **GraphQL**: field type `GLocString`, `schema = localizeSchema(schema)`, `context: localizedContext(fn)`. `JSON`/`JSONObject` scalar fields are translated recursively too.
|
|
27
|
+
8. **React**: render a `LocStr` directly as a child (`<h1>{loc`...`}</h1>`). For string-only props (`placeholder`, `title`, `aria-*`) use `useAsString(locStr)` inside the component. Do not call `.toString()` during render: it does not re-render on `changeLocale()`.
|
|
28
|
+
9. **Translation files**: run `npx smartloc collect --format=json --locales=fr,de --defaultLocale=en` (writes `./i18n/<locale>.json`). Do not edit `source` fields; fill `target`. Load with `loadAllLocales('./i18n')` (Node) or `loadJsonLocale(url | object)` (browser). Prefer this over hand-written `addLocale` calls.
|
|
29
|
+
10. **Persisting `LocStr`** (DB, queues): `JSON.stringify(toJsonStorable(value))` to store, `toLocalizable(JSON.parse(text))` to restore. Plain `JSON.stringify` outside a serialization context translates to the default language and loses translatability. `transform()` and `joinArray()` results cannot be stored.
|
|
30
|
+
11. **Cloning**: `LocStr` are frozen objects tagged with a symbol. In deep clones, skip values where `isLocStr(v)` is true.
|
|
31
|
+
12. **Locale IDs**: `'fr'` or `'fr-FR'`, case-insensitive. `fr-FR` also serves requests for `fr` and `fr-CA` when no closer match exists.
|
|
32
|
+
|
|
33
|
+
## Minimal examples
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
// server bootstrap
|
|
37
|
+
import { setDefaultLocale } from 'smartloc';
|
|
38
|
+
import { loadAllLocales } from 'smartloc/node';
|
|
39
|
+
setDefaultLocale('en');
|
|
40
|
+
await loadAllLocales('./i18n');
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
// express
|
|
45
|
+
import smartloc from 'smartloc/express';
|
|
46
|
+
app.use(smartloc());
|
|
47
|
+
app.get('/', (req, res) => res.json({ message: loc('home.hello')`Hello ${req.query.name}` }));
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
// react
|
|
52
|
+
const hint = useAsString(loc('search.hint')`Type to search`);
|
|
53
|
+
return <input placeholder={hint} aria-label={hint} />;
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
// test / one-off translation
|
|
58
|
+
withLocales(['fr'], () => msg.toString());
|
|
59
|
+
translateObject('fr', { title: loc('t')`Hi` });
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Translation file shape (JSON)
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{ "sourceLanguage": "en", "targetLanguage": "fr",
|
|
66
|
+
"resources": { "home": { "hello": { "source": "Hello {0}", "target": "Bonjour {0}" } },
|
|
67
|
+
"cart": { "items": { "source": { "singular": null, "plural": "{0} items" },
|
|
68
|
+
"target": { "singular": "Un article", "plural": "{0} articles" } } } } }
|
|
69
|
+
```
|
package/README.md
CHANGED
|
@@ -1,250 +1,99 @@
|
|
|
1
|
-
#
|
|
1
|
+
# smartloc
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
- you are developping a NodeJS API server which requires internationalization.
|
|
5
|
-
- you find most i18n libraries too complicated for your needs, or requiring a refactoring of your existing architecture
|
|
6
|
-
- you find it painful to propagate the request accepted languages in all your application parts
|
|
3
|
+
Internationalization with tagged template strings, for Node APIs and React apps.
|
|
7
4
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
# Framework support
|
|
5
|
+
```ts
|
|
6
|
+
const msg = loc('greeting')`Hello ${name}!`;
|
|
7
|
+
```
|
|
14
8
|
|
|
15
|
-
|
|
9
|
+
`msg` is not translated yet. It gets translated at the very last moment: when Express sends the response, when GraphQL resolves the field, or when React renders it.
|
|
16
10
|
|
|
17
|
-
|
|
18
|
-
- [Apollo server express](https://github.com/apollographql/apollo-server/tree/master/packages/apollo-server-express) => see [this sample](./samples/graphql/main.ts)
|
|
11
|
+
## Why smartloc?
|
|
19
12
|
|
|
20
|
-
|
|
13
|
+
- **Your code stays readable.** The source text lives in the code, not in a `keys.json` you have to look up.
|
|
14
|
+
- **No language to pass around.** Build your objects as usual. Translation happens at the boundary, based on `Accept-Language` or the current UI locale.
|
|
15
|
+
- **Server and browser.** Same strings, same API, in Express, Apollo/GraphQL and React.
|
|
16
|
+
- **A CLI does the bookkeeping.** It collects strings from your code and keeps your JSON/XLIFF translation files in sync.
|
|
21
17
|
|
|
22
|
-
Install
|
|
18
|
+
## Install
|
|
23
19
|
|
|
24
20
|
```bash
|
|
25
|
-
npm install smartloc
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
Just forget manipulating already translated strings in your code. It is cumbersome, and might leak languages you dont understand in your logs.
|
|
29
|
-
|
|
30
|
-
Smartloc allows you to declare in your code strings like that:
|
|
31
|
-
|
|
32
|
-
```typescript
|
|
33
|
-
// recommanded: Explicitely specify a string unique ID
|
|
34
|
-
const myString = loc('stringUniqueId')`Hello ${name}, how are you today ?`;
|
|
35
|
-
|
|
36
|
-
// If you are not affraid of occasionally losing some translations when changing your code,
|
|
37
|
-
// then you can use this simpler form:
|
|
38
|
-
const myString = loc`Hello ${name}, how are you today ?`;
|
|
39
|
-
// => An ID will be autogenerated based on this string hash
|
|
40
|
-
// => you might lose translations when changing the original string in code.
|
|
21
|
+
npm install smartloc
|
|
41
22
|
```
|
|
42
23
|
|
|
43
|
-
|
|
24
|
+
## Quick start
|
|
44
25
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
// build a translatable string
|
|
48
|
-
const name = 'world';
|
|
49
|
-
const str = loc`Hello ${name}`; // nb: This one will have an auto-generated id.
|
|
26
|
+
```ts
|
|
27
|
+
import { loc, setDefaultLocale, addLocale, withLocales } from 'smartloc';
|
|
50
28
|
|
|
51
|
-
// Just fake loading translations
|
|
52
29
|
setDefaultLocale('en');
|
|
53
|
-
addLocale('fr', {
|
|
54
|
-
[str.id]: 'Bonjour {0}',
|
|
55
|
-
});
|
|
30
|
+
addLocale('fr', { greeting: 'Bonjour {0} !' });
|
|
56
31
|
|
|
57
|
-
|
|
58
|
-
console.log(str.toString()); // => Hello world
|
|
59
|
-
console.log(JSON.stringify({msg: str})); // => {"msg": "Hello world"}
|
|
32
|
+
const msg = loc('greeting')`Hello ${'world'}!`;
|
|
60
33
|
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
34
|
+
msg.toString(); // Hello world!
|
|
35
|
+
withLocales(['fr'], () => msg.toString()); // Bonjour world !
|
|
36
|
+
withLocales(['fr'], () => JSON.stringify({ msg })); // {"msg":"Bonjour world !"}
|
|
64
37
|
```
|
|
65
38
|
|
|
66
|
-
|
|
39
|
+
The ID is optional: ``loc`Hello` `` gets an ID derived from its text. Numbers and dates in placeholders are formatted for the target locale.
|
|
67
40
|
|
|
68
|
-
|
|
69
|
-
The translation will be automatically performed when sending actual json to your user, through a simple middleware (see samples listed in "Framework Support")
|
|
41
|
+
## Use it with
|
|
70
42
|
|
|
43
|
+
### Express
|
|
71
44
|
|
|
72
|
-
|
|
45
|
+
```ts
|
|
46
|
+
import smartloc from 'smartloc/express';
|
|
73
47
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
```typescript
|
|
78
|
-
{
|
|
79
|
-
"scripts": {
|
|
80
|
-
"smartloc": "smartloc collect --format=json --locales=fr-FR,de-DE --defaultLocale=en-US"
|
|
81
|
-
}
|
|
82
|
-
}
|
|
48
|
+
app.use(smartloc());
|
|
49
|
+
app.get('/', (req, res) => res.json({ answer: loc`Hello` }));
|
|
83
50
|
```
|
|
84
51
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
nb: The `--defaultLocale` argument is optional, and will be infered from your code if you explicitly call `setDefaultLocale()` somewhere.
|
|
88
|
-
|
|
89
|
-
## Loading available translations on server boot
|
|
90
|
-
|
|
91
|
-
Before serving any request, you must:
|
|
92
|
-
1) Tell smartloc which is the default locale (the one you wrote your translations in)
|
|
93
|
-
2) Load other locales
|
|
94
|
-
|
|
95
|
-
For 1), this is straightforward: `setDefaultLocale('en-US')`
|
|
52
|
+
`res.json()` and `res.send()` are translated according to the request's `Accept-Language` header. See [docs/express.md](docs/express.md).
|
|
96
53
|
|
|
97
|
-
|
|
54
|
+
### GraphQL (Apollo)
|
|
98
55
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
import {addLocale} from 'smartloc';
|
|
102
|
-
|
|
103
|
-
// you could also pass here an object loaded from your database, or whatever
|
|
104
|
-
addLocale('fr-FR', {
|
|
105
|
-
mySentenceId: 'Une traduite en français',
|
|
106
|
-
});
|
|
56
|
+
```ts
|
|
57
|
+
import { GLocString, localizeSchema, localizedContext } from 'smartloc/graphql';
|
|
107
58
|
```
|
|
108
59
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
```typescript
|
|
112
|
-
import { loadAllLocales } from 'smartloc/node';
|
|
113
|
-
|
|
114
|
-
await loadAllLocales('/path/to/my-translation.json', true);
|
|
115
|
-
```
|
|
60
|
+
Declare fields as `LocalizedString`, wrap your schema and context, and resolvers can return `loc` strings. See [docs/graphql.md](docs/graphql.md).
|
|
116
61
|
|
|
117
|
-
|
|
62
|
+
### React
|
|
118
63
|
|
|
64
|
+
A `loc` string is also a React element. Placeholders can be React nodes.
|
|
119
65
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
```typescript
|
|
123
|
-
import { loadAllLocales } from 'smartloc/node';
|
|
124
|
-
|
|
125
|
-
await loadAllLocales('/path/to/dir/to/scan', true);
|
|
66
|
+
```tsx
|
|
67
|
+
<h1>{loc('welcome')`Welcome ${<b>{user.name}</b>}`}</h1>
|
|
126
68
|
```
|
|
127
69
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
## Supported formats
|
|
134
|
-
|
|
135
|
-
Smartloc cli implements two translation format through the `--format` argument
|
|
136
|
-
|
|
137
|
-
- `--format=json` : JSON translation files
|
|
138
|
-
- `--format=xliff` : XLIFF translation files
|
|
139
|
-
|
|
140
|
-
nb: Smartloc is grouping your translation IDs by category, detected by the first "." in your ID.
|
|
70
|
+
`changeLocale('fr')` re-renders every rendered string. Use `useAsString()` where a plain string is needed. See [docs/react.md](docs/react.md).
|
|
141
71
|
|
|
142
|
-
|
|
72
|
+
## Translate your app
|
|
143
73
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
### Smartloc
|
|
147
|
-
The default one which is returned when using the `loc` tag:
|
|
148
|
-
```typescript
|
|
149
|
-
return loc`Hello`;
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
### MultiLoc
|
|
153
|
-
If you wish to declare all translations directly in your code:
|
|
154
|
-
```typescript
|
|
155
|
-
return new MultiLoc({
|
|
156
|
-
en: 'Hello',
|
|
157
|
-
fr: 'Bonjour',
|
|
158
|
-
});
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
### SingleLoc
|
|
162
|
-
If you wish to declare a string that is the same in all languages, but which is typed as a `LocStr`:
|
|
163
|
-
```typescript
|
|
164
|
-
return new SingleLoc('Typescript');
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
### TransformedLoc
|
|
168
|
-
Sometimes, you will want to apply transformations to your final string.
|
|
169
|
-
You can do that using the `.transform()` method available on `LocStr`, which will return you a transformed translatable string.
|
|
170
|
-
|
|
171
|
-
```typescript
|
|
172
|
-
return loc`Some string wich can contain html`
|
|
173
|
-
.transform(x => escapeHtml(x)); // apply a transformation
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
## Array of LocStr
|
|
177
|
-
When you have an array of smartloc strings that you want to join, you can use the `LocStringArray` class:
|
|
178
|
-
|
|
179
|
-
```typescript
|
|
180
|
-
const array = new LocStringArray([loc`Hello`, loc`world`]);
|
|
181
|
-
|
|
182
|
-
const str = array.join(' ').transform(x => x + ' !');
|
|
183
|
-
|
|
184
|
-
console.log(str.toString('en')); // => Hello world !
|
|
185
|
-
console.log(str.toString('fr')); // => Bonjour monde !
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
## Serialization in an untranslated form
|
|
189
|
-
|
|
190
|
-
Somtimes, you will want to serialize an arbitrary `LocStr` in its untranslated form (to store a localizable sentence in a DB, for instance).
|
|
191
|
-
|
|
192
|
-
In this case, you can serialize it like that:
|
|
193
|
-
|
|
194
|
-
```typescript
|
|
195
|
-
import {loc, MultiLoc, withSerializationContext} from 'smartloc';
|
|
196
|
-
const sampleObject = {
|
|
197
|
-
reference: loc('stringId')`Hello {world}`,
|
|
198
|
-
multi: new MultiLoc({ en: 'A string', fr: 'Une chaine' }),
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
// serialize
|
|
202
|
-
const serialized = withSerializationContext(() => JSON.stringify(sampleObject));
|
|
203
|
-
|
|
204
|
-
// store ... nb: it will look like {"reference": "i18n/id:stringId", "multi": {"i18n:fr": "A string", "i18n:en": "Une chaine"}}
|
|
205
|
-
storeInDb(serialized);
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
You can deserialize it back to translatable instance later like that:
|
|
209
|
-
|
|
210
|
-
```typescript
|
|
211
|
-
import {toLocalizable} from 'smartloc';
|
|
212
|
-
const obj = loadFromDb();
|
|
213
|
-
|
|
214
|
-
// get back a translatable intance
|
|
215
|
-
const serializable = toLocalizable(obj);
|
|
74
|
+
```bash
|
|
75
|
+
npx smartloc collect --format=json --locales=fr,de --defaultLocale=en
|
|
216
76
|
```
|
|
217
77
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
`toJsonStorable(value)` recursively converts messages in objects and arrays to
|
|
221
|
-
untranslated storage values. Serialize that result with `JSON.stringify` when
|
|
222
|
-
storage requires JSON text. `toLocalizable` accepts the already parsed value:
|
|
78
|
+
This scans your sources and writes one file per locale in `./i18n`. Translate the `target` values, then load them at startup:
|
|
223
79
|
|
|
224
|
-
```
|
|
225
|
-
import {
|
|
80
|
+
```ts
|
|
81
|
+
import { loadAllLocales } from 'smartloc/node'; // server
|
|
82
|
+
await loadAllLocales('./i18n');
|
|
226
83
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const restored = toLocalizable(JSON.parse(stored));
|
|
84
|
+
import { loadJsonLocale } from 'smartloc'; // browser
|
|
85
|
+
await loadJsonLocale('/i18n/fr.json');
|
|
230
86
|
```
|
|
231
87
|
|
|
232
|
-
|
|
233
|
-
recursively converts messages used as interpolation values, in both directions.
|
|
234
|
-
Stored ID references require loaded catalogs, including the default-language
|
|
235
|
-
catalog; source templates are not included. These changes keep the existing
|
|
236
|
-
stored representation.
|
|
88
|
+
Re-run `collect` whenever your code changes. It keeps existing translations and flags the ones whose source text changed. See [docs/cli.md](docs/cli.md).
|
|
237
89
|
|
|
238
|
-
|
|
239
|
-
JSON turns them into ISO strings; `toLocalizable` does not infer dates from those
|
|
240
|
-
strings, including inside message interpolations. React elements and
|
|
241
|
-
joined/transformed messages are not supported storage values.
|
|
90
|
+
## Going further
|
|
242
91
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
-
|
|
246
|
-
-
|
|
92
|
+
- [docs/api.md](docs/api.md): plurals, locale resolution, `multiLoc`, `singleLoc`, `joinArray`, `transform`
|
|
93
|
+
- [docs/storage.md](docs/storage.md): storing untranslated strings in a database
|
|
94
|
+
- [Tutorial on dev.to](https://dev.to/oguimbal/i18n-express-apollo-graphql-server-translation-made-simple-33f5)
|
|
95
|
+
- [AGENTS.md](AGENTS.md): instructions for AI coding agents using this library
|
|
247
96
|
|
|
248
|
-
|
|
97
|
+
## License
|
|
249
98
|
|
|
250
|
-
|
|
99
|
+
MIT
|
package/docs/api.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Core API
|
|
2
|
+
|
|
3
|
+
Everything below is exported from `smartloc`.
|
|
4
|
+
|
|
5
|
+
## Declaring strings
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { loc } from 'smartloc';
|
|
9
|
+
|
|
10
|
+
loc('user.greeting')`Hello ${name}`; // explicit ID (recommended)
|
|
11
|
+
loc`Hello ${name}`; // auto ID: sha1 of the source text
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
- **Explicit IDs** survive text changes. Use a `.` to namespace them (`user.greeting`): the CLI groups translation files by the part before the first dot.
|
|
15
|
+
- **Auto IDs** change whenever the source text changes, so the translation is lost. Fine for throwaway strings.
|
|
16
|
+
- Placeholders can be strings, numbers, `Date`, other `loc` strings, or React nodes.
|
|
17
|
+
- Numbers are formatted with `Intl.NumberFormat` for the target locale.
|
|
18
|
+
- Dates are formatted with `Intl.DateTimeFormat` (day + month + year, plus time when the date has one).
|
|
19
|
+
- Nested `loc` strings are translated in the same locale.
|
|
20
|
+
- In translation files, placeholders appear as `{0}`, `{1}`, ... in source order.
|
|
21
|
+
|
|
22
|
+
### Plurals
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
loc.plural(count)('cart.items')`${count} items`;
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`count === 1` selects the `singular` form, anything else the `plural` form:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
addLocale('fr', {
|
|
32
|
+
'cart.items': { singular: 'Un article', plural: '{0} articles' },
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The code only contains the plural form. The singular form of your default language lives in the default-locale translation file. Generate it with `--defaultLocale` and load it like any other locale.
|
|
37
|
+
|
|
38
|
+
### `loc.nocollect`
|
|
39
|
+
|
|
40
|
+
Same as `loc`, but the CLI ignores it. Useful for formatting helpers.
|
|
41
|
+
|
|
42
|
+
## Locales
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { setDefaultLocale, addLocale, changeLocale, removeLocale, clearLocales } from 'smartloc';
|
|
46
|
+
|
|
47
|
+
setDefaultLocale('en'); // the language you write code in (required)
|
|
48
|
+
addLocale('fr-FR', { id: 'Texte {0}' }); // register translations
|
|
49
|
+
addLocale('fr-FR', { other: '...' }, true); // merge into an existing locale
|
|
50
|
+
changeLocale('fr-FR'); // switch the current locale (UI apps)
|
|
51
|
+
removeLocale('fr-FR');
|
|
52
|
+
clearLocales();
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Locale IDs are case-insensitive. `fr-FR` also registers `fr` when no `fr` exists, so a request for `fr-CA` falls back to it.
|
|
56
|
+
|
|
57
|
+
Prefer the CLI-generated files over hand-written `addLocale` calls: see [cli.md](cli.md).
|
|
58
|
+
|
|
59
|
+
## Getting a translation
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
const s = loc('id')`Hello`;
|
|
63
|
+
|
|
64
|
+
s.toString(); // resolved from context (see below)
|
|
65
|
+
s.toString('fr'); // force a locale
|
|
66
|
+
withLocales(['fr-CA', 'fr'], () => s.toString()); // Accept-Language style list
|
|
67
|
+
JSON.stringify({ s }); // toJSON() translates like toString()
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Resolution order for `toString()`:
|
|
71
|
+
|
|
72
|
+
1. The locale passed as argument.
|
|
73
|
+
2. The `withLocales()` list, first locale that has the string.
|
|
74
|
+
3. The current locale set with `changeLocale()`.
|
|
75
|
+
4. The default locale (in-code text).
|
|
76
|
+
|
|
77
|
+
`withLocales` is what the Express and GraphQL integrations call for you.
|
|
78
|
+
|
|
79
|
+
### Translating whole objects
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { translateObject, translateInContext } from 'smartloc';
|
|
83
|
+
|
|
84
|
+
translateObject('fr', { title: loc`Hello`, items: [loc`A`] }); // { title: 'Bonjour', items: ['A'] }
|
|
85
|
+
withLocales(['fr'], () => translateInContext(obj)); // same, using ambient context
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Both return a copy only when something changed. Typed as `TranslationOf<T>`.
|
|
89
|
+
|
|
90
|
+
## Other kinds of strings
|
|
91
|
+
|
|
92
|
+
All of them implement the same `LocStr` interface as `loc`.
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
import { singleLoc, multiLoc, joinArray } from 'smartloc';
|
|
96
|
+
|
|
97
|
+
singleLoc('TypeScript'); // same text in every language
|
|
98
|
+
multiLoc({ en: 'Hello', fr: 'Bonjour' }); // translations declared inline
|
|
99
|
+
joinArray(', ', [loc`Red`, loc`Blue`]); // translated join; separator may be a LocStr too
|
|
100
|
+
loc`<b>Hi</b>`.transform(escapeHtml); // post-process the translated text
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`transform()` results cannot be stored (see [storage.md](storage.md)) and render as their parent in React.
|
|
104
|
+
|
|
105
|
+
## Utilities
|
|
106
|
+
|
|
107
|
+
- `isLocStr(value)`: type guard for any `LocStr`.
|
|
108
|
+
- `getDefaultLocale()`: the locale set with `setDefaultLocale`.
|
|
109
|
+
|
|
110
|
+
## Cloning objects that contain `LocStr`
|
|
111
|
+
|
|
112
|
+
`LocStr` instances are frozen objects tagged with a symbol. A deep clone must copy the prototype and symbol properties, or simply skip values where `isLocStr(v)` is true (they are immutable). `JSON.parse(JSON.stringify(obj))` turns them into plain, default-language strings.
|
package/docs/cli.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# CLI: collecting and loading translations
|
|
2
|
+
|
|
3
|
+
## `smartloc collect`
|
|
4
|
+
|
|
5
|
+
Scans your sources for `loc` strings and writes one translation file per locale.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx smartloc collect --format=json --locales=fr,de --defaultLocale=en
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
| Option | Default | Description |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `--locales` | required | Comma-separated locales to generate, other than the default one. |
|
|
14
|
+
| `--defaultLocale` | inferred | Language of the in-code text. Inferred from `setDefaultLocale('xx')` in sources when omitted. When given, also writes the default-locale file (needed for plurals and for stored strings, see below). |
|
|
15
|
+
| `--format` | `xliff` | `json` or `xliff`. |
|
|
16
|
+
| `--outDir` | `i18n` | Output directory. |
|
|
17
|
+
| `--source` | `.` | Directory to scan. `.js`, `.ts`, `.jsx`, `.tsx` files; `.gitignore` is honored. |
|
|
18
|
+
| `--additionalSimpleTag` | | Also collect strings matching a pattern, `*` being the string. Example: `Named<*>` collects `Named<'text'>`. |
|
|
19
|
+
|
|
20
|
+
Add it to `package.json` and run it whenever code changes:
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{ "scripts": { "i18n": "smartloc collect --format=json --locales=fr,de --defaultLocale=en" } }
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### What a run does
|
|
27
|
+
|
|
28
|
+
- New strings are added with an empty `target`.
|
|
29
|
+
- Strings that disappeared from code are removed. If a string with the same text reappears under another ID, its translation is moved.
|
|
30
|
+
- Strings whose source text changed keep their translation but are marked `"dirty": true`.
|
|
31
|
+
- The command prints, per locale, how many translations are missing, dirty, or identical to the source.
|
|
32
|
+
|
|
33
|
+
### File format (JSON)
|
|
34
|
+
|
|
35
|
+
`i18n/fr.json`:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"sourceLanguage": "en",
|
|
40
|
+
"targetLanguage": "fr",
|
|
41
|
+
"resources": {
|
|
42
|
+
"$default": {
|
|
43
|
+
"sha1.b128...": { "source": "Auto id", "target": null }
|
|
44
|
+
},
|
|
45
|
+
"cart": {
|
|
46
|
+
"items": {
|
|
47
|
+
"source": { "singular": null, "plural": "{0} items" },
|
|
48
|
+
"target": { "singular": "Un article", "plural": "{0} articles" }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
- IDs are grouped by namespace: the part before the first `.`. Strings without a dot go under `$default`.
|
|
56
|
+
- Fill in `target`. A `null` target falls back to the next locale, then to the default language.
|
|
57
|
+
- Plural entries hold `{ singular, plural }`. `singular` is used when the count is exactly 1.
|
|
58
|
+
- `--format=xliff` writes the same data as XLIFF. JSON is the recommended format: the XLIFF writer currently uses the `.json` extension, which the loaders do not recognize as XLIFF.
|
|
59
|
+
|
|
60
|
+
## Loading translations
|
|
61
|
+
|
|
62
|
+
### Node
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { loadAllLocales, loadLocale } from 'smartloc/node';
|
|
66
|
+
|
|
67
|
+
await loadAllLocales('./i18n'); // every file in the directory
|
|
68
|
+
await loadLocale('./i18n/fr.json'); // one file
|
|
69
|
+
await loadAllLocales('./more', true); // merge into already loaded locales
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Without `merge`, loading a locale that already exists throws.
|
|
73
|
+
|
|
74
|
+
### Browser
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import { loadJsonLocale } from 'smartloc';
|
|
78
|
+
|
|
79
|
+
await loadJsonLocale('/i18n/fr.json'); // fetch() then register
|
|
80
|
+
loadJsonLocale(importedJsonObject); // or pass the parsed object
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Default locale file
|
|
84
|
+
|
|
85
|
+
Load the default-locale file too (`i18n/en.json`) when you use plurals, or when you deserialize stored strings (see [storage.md](storage.md)). It is the only place holding the singular form of your default language.
|
package/docs/express.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Express
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
import express from 'express';
|
|
5
|
+
import { loc, setDefaultLocale } from 'smartloc';
|
|
6
|
+
import smartloc from 'smartloc/express';
|
|
7
|
+
import { loadAllLocales } from 'smartloc/node';
|
|
8
|
+
|
|
9
|
+
setDefaultLocale('en');
|
|
10
|
+
const ready = loadAllLocales('./i18n');
|
|
11
|
+
|
|
12
|
+
const app = express();
|
|
13
|
+
app.use(smartloc({ waitFor: ready }));
|
|
14
|
+
|
|
15
|
+
app.get('/', (req, res) => res.json({ answer: loc('hello')`Hello world` }));
|
|
16
|
+
app.get('/:name', (req, res) => res.send(loc('hi')`Hi ${req.params.name}`));
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The middleware wraps `res.json()`, `res.jsonp()` and `res.send()` so that everything they serialize is translated for the request's `Accept-Language` header. Requests without that header get the default language.
|
|
20
|
+
|
|
21
|
+
## Options
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
smartloc({
|
|
25
|
+
// Promise to await before translating (e.g. locale loading)
|
|
26
|
+
waitFor: ready,
|
|
27
|
+
|
|
28
|
+
// Pick the locale yourself; falls back to Accept-Language when it returns nothing
|
|
29
|
+
customLocaleResolver: req => req.user?.language, // string, string[], or a Promise of those
|
|
30
|
+
|
|
31
|
+
// Called when the resolver throws; the request is then served untranslated
|
|
32
|
+
errorLogger: err => console.error(err),
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Full runnable example: [samples/express/main.ts](../samples/express/main.ts).
|
package/docs/graphql.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# GraphQL (Apollo Server + Express)
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
import { ApolloServer } from 'apollo-server-express';
|
|
5
|
+
import { GraphQLSchema, GraphQLObjectType } from 'graphql';
|
|
6
|
+
import { loc, setDefaultLocale } from 'smartloc';
|
|
7
|
+
import { GLocString, localizeSchema, localizedContext } from 'smartloc/graphql';
|
|
8
|
+
|
|
9
|
+
setDefaultLocale('en');
|
|
10
|
+
|
|
11
|
+
const schema = new GraphQLSchema({
|
|
12
|
+
query: new GraphQLObjectType({
|
|
13
|
+
name: 'Root',
|
|
14
|
+
fields: () => ({
|
|
15
|
+
hello: {
|
|
16
|
+
type: GLocString, // exposed as `LocalizedString`
|
|
17
|
+
resolve: () => loc('hello')`Hello world`,
|
|
18
|
+
},
|
|
19
|
+
}),
|
|
20
|
+
}),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const apollo = new ApolloServer({
|
|
24
|
+
schema: localizeSchema(schema),
|
|
25
|
+
context: localizedContext(async ({ req }) => ({ /* your context */ })),
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Three pieces:
|
|
30
|
+
|
|
31
|
+
- **`GLocString`**: a `LocalizedString` scalar. Resolvers return any `LocStr`. As an input, it accepts a plain string (`singleLoc`) or `{ "en": "...", "fr": "..." }` (`multiLoc`).
|
|
32
|
+
- **`localizeSchema(schema)`**: patches resolvers of `LocalizedString` fields, and of `JSON`/`JSONObject` scalar fields, so their result is translated. Call it once on your built schema.
|
|
33
|
+
- **`localizedContext(fn)`**: wraps your context factory and reads `Accept-Language` from the request. If you build the context yourself, tag it with `localizedContextObject(ctx, ['fr', 'en'])` instead.
|
|
34
|
+
|
|
35
|
+
`JSON` fields are translated recursively, so an object containing `loc` strings can be returned as-is.
|
|
36
|
+
|
|
37
|
+
Full runnable example: [samples/graphql/main.ts](../samples/graphql/main.ts).
|
package/docs/react.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# React
|
|
2
|
+
|
|
3
|
+
Every `LocStr` is also a valid React element. Render it directly:
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { loc, setDefaultLocale, changeLocale, loadJsonLocale } from 'smartloc';
|
|
7
|
+
|
|
8
|
+
setDefaultLocale('en');
|
|
9
|
+
await loadJsonLocale('/i18n/fr.json');
|
|
10
|
+
|
|
11
|
+
function Welcome({ user }) {
|
|
12
|
+
return <h1>{loc('welcome')`Welcome ${<b>{user.name}</b>}`}</h1>;
|
|
13
|
+
}
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
- Placeholders can be React nodes. They are kept as nodes when rendered, so the translation `Bienvenue {0} !` yields `Bienvenue <b>Alice</b> !`.
|
|
17
|
+
- `changeLocale('fr')` re-renders every mounted `loc` string. No provider or context needed.
|
|
18
|
+
- Numbers and dates in placeholders are formatted for the active locale.
|
|
19
|
+
|
|
20
|
+
## Strings where an element is not allowed
|
|
21
|
+
|
|
22
|
+
Attributes such as `placeholder`, `title` or `aria-label` need a plain string. Use the hook, which re-renders on locale change:
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
import { useAsString } from 'smartloc';
|
|
26
|
+
|
|
27
|
+
function Search() {
|
|
28
|
+
const hint = useAsString(loc('search.hint')`Type to search`);
|
|
29
|
+
return <input placeholder={hint} />;
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`useAsString` accepts a `LocStr`, a plain string, `null` or `undefined`. The same hook exists as a method: `loc`...`.useAsString()`.
|
|
34
|
+
|
|
35
|
+
Outside components, `.toString()` gives the text for the current locale.
|
|
36
|
+
|
|
37
|
+
## Switching language
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
<select onChange={e => changeLocale(e.target.value)}>
|
|
41
|
+
<option value="en">English</option>
|
|
42
|
+
<option value="fr">Français</option>
|
|
43
|
+
</select>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The locale must be loaded first (`loadJsonLocale` or `addLocale`), otherwise `changeLocale` throws. Pass `false` as second argument to get `'not found'` instead.
|
|
47
|
+
|
|
48
|
+
## Notes
|
|
49
|
+
|
|
50
|
+
- `transform()` results render as their untransformed parent (with a console warning). Apply transformations to the string from `useAsString()` instead.
|
|
51
|
+
- `joinArray()` renders its items and separators as a fragment.
|
|
52
|
+
- React 19 is a peer dependency. Server-side rendering works with `renderToString`.
|
package/docs/storage.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Storing untranslated strings
|
|
2
|
+
|
|
3
|
+
Sometimes a `LocStr` must be persisted (a DB record, a queue message) and translated later, when it is finally shown to a user.
|
|
4
|
+
|
|
5
|
+
## Store
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { loc, multiLoc, toJsonStorable } from 'smartloc';
|
|
9
|
+
|
|
10
|
+
const record = {
|
|
11
|
+
title: loc('doc.title')`Report for ${customer}`,
|
|
12
|
+
status: multiLoc({ en: 'Draft', fr: 'Brouillon' }),
|
|
13
|
+
plain: 'Untouched',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const stored = JSON.stringify(toJsonStorable(record));
|
|
17
|
+
// {"title":{"i18n":"doc.title","data":["Acme"]},"status":{"i18n:en":"Draft","i18n:fr":"Brouillon"},"plain":"Untouched"}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`toJsonStorable` walks objects and arrays recursively. Plain values, `Date`s and nested `LocStr` placeholders are preserved. Plural counts are stored too.
|
|
21
|
+
|
|
22
|
+
`withSerializationContext(() => JSON.stringify(record))` does the same for a direct `JSON.stringify`.
|
|
23
|
+
|
|
24
|
+
### Stored forms
|
|
25
|
+
|
|
26
|
+
| Kind | Stored as |
|
|
27
|
+
|---|---|
|
|
28
|
+
| `loc` without placeholders | `"i18n/id:doc.title"` |
|
|
29
|
+
| `loc` with placeholders or plural | `{ "i18n": "doc.title", "data": [...], "count": n }` |
|
|
30
|
+
| `singleLoc` | `"i18n/single:text"` |
|
|
31
|
+
| `multiLoc` | `{ "i18n:en": "...", "i18n:fr": "..." }` |
|
|
32
|
+
| `transform()` / `joinArray()` | not supported, throws |
|
|
33
|
+
|
|
34
|
+
### Options
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
toJsonStorable(record, { nonSelfDescriptive: 'toMulti' });
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
- `'id'` (default): store the ID. The reader must have the same translations loaded.
|
|
41
|
+
- `'toMulti'`: expand into every loaded locale, stored as a `multiLoc`. Self-contained, but not updated when translations change.
|
|
42
|
+
- `'skip'`: leave `loc` strings as-is, so `JSON.stringify` translates them in the current locale.
|
|
43
|
+
|
|
44
|
+
## Restore
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { toLocalizable, jsonParseLocalized } from 'smartloc';
|
|
48
|
+
|
|
49
|
+
const record = toLocalizable(JSON.parse(stored)); // from a parsed value
|
|
50
|
+
const record = jsonParseLocalized(stored); // from the JSON text
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The result contains `LocStr` instances again, ready for `withLocales`, Express, GraphQL or React.
|
|
54
|
+
|
|
55
|
+
Restored `loc` strings no longer carry their in-code text. The default locale must therefore be loaded as a regular translation file (generate it with `smartloc collect --defaultLocale=en`, see [cli.md](cli.md)), otherwise translating them in the default language throws.
|
|
56
|
+
|
|
57
|
+
Dates become ISO strings through JSON and are not converted back.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "smartloc",
|
|
3
|
-
"version": "2.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.0.11",
|
|
4
|
+
"description": "i18n with tagged template strings, for Node APIs (Express, GraphQL) and React apps",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"smartloc": "cli/cli.js"
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
"tagged-string-template",
|
|
17
17
|
"graphql",
|
|
18
18
|
"translation",
|
|
19
|
-
"
|
|
20
|
-
"
|
|
19
|
+
"typescript",
|
|
20
|
+
"react"
|
|
21
21
|
],
|
|
22
22
|
"author": "Olivier Guimbal",
|
|
23
23
|
"license": "MIT",
|
|
@@ -37,5 +37,13 @@
|
|
|
37
37
|
"express": "~4.17.1",
|
|
38
38
|
"graphql": "0.13.2",
|
|
39
39
|
"react": "^19"
|
|
40
|
+
},
|
|
41
|
+
"agents": {
|
|
42
|
+
"skills": [
|
|
43
|
+
{
|
|
44
|
+
"name": "smartloc",
|
|
45
|
+
"path": "./skills/smartloc"
|
|
46
|
+
}
|
|
47
|
+
]
|
|
40
48
|
}
|
|
41
49
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: smartloc
|
|
3
|
+
description: Use when adding or changing user-facing text in a project that depends on the smartloc i18n library (tagged template strings, Express/GraphQL/React), or when generating and loading its translation files.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# smartloc
|
|
7
|
+
|
|
8
|
+
Read `AGENTS.md` at the root of the `smartloc` package (`node_modules/smartloc/AGENTS.md`) for the rules and API table. Full docs are in `node_modules/smartloc/docs/`:
|
|
9
|
+
|
|
10
|
+
- `api.md`: `loc`, plurals, locale resolution, other string kinds
|
|
11
|
+
- `cli.md`: `smartloc collect`, file format, loading
|
|
12
|
+
- `express.md`, `graphql.md`, `react.md`: integrations
|
|
13
|
+
- `storage.md`: persisting untranslated strings
|
|
14
|
+
|
|
15
|
+
## Workflow for new user-facing text
|
|
16
|
+
|
|
17
|
+
1. Write ``loc('namespace.id')`Text with ${placeholders}` `` in code. Never pre-translate or concatenate.
|
|
18
|
+
2. In React, render the `LocStr` directly; use `useAsString()` only for string-only props.
|
|
19
|
+
3. Run the project's collect script (look for `smartloc collect` in `package.json`), or `npx smartloc collect --format=json --locales=<locales> --defaultLocale=<default>`.
|
|
20
|
+
4. Fill the new `target` entries in `i18n/<locale>.json`. Leave `source` untouched.
|
|
21
|
+
5. Do not commit `i18n` if the project ignores it; check `.gitignore`.
|
|
22
|
+
|
|
23
|
+
## Quick checks
|
|
24
|
+
|
|
25
|
+
- `setDefaultLocale('xx')` must run once at startup, with a literal locale string.
|
|
26
|
+
- Translated output only appears inside `withLocales()`, the Express/GraphQL integrations, or React rendering.
|
|
27
|
+
- Plurals: `loc.plural(n)('id')`...``, and the default-locale file must be generated and loaded.
|