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 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
- # Purpose
1
+ # smartloc
2
2
 
3
- If like me:
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
- ... then this library might be for you.
9
-
10
- Read a tutorial to learn how to use this lib [here](https://dev.to/oguimbal/i18n-express-apollo-graphql-server-translation-made-simple-33f5)
11
-
12
-
13
- # Framework support
5
+ ```ts
6
+ const msg = loc('greeting')`Hello ${name}!`;
7
+ ```
14
8
 
15
- There are almost-one-liners integrations with the following frameworks:
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
- - [Express](https://github.com/apollographql/apollo-server/tree/master/packages/apollo-server-express) => see [this sample](./samples/express/main.ts)
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
- # How this works
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 it
18
+ ## Install
23
19
 
24
20
  ```bash
25
- npm install smartloc --save
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
- Those will give you an instance of `StrLoc` interface.
24
+ ## Quick start
44
25
 
45
- Here is how you can use it:
46
- ```typescript
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
- // Use the string without language context (logs, ...)
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
- // ... 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"}
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
- 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.
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
- 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")
41
+ ## Use it with
70
42
 
43
+ ### Express
71
44
 
72
- # Translating your app
45
+ ```ts
46
+ import smartloc from 'smartloc/express';
73
47
 
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
- }
48
+ app.use(smartloc());
49
+ app.get('/', (req, res) => res.json({ answer: loc`Hello` }));
83
50
  ```
84
51
 
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
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
- To load other locales, you have several options:
54
+ ### GraphQL (Apollo)
98
55
 
99
- ### Option 1 - Define in code:
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
- });
56
+ ```ts
57
+ import { GLocString, localizeSchema, localizedContext } from 'smartloc/graphql';
107
58
  ```
108
59
 
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);
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
- nb: The second argument is 'merge'... if false, all previously loaded translations will be cleared. Else, translations will be merged.
62
+ ### React
118
63
 
64
+ A `loc` string is also a React element. Placeholders can be React nodes.
119
65
 
120
- ### Option 3 - Scan a directory for translations
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
- nb: The second argument is 'merge'... if false, all previously loaded translations will be cleared. Else, translations will be merged.
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
- # Other use cases
72
+ ## Translate your app
143
73
 
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`;
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
- ### Recursive object conversion
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
- ```typescript
225
- import { loc, toJsonStorable, toLocalizable } from 'smartloc';
80
+ ```ts
81
+ import { loadAllLocales } from 'smartloc/node'; // server
82
+ await loadAllLocales('./i18n');
226
83
 
227
- const value = { labels: ['Custom text', loc('save')`Save`, ''] };
228
- const stored = JSON.stringify(toJsonStorable(value));
229
- const restored = toLocalizable(JSON.parse(stored));
84
+ import { loadJsonLocale } from 'smartloc'; // browser
85
+ await loadJsonLocale('/i18n/fr.json');
230
86
  ```
231
87
 
232
- Plain strings remain strings. Version 2.0.10 also preserves plural counts and
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
- `toJsonStorable` leaves ordinary `Date` values intact until JSON serialization.
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
- ## Cloning
244
- Beware, if you deep-clone an object containing smartloc string instances, you must:
245
- - Clone the object prototype
246
- - Clone symbol properties
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
- ... or you could just ignore cloning smartloc strings altogether (they are immutable anyway): You can detect them using the `isLocStr()` method and skip them when performing your deep clone.
97
+ ## License
249
98
 
250
- NB: Of course, if you clone your object using `JSON.parse(JSON.stringify(obj))`, then you will lose translatability (smartloc strings will be translated as strings in your default language).
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.
@@ -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).
@@ -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`.
@@ -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.10",
4
- "description": "A i18n toolset for nodejs apis leveraging tagged strings",
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
- "internationalization",
20
- "typescript"
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.