smartloc 2.0.9 → 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 -181
- package/core/json-utils.js +16 -4
- package/core/json-utils.js.map +1 -1
- 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,225 +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
|
-
});
|
|
56
|
-
|
|
57
|
-
// Use the string without language context (logs, ...)
|
|
58
|
-
console.log(str.toString()); // => Hello world
|
|
59
|
-
console.log(JSON.stringify({msg: str})); // => {"msg": "Hello world"}
|
|
60
|
-
|
|
61
|
-
// ... or with language context (when returning a query result, ...)
|
|
62
|
-
console.log(withLocales(['it', 'fr'], () => str.toString())); // => Bonjour world
|
|
63
|
-
console.log(withLocales(['it', 'fr'], () => JSON.stringify({msg: str})); // => {"msg": "Bonjour world"}
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
As you might see, the translation is NOT performed when you build the string, but when you actually try to send a result to your end user.
|
|
67
|
-
|
|
68
|
-
This allows you to build your app without caring about knowing which language your user accepts.
|
|
69
|
-
The translation will be automatically performed when sending actual json to your user, through a simple middleware (see samples listed in "Framework Support")
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
# Translating your app
|
|
73
|
-
|
|
74
|
-
## Generating/updating translations from code
|
|
75
|
-
As an example, if you write your code in english, and you would like to translate your app in French and Deutsch, add the following script to your package.json file:
|
|
76
|
-
|
|
77
|
-
```typescript
|
|
78
|
-
{
|
|
79
|
-
"scripts": {
|
|
80
|
-
"smartloc": "smartloc collect --format=json --locales=fr-FR,de-DE --defaultLocale=en-US"
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
Once you have written your code (or each time you have changed it), you can run `npm run smartloc` to create/update your translation files.
|
|
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
|
|
30
|
+
addLocale('fr', { greeting: 'Bonjour {0} !' });
|
|
94
31
|
|
|
95
|
-
|
|
32
|
+
const msg = loc('greeting')`Hello ${'world'}!`;
|
|
96
33
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
```typescript
|
|
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
|
-
});
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
### Option 2 - Load a single given file
|
|
110
|
-
|
|
111
|
-
```typescript
|
|
112
|
-
import { loadAllLocales } from 'smartloc/node';
|
|
113
|
-
|
|
114
|
-
await loadAllLocales('/path/to/my-translation.json', true);
|
|
34
|
+
msg.toString(); // Hello world!
|
|
35
|
+
withLocales(['fr'], () => msg.toString()); // Bonjour world !
|
|
36
|
+
withLocales(['fr'], () => JSON.stringify({ msg })); // {"msg":"Bonjour world !"}
|
|
115
37
|
```
|
|
116
38
|
|
|
117
|
-
|
|
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.
|
|
118
40
|
|
|
41
|
+
## Use it with
|
|
119
42
|
|
|
120
|
-
###
|
|
43
|
+
### Express
|
|
121
44
|
|
|
122
|
-
```
|
|
123
|
-
import
|
|
45
|
+
```ts
|
|
46
|
+
import smartloc from 'smartloc/express';
|
|
124
47
|
|
|
125
|
-
|
|
48
|
+
app.use(smartloc());
|
|
49
|
+
app.get('/', (req, res) => res.json({ answer: loc`Hello` }));
|
|
126
50
|
```
|
|
127
51
|
|
|
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
|
|
52
|
+
`res.json()` and `res.send()` are translated according to the request's `Accept-Language` header. See [docs/express.md](docs/express.md).
|
|
139
53
|
|
|
140
|
-
|
|
54
|
+
### GraphQL (Apollo)
|
|
141
55
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
The `LocStr` interface has [several implementations](./src/core/smartloc.ts):
|
|
145
|
-
|
|
146
|
-
### Smartloc
|
|
147
|
-
The default one which is returned when using the `loc` tag:
|
|
148
|
-
```typescript
|
|
149
|
-
return loc`Hello`;
|
|
56
|
+
```ts
|
|
57
|
+
import { GLocString, localizeSchema, localizedContext } from 'smartloc/graphql';
|
|
150
58
|
```
|
|
151
59
|
|
|
152
|
-
|
|
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
|
-
```
|
|
60
|
+
Declare fields as `LocalizedString`, wrap your schema and context, and resolvers can return `loc` strings. See [docs/graphql.md](docs/graphql.md).
|
|
160
61
|
|
|
161
|
-
###
|
|
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
|
-
```
|
|
62
|
+
### React
|
|
166
63
|
|
|
167
|
-
|
|
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.
|
|
64
|
+
A `loc` string is also a React element. Placeholders can be React nodes.
|
|
170
65
|
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
.transform(x => escapeHtml(x)); // apply a transformation
|
|
66
|
+
```tsx
|
|
67
|
+
<h1>{loc('welcome')`Welcome ${<b>{user.name}</b>}`}</h1>
|
|
174
68
|
```
|
|
175
69
|
|
|
176
|
-
|
|
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`]);
|
|
70
|
+
`changeLocale('fr')` re-renders every rendered string. Use `useAsString()` where a plain string is needed. See [docs/react.md](docs/react.md).
|
|
181
71
|
|
|
182
|
-
|
|
72
|
+
## Translate your app
|
|
183
73
|
|
|
184
|
-
|
|
185
|
-
|
|
74
|
+
```bash
|
|
75
|
+
npx smartloc collect --format=json --locales=fr,de --defaultLocale=en
|
|
186
76
|
```
|
|
187
77
|
|
|
188
|
-
|
|
78
|
+
This scans your sources and writes one file per locale in `./i18n`. Translate the `target` values, then load them at startup:
|
|
189
79
|
|
|
190
|
-
|
|
80
|
+
```ts
|
|
81
|
+
import { loadAllLocales } from 'smartloc/node'; // server
|
|
82
|
+
await loadAllLocales('./i18n');
|
|
191
83
|
|
|
192
|
-
|
|
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);
|
|
84
|
+
import { loadJsonLocale } from 'smartloc'; // browser
|
|
85
|
+
await loadJsonLocale('/i18n/fr.json');
|
|
206
86
|
```
|
|
207
87
|
|
|
208
|
-
|
|
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).
|
|
209
89
|
|
|
210
|
-
|
|
211
|
-
import {toLocalizable} from 'smartloc';
|
|
212
|
-
const obj = loadFromDb();
|
|
213
|
-
|
|
214
|
-
// get back a translatable intance
|
|
215
|
-
const serializable = toLocalizable(obj);
|
|
216
|
-
```
|
|
90
|
+
## Going further
|
|
217
91
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
-
|
|
221
|
-
-
|
|
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
|
|
222
96
|
|
|
223
|
-
|
|
97
|
+
## License
|
|
224
98
|
|
|
225
|
-
|
|
99
|
+
MIT
|
package/core/json-utils.js
CHANGED
|
@@ -41,7 +41,7 @@ function _toLocalizable(value, depth) {
|
|
|
41
41
|
}
|
|
42
42
|
return value;
|
|
43
43
|
}
|
|
44
|
-
if (typeof value !== 'object' || value instanceof Date) {
|
|
44
|
+
if (typeof value !== 'object' || value instanceof Date || (0, literal_1.isLocStr)(value)) {
|
|
45
45
|
return value;
|
|
46
46
|
}
|
|
47
47
|
// === handle arrays
|
|
@@ -62,11 +62,19 @@ function _toLocalizable(value, depth) {
|
|
|
62
62
|
return value;
|
|
63
63
|
}
|
|
64
64
|
// === handle smartloc with args
|
|
65
|
-
|
|
65
|
+
// The native encoder includes count (possibly undefined before stringify).
|
|
66
|
+
// A counted message can have no interpolation data; JSON then keeps null.
|
|
67
|
+
if (typeof value.i18n === 'string'
|
|
68
|
+
&& (value.data instanceof Array || value.data === null)
|
|
69
|
+
&& keys.includes('data')
|
|
70
|
+
&& (keys.length === 2 || (keys.length === 3 && keys.includes('count')))
|
|
71
|
+
&& (value.count === undefined || typeof value.count === 'number')) {
|
|
66
72
|
return (0, smartloc_1.smartLoc)({
|
|
67
73
|
id: value.i18n,
|
|
68
74
|
literals: null,
|
|
69
|
-
|
|
75
|
+
// These descriptors can themselves contain messages. Restore them
|
|
76
|
+
// before rendering so their IDs are not interpolated as literal text.
|
|
77
|
+
placeholders: _toLocalizable(value.data, depth - 1),
|
|
70
78
|
count: value.count
|
|
71
79
|
});
|
|
72
80
|
}
|
|
@@ -136,7 +144,11 @@ function _toJsonStorable(v, depth) {
|
|
|
136
144
|
return ret || v;
|
|
137
145
|
}
|
|
138
146
|
if ((0, literal_1.isLocStr)(v)) {
|
|
139
|
-
|
|
147
|
+
const stored = v.toJSON();
|
|
148
|
+
// toJSON returns the interpolation values as-is. Convert those while
|
|
149
|
+
// the serialization scope is still active; later JSON.stringify must
|
|
150
|
+
// not translate a remaining nested LocStr into a fixed-language string.
|
|
151
|
+
return stored === v ? v : _toJsonStorable(stored, depth - 1);
|
|
140
152
|
}
|
|
141
153
|
const obj = {};
|
|
142
154
|
let diff = false;
|
package/core/json-utils.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"json-utils.js","sourceRoot":"","sources":["../../src/core/json-utils.ts"],"names":[],"mappings":";;AAOA,gDAGC;AAKD,sCAEC;
|
|
1
|
+
{"version":3,"file":"json-utils.js","sourceRoot":"","sources":["../../src/core/json-utils.ts"],"names":[],"mappings":"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"}
|
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.
|