saykit 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +15 -0
- package/dist/index.mjs +32 -8
- package/package.json +1 -1
- package/skills/saykit/SKILL.md +21 -9
package/dist/index.d.mts
CHANGED
|
@@ -402,8 +402,23 @@ interface Catalogue<Locale extends string = string> {
|
|
|
402
402
|
/**
|
|
403
403
|
* Matches the best locale from a list of guesses.
|
|
404
404
|
*
|
|
405
|
+
* Guesses are tried in order, and an earlier guess that matches at all wins
|
|
406
|
+
* over a later one that would match more closely. Each guess is
|
|
407
|
+
* canonicalised first, so case does not matter (`en-us` is `en-US`), and
|
|
408
|
+
* then compared in steps, most specific first:
|
|
409
|
+
*
|
|
410
|
+
* 1. The same tag
|
|
411
|
+
* 2. The same language and script, filling in the script a tag implies, so
|
|
412
|
+
* `zh-HK` (Traditional) finds `zh-TW` rather than `zh-CN`
|
|
413
|
+
* 3. The same language subtag, whole, so `fi` never matches `fil`
|
|
414
|
+
*
|
|
415
|
+
* Within a step, the locale written first wins, so with `pt-BR` before
|
|
416
|
+
* `pt-PT`, `pt` matches `pt-BR`.
|
|
417
|
+
*
|
|
405
418
|
* An absent or empty guess is skipped rather than throwing, which lets a
|
|
406
419
|
* caller write `match(fromCookie, fromHeader)` without narrowing each first.
|
|
420
|
+
* So is one that is not a well-formed locale tag, unless it is written
|
|
421
|
+
* exactly as one of {@link locales}.
|
|
407
422
|
*
|
|
408
423
|
* @param guesses List of locale guesses
|
|
409
424
|
* @returns The best matching locale, or the first of {@link locales} if no
|
package/dist/index.mjs
CHANGED
|
@@ -231,6 +231,10 @@ function createCatalogue(messages, options = {}) {
|
|
|
231
231
|
const keys = Object.freeze(Object.keys(sources));
|
|
232
232
|
if (keys.length === 0) throw new Error("A catalogue needs at least one locale, none were given");
|
|
233
233
|
const locales = keys;
|
|
234
|
+
const tags = locales.flatMap((locale) => {
|
|
235
|
+
const tag = parseTag(locale);
|
|
236
|
+
return tag ? [[locale, tag]] : [];
|
|
237
|
+
});
|
|
234
238
|
const store = /* @__PURE__ */ new Map();
|
|
235
239
|
const views = /* @__PURE__ */ new Map();
|
|
236
240
|
const loading = /* @__PURE__ */ new Map();
|
|
@@ -271,14 +275,15 @@ function createCatalogue(messages, options = {}) {
|
|
|
271
275
|
return task;
|
|
272
276
|
},
|
|
273
277
|
match(...guesses) {
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
if (
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
278
|
+
for (const guess of guesses.flat()) {
|
|
279
|
+
if (typeof guess !== "string" || guess === "") continue;
|
|
280
|
+
const tag = parseTag(guess);
|
|
281
|
+
if (!tag) {
|
|
282
|
+
if (locales.includes(guess)) return guess;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
const found = tags.find(([, t]) => t.canonical === tag.canonical) ?? (tag.script ? tags.find(([, t]) => t.language === tag.language && t.script === tag.script) : void 0) ?? tags.find(([, t]) => t.language === tag.language);
|
|
286
|
+
if (found) return found[0];
|
|
282
287
|
}
|
|
283
288
|
return locales[0];
|
|
284
289
|
},
|
|
@@ -292,6 +297,25 @@ function createCatalogue(messages, options = {}) {
|
|
|
292
297
|
}
|
|
293
298
|
return Object.freeze(catalogue);
|
|
294
299
|
}
|
|
300
|
+
function parseTag(input) {
|
|
301
|
+
let canonical;
|
|
302
|
+
try {
|
|
303
|
+
[canonical] = Intl.getCanonicalLocales(input);
|
|
304
|
+
} catch {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if (!canonical) return void 0;
|
|
308
|
+
const language = canonical.split("-")[0];
|
|
309
|
+
let script;
|
|
310
|
+
try {
|
|
311
|
+
script = new Intl.Locale(canonical).maximize().script;
|
|
312
|
+
} catch {}
|
|
313
|
+
return {
|
|
314
|
+
canonical,
|
|
315
|
+
language,
|
|
316
|
+
script
|
|
317
|
+
};
|
|
318
|
+
}
|
|
295
319
|
// src/scope.ts
|
|
296
320
|
const NO_VIEW = "No view is in scope for 'say'. Run the work inside 'scope.run(view, callback)', or establish one with 'scope.use(store)'.";
|
|
297
321
|
function createVariableStorage() {
|
package/package.json
CHANGED
package/skills/saykit/SKILL.md
CHANGED
|
@@ -97,18 +97,30 @@ module.exports = function (api) {
|
|
|
97
97
|
};
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
Next.js and Metro also need `withSayKit` around their config, which compiles the catalogues (see below):
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
// next.config.mjs
|
|
104
|
+
import { withSayKit } from 'babel-plugin-saykit/next';
|
|
105
|
+
export default withSayKit({});
|
|
106
|
+
|
|
107
|
+
// metro.config.js
|
|
108
|
+
const { withSayKit } = require('babel-plugin-saykit/metro');
|
|
109
|
+
module.exports = withSayKit(getDefaultConfig(__dirname));
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Catalogues are never loaded through a plugin: no loader options, nothing intercepts an import.
|
|
101
113
|
|
|
102
114
|
### Compiled locale modules
|
|
103
115
|
|
|
104
|
-
|
|
116
|
+
Each catalogue compiles to a `{locale}.js` beside it (`src/locales/fr.po` → `src/locales/fr.js`), with the `fallbackLocales` chain already merged in, plus a `{locale}.d.ts` declaring `Record<string, Message>`. The build plugin does this: `unplugin-saykit` when the build starts, `withSayKit` when Next or Metro loads its config, and both recompile in dev whenever a catalogue changes. `saykit extract` compiles too. Gitignore the `.js` modules and commit the `.d.ts`, which never changes, so `tsc` passes on a fresh clone.
|
|
117
|
+
|
|
118
|
+
No `pre*` script is needed with a plugin. Without one (plain `babel`, a script Node runs directly), run `saykit compile` before anything imports a locale:
|
|
105
119
|
|
|
106
120
|
```json title="package.json"
|
|
107
121
|
{
|
|
108
122
|
"scripts": {
|
|
109
|
-
"
|
|
110
|
-
"prebuild": "saykit compile",
|
|
111
|
-
"pretest": "saykit compile"
|
|
123
|
+
"prebuild": "saykit compile"
|
|
112
124
|
}
|
|
113
125
|
}
|
|
114
126
|
```
|
|
@@ -210,7 +222,7 @@ export type Locale = (typeof catalogue.locales)[number];
|
|
|
210
222
|
|
|
211
223
|
```ts
|
|
212
224
|
catalogue.locales; // ['en', 'fr']
|
|
213
|
-
catalogue.match(navigator.languages, cookieLocale); // exact, then language
|
|
225
|
+
catalogue.match(navigator.languages, cookieLocale); // case-insensitive: exact, then language + script, then language, then locales[0]; nullish and malformed guesses are skipped
|
|
214
226
|
catalogue.loaded('fr'); // false until loaded
|
|
215
227
|
const say = await catalogue.load('fr'); // calls the thunk once; synchronous when already loaded
|
|
216
228
|
catalogue.locale('fr'); // synchronous; throws if a lazy locale has not been loaded
|
|
@@ -333,7 +345,7 @@ new Client(options, { commands: [new Ping()] }, [new SayPlugin(catalogue)]);
|
|
|
333
345
|
|
|
334
346
|
1. Write or change a message.
|
|
335
347
|
2. `saykit extract` (or `saykit extract --watch`). Only the source locale file is written; other locales are created header-only and never touched afterwards. Every locale is then compiled.
|
|
336
|
-
3. Translate the other locales by hand or through a TMS. A locale file needs only the entries actually translated; the rest fall back through `fallbackLocales` to the source string at compile time. After pulling translations, `saykit compile
|
|
348
|
+
3. Translate the other locales by hand or through a TMS. A locale file needs only the entries actually translated; the rest fall back through `fallbackLocales` to the source string at compile time. After pulling translations, the next dev server or build compiles them (or `saykit compile` without a plugin).
|
|
337
349
|
4. `saykit clean` when source strings were removed or reworded: it drops orphaned and empty entries from non-source locales and never adds anything.
|
|
338
350
|
|
|
339
351
|
Commit the catalogues and `.d.ts` files, not the compiled `.js`. CI check that extraction was run:
|
|
@@ -353,11 +365,11 @@ Custom formats: a `Formatter` is `{ extension: '.yaml', parse(content) => Messag
|
|
|
353
365
|
| ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
354
366
|
| `'say' is a macro and must be used with the relevant saykit plugin` | The file was not transformed. Check the bucket `include` glob and that the plugin is in the bundler and test config |
|
|
355
367
|
| `No message for <id> in locale 'x'` | Stale or hand-written module. Run `saykit extract`; import the compiled file, never declare messages |
|
|
356
|
-
| `Cannot find module './locales/en.js'` | Not compiled yet.
|
|
368
|
+
| `Cannot find module './locales/en.js'` | Not compiled yet. Add the plugin (and `withSayKit` for Next/Metro), or run `saykit compile` where there is none |
|
|
357
369
|
| `Messages for locale 'x' have not been loaded yet` | `catalogue.locale('x')` on a lazy locale. `await catalogue.load('x')` or `store.set('x')` first |
|
|
358
370
|
| `No messages for locale 'x'` | `x` is not a key of the catalogue. Run guesses through `match` first |
|
|
359
371
|
| `'getSay' must be called below a 'withSay'` | This segment is not wrapped. Wrap every layout and page, not only the root |
|
|
360
|
-
| Edited a `.po`, dev server shows old strings | The module was not recompiled.
|
|
372
|
+
| Edited a `.po`, dev server shows old strings | The module was not recompiled. On Next/Metro, check `withSayKit` wraps the config; otherwise run `saykit compile` |
|
|
361
373
|
| Translator sees `{0}` | Placeholder is not an identifier. Name it: `${{ name: value }}` |
|
|
362
374
|
| Build fails on a branch key such as `sold-out` | ICU keys are identifiers. Rename the discriminant at its source |
|
|
363
375
|
| `Text strings must be rendered within a <Text> component` | React Native. `<Say whitespace={false}>` |
|