@vestra-player/sdk 1.5.8 → 1.5.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +4 -1
  3. package/bin/vestra-plugin.mjs +170 -7
  4. package/dist/commands.d.ts +7 -4
  5. package/dist/i18n-tools.d.ts +67 -0
  6. package/dist/i18n-tools.js +787 -0
  7. package/dist/i18n.d.ts +222 -0
  8. package/dist/i18n.js +972 -0
  9. package/dist/player-runtime.d.ts +2 -0
  10. package/dist/player-runtime.js +2 -0
  11. package/dist/plugin/appearance.js +27 -1
  12. package/dist/plugin/bindings.js +1 -0
  13. package/dist/plugin/commands.js +24 -4
  14. package/dist/plugin/index.js +9 -1
  15. package/dist/plugin/language.d.ts +8 -0
  16. package/dist/plugin/language.js +79 -0
  17. package/dist/plugin/osd.d.ts +11 -0
  18. package/dist/plugin/osd.js +64 -9
  19. package/dist/plugin/packages.js +10 -0
  20. package/dist/plugin/runtime.d.ts +3 -1
  21. package/dist/plugin/runtime.js +6 -0
  22. package/dist/plugin/scope.d.ts +1 -0
  23. package/dist/plugin/scope.js +2 -0
  24. package/dist/plugin/subtitles.js +9 -1
  25. package/dist/plugin/testing.d.ts +184 -5
  26. package/dist/plugin/testing.js +459 -67
  27. package/dist/plugin/timeline.js +3 -1
  28. package/dist/plugin/types.d.ts +523 -54
  29. package/dist/plugin/ui.js +29 -10
  30. package/dist/plugin/values.d.ts +55 -0
  31. package/dist/plugin/values.js +117 -0
  32. package/dist/plugin/window.d.ts +2 -0
  33. package/dist/plugin/window.js +4 -0
  34. package/dist/source-resolvers.d.ts +0 -1
  35. package/dist/source-resolvers.js +9 -1
  36. package/dist/subtitles.d.ts +3 -1
  37. package/dist/wire.d.ts +38 -6
  38. package/dist/wire.js +13 -2
  39. package/llms-full.txt +646 -58
  40. package/llms.txt +44 -40
  41. package/package.json +1 -1
  42. package/slint/vestra/markdown.slint +5 -5
  43. package/slint/vestra/widgets.slint +4 -4
package/CHANGELOG.md CHANGED
@@ -1,5 +1,44 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.5.11
4
+
5
+ **Text values: words mixed with data, translated by the player** (player 0.4.3). Additive; `apiVersion` stays `vestra/1`, and plugins that worked need no change. 1.5.9 and 1.5.10 were not published to npm; this release includes them. Round-2 i18n requests R2-audio-1, R2-video-1, R2-subtitles-1/2, R2-playlist-1/2, R2-store-1, R2-workshop-1, R2-nes-1, capture #1, P-2, the info and hotkeys export texts, timeline and subtitle titles: one mechanism. Guide: [Translate a plugin ▸ Text that mixes words and data](docs/how-to/i18n.md#text-that-mixes-words-and-data).
6
+
7
+ - **`plugin.i18n.text(source, args?)`** returns a `Text`: plain, frozen data (`{ $vestraText, args }`), not a translation. The player translates `source` with the plugin's catalog and fills in `args` wherever it shows it. Arguments are data, or text values that are translated too, so a sentence can be built from translated parts without composing it in TypeScript.
8
+ - **`plugin.i18n.list(items, style?)`** returns a `TextList`: items joined with the interface language's separator (`ListStyle`: `"list"` `, ` / `、`, `"clauses"` `, ` / `,`, `"semicolons"`, `"sentences"`, `"lines"`; `LIST_SEPARATORS` in `@vestra-player/sdk/testing`).
9
+ - **Accepted wherever the player shows a plugin's text**: command `title`, `path` segments (the menu merges by the English rendering) and `reason`; `osd.show` and `OsdPanel.set` as the text or an argument (`TemplateArgs` now takes `TextArg`); every `dialogs.confirm` text; file dialog `title` and `saveFile`'s `fileName` (a string `fileName` stays data); property values where Slint has a `string`, at any depth (`PropData` takes `Text` and `TextList`), so a ComboBox model can mix fixed words and data; `TimelineLayer.set` mark titles; `subtitles.load({ title })`.
10
+ - **`plugin.i18n.error(source, args?)`**: an `Error` (`TextError`) whose words the player shows translated when a file or web resolver's `resolve` throws it (also a stream failing while it starts): the open's failure and `player.state.error`. `message` is the English rendering.
11
+ - **`plugin.i18n.resolve(value)`**: the translated string, for text that leaves the player only (clipboard, files). Never for the interface.
12
+ - **`OsdPanel.set(text, args, { maxColumns })`** (`OsdPanelOptions`): the player cuts each line after translation (a wide character is two columns).
13
+ - **`BindingInfo.key`**: the menu path's English keys, the same in every language (P-2). Find a command by its place in the menu with it, not with `path`.
14
+ - **Limits** (`TEXT_LIMITS` in `@vestra-player/sdk/testing`): nesting 8, 1024 values in one text, 16 arguments per text, 256 items per list, 4096 bytes per source or string argument (template string arguments were 1024 bytes). A malformed value throws `invalid` where it is made, and the player refuses it with `invalid`. A property value's limits count a text value as the string it becomes.
15
+ - **`vestra-plugin i18n extract`** collects the source of every `plugin.i18n.text(…)`, `error(…)` and `resolve("…")` call (also destructured: `const { text } = plugin.i18n`) and of a `{ $vestraText: "…" }` literal. Every source is collected, also one made only of placeholders and punctuation (`"{} ({})"`, which Chinese writes `"{}({})"`). It now reads fields spread into a call's options (`commands.add({ ...extra })`) by their types and reports the ones it cannot extract; before, it skipped them without a word. A field typed as a text value is not reported as "not a literal".
16
+ - **Type note**: the option fields that now take a text value (`ConfirmOptions.message` and the other dialog texts, `CommandOptions.title`/`path`/`reason`, `CommandState.reason`, file dialog `title`/`fileName`, `TemplateArgs`) are wider types. Code that only passes them is unchanged; code that reads such a field back as a `string` (a helper that cuts `options.message`) needs a `typeof … === "string"` check.
17
+ - **`TestHost`** shows text values as the player does: `FakeCommand.display` (and the non-enumerable `display.reason`), `host.osd`, `host.osdPanel` (cut to `maxColumns`), `host.confirmTexts`, new `host.fileDialogs` (`FileDialogText`), window, panel and settings page `props` (text values replaced by their strings; `host.sent(...)` keeps what the plugin sent), timeline titles, subtitle titles, a resolver's `TextError`, and `BindingInfo.key`. `host.translate` takes a text value too. It refuses malformed text values with the player's messages.
18
+
19
+ ## 1.5.10
20
+
21
+ **Property value limits: documented, higher, enforced by `TestHost`, reported loudly** (player 0.4.3). A fix; `apiVersion` stays `vestra/1`, and plugins that worked need no change. 1.5.9 was not published to npm; this release includes its changes.
22
+
23
+ - **`UI_VALUE_LIMITS`** in `@vestra-player/sdk/testing`: the player's limits on one property value, which used to appear only in the `PropValue` description. Raised in player 0.4.3 (older players in brackets): nesting depth 8, **16384 values** (4096), **4096 rows per array** (1024), 64 KiB per string or field name, 1 MiB of text. A list of 8-field rows is 9 values per row, so about 1800 rows fit (about 450 before). The player's tests check that the constant agrees with the player.
24
+ - **`TestHost` enforces them** on `ui.open`, `ui.set`, `ui.mount`, `ui.setMount`, `settings.register` and `settings.set`, with the player's code and message. Before, it took any value, so a plugin whose list outgrew the limit passed its tests and showed a stale list in the player (D1: a 3000-row playlist). A test that writes a value over the limits now sees the failure in `onError` (or, for a tool window's initial property, a rejected `Window.show`).
25
+ - **Failures say what they are.** A value over the limits fails with code **`limit`** (was `host`) and the message names the property (`rows: UI array exceeds 4096 rows`). A panel's or settings page's initial property over the limits is still left out (the panel mounts without it; a failed mount during activation would fail the plugin), but the player now reports it to `onError` instead of only writing its log.
26
+ - **`ui.error` reaches `onError`.** The player reports failures nobody asked about: a callback whose arguments it could not convert, a property it could not show again when it rebuilt a panel or the Options window (an image file removed since), a detached sidebar it could not prepare. The SDK used to drop these; it now reports them to the root scope's `onError` with the player's code (`ui` from older players).
27
+ - The player's runner (player 0.4.3) refuses a request longer than the player's 1 MiB message with `limit`; before, the player stopped the plugin.
28
+
29
+ ## 1.5.9
30
+
31
+ **Translations** (player 0.4.3). Additive; `apiVersion` stays `vestra/1`, and plugins written for 1.5.8 need no change. One test-visible change: a core action's `declaration.path` in `TestHost` is now the player's English key (read `display` for what the menu shows). The player now shows its interface in English, Simplified Chinese, Japanese, Korean, German, French or Spanish, chosen at start-up; a plugin writes every text in English and ships one gettext file per language. Plugins write no translation code: the player translates. Guides: [Translate a plugin](docs/how-to/i18n.md), [Migrate a plugin to English source text](docs/how-to/i18n-migration.md); example: `examples/i18n`.
32
+
33
+ - **`plugin.i18n.locale`** (new `I18n`, `Locale`): the interface language of this run, `"en"`, `"zh-Hans"`, `"ja"`, `"ko"`, `"de"`, `"fr"` or `"es"`; it never changes while the player runs. `"en"` from older players.
34
+ - **Templates with arguments**: `osd.show(text, args?)`, `OsdPanel.set(text, args?)` and `ConfirmOptions.args` (new `TemplateArgs`: an array for `{}` / `{0}`, an object for `{name}`). The player translates the template with the plugin's catalog, then fills it in. Without `args` the text shows as it is, so existing calls are unchanged; older players show the template unfilled.
35
+ - **Declarations are translated by the player** from the plugin's catalog: command titles, path segments and `reason`, settings page and panel titles, and the manifest's display fields. Menu paths are English source text and menus merge by them; the player's own menus have the English keys `Open`, `Playback`, `Subtitles`, `Video`, `Audio`, `Screen`, `Settings` (the `@video`-style ids still work, and the player still accepts the old Chinese titles of its own menus). New `PluginInfo.display` (`PluginDisplay`): a package's name, punchline, description and tags as shown.
36
+ - **`appearance.language`** (`LanguagePreference`: `"system"` or a locale) and **`appearance.setLanguage(language)`** (needs `ui.theme`): the saved choice, used from the next start.
37
+ - **Catalogs**: `locales/messages.pot` and `locales/<lang>.po` in the plugin's folder. New commands **`vestra-plugin i18n extract`** (finds `@tr` in `.slint` files, the texts of `commands.add`, `osd.show`, dialogs, settings and panel titles through the TypeScript type checker, and the manifest's display fields; merges into every `.po`, keeping translations; `--locale <lang>` starts one) and **`vestra-plugin i18n check`** (coverage, placeholder and plural problems, a stale template, sentences composed in TypeScript, non-literal values, non-English source text, Slint text without `@tr`; warnings only). `vestra-plugin check` prints the same warnings when the plugin has `locales/`. `vestra-plugin build` compiles each `.po` into `dist/locales/<lang>.json` and no longer packs `.po` files.
38
+ - **`TestHost`**: `locale` (also `startPlugin({ locale })`) loads the plugin's catalog for that language; `FakeCommand.display` is the menu as shown, `host.osd` and `osdPanel` the shown text (the template and args stay in `host.sent(...)`), new `host.confirmTexts`, `host.translate(text, args)`, `host.loadCatalog(directory)`, `host.language` / `setLanguage(...)`, settings page and panel titles as shown. The core actions' declarations use the player's English keys (`["Playback", "Seek", "Forward 5 s"]`); their `display` and the binding table show the player's words. While `locale` is not set, the player's own messages (core actions, refusals) stay in the Chinese of 1.5.8, so existing tests keep passing; with a `locale` they are the player's English (or its Chinese for `"zh-Hans"`).
39
+ - `LOCALES` in `@vestra-player/sdk/testing`.
40
+ - The Slint library copy (`slint/vestra/widgets.slint`, `markdown.slint`) uses `@tr` for its own texts (`UrlInputRow`'s default buttons, the code block's Copy button, the image placeholder).
41
+
3
42
  ## 1.5.8
4
43
 
5
44
  **Trial loading is removed; `resolvers.fileTypes`; the CLI under `--permission`.** `apiVersion` stays `vestra/1`. The removal breaks code that called `PackageCandidate.trial()`: only Vestra Player AI did, and it moves to installing (an install can be rolled back).
package/README.md CHANGED
@@ -72,6 +72,7 @@ Complete, tested plugins in [`examples/`](https://github.com/vestrasys/vestra-pl
72
72
  | [menus](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples/menus) | Radio items, separator groups and a menu item whose title changes. | [Step by step](https://vestrasys.github.io/vestra-player-sdk/documents/Example__Menus.html) |
73
73
  | [theme-widgets](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples/theme-widgets) | A tool window drawn with the player's theme and widgets. | [Step by step](https://vestrasys.github.io/vestra-player-sdk/documents/Example__Theme_widgets.html) |
74
74
  | [markdown](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples/markdown) | A window that shows a Markdown document. | [Step by step](https://vestrasys.github.io/vestra-player-sdk/documents/Example__Markdown.html) |
75
+ | [i18n](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples/i18n) | A plugin in the player's language: English source text, a translation. | [Step by step](https://vestrasys.github.io/vestra-player-sdk/documents/Counter__i18n_.html) |
75
76
 
76
77
  ## Command line
77
78
 
@@ -81,6 +82,8 @@ Complete, tested plugins in [`examples/`](https://github.com/vestrasys/vestra-pl
81
82
  | `vestra-plugin test [dir...]` | Runs `node --test` on `tests/*.test.{ts,mjs}`. |
82
83
  | `vestra-plugin build [dir...] [--version x.y.z]` | Writes `dist/`: types stripped, files copied, manifest versioned. |
83
84
  | `vestra-plugin pack [dir...] [--version x.y.z] [--out dir]` | Builds, then packs `dist/` as `<id>-<version>.vxp`. |
85
+ | `vestra-plugin i18n extract [dir...] [--locale lang]` | Collects the texts to translate into `locales/`, keeping translations. |
86
+ | `vestra-plugin i18n check [dir...]` | Translation coverage and untranslatable text (warnings only). |
84
87
 
85
88
  ## Compatibility
86
89
 
@@ -91,7 +94,7 @@ Complete, tested plugins in [`examples/`](https://github.com/vestrasys/vestra-pl
91
94
  | 1.2 | `vestra/1` | 0.2 or newer | 26 (shipped with the player) |
92
95
  | 1.3 | `vestra/1` | 0.2 or newer; 0.3 for the 1.3 additions (menu ids, `Theme.danger`, empty video chains) | 26 (shipped with the player) |
93
96
  | 1.4 | `vestra/1` | 0.2 or newer; 0.3 for the 1.3 additions. `ThemeId` is `"vestra"` only (see the CHANGELOG) | 26 (shipped with the player) |
94
- | 1.5 | `vestra/1` | 0.2 or newer; 0.3 for the 1.3 additions; 0.4 for the 1.5 additions (`vestra-light`, `@vestra/icons.slint`) | 26 (shipped with the player) |
97
+ | 1.5 | `vestra/1` | 0.2 or newer; 0.3 for the 1.3 additions; 0.4 for the 1.5 additions (`vestra-light`, `@vestra/icons.slint`); 0.4.3 for translations (1.5.9) | 26 (shipped with the player) |
95
98
 
96
99
  Minor releases only add backwards-compatible API; a breaking change gets a new `apiVersion` and a new major version. `@vestra-player/sdk/runtime` is for the player only and not covered by semver.
97
100
 
@@ -1,8 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * vestra-plugin: check, build, test and pack a Vestra Player plugin.
3
+ * vestra-plugin: check, build, test and pack a Vestra Player plugin, and
4
+ * keep its translations up to date.
4
5
  *
5
6
  * vestra-plugin <check|build|test|pack> [plugin-dir...] [--version x.y.z] [--out dir]
7
+ * vestra-plugin i18n extract [plugin-dir...] [--locale <lang>]...
8
+ * vestra-plugin i18n check [plugin-dir...]
6
9
  *
7
10
  * A plugin directory holds manifest.json; the default is the current
8
11
  * directory. Works in any project that depends on @vestra-player/sdk.
@@ -14,6 +17,24 @@
14
17
  * pack build, then dist/ → <out>/<id>-<version>.vxp (a zip; default
15
18
  * out: the current directory) with its sha256 and size
16
19
  *
20
+ * Translations (docs/how-to/i18n.md). English is the source text; each
21
+ * language is a gettext file locales/<lang>.po:
22
+ *
23
+ * i18n extract find the texts to translate (@tr in .slint files, the
24
+ * texts handed to commands.add, osd.show, dialogs, settings
25
+ * and panel titles, the manifest's display fields), write
26
+ * locales/messages.pot and merge it into every
27
+ * locales/<lang>.po (translations kept, vanished ones marked
28
+ * obsolete); --locale <lang> starts a new language
29
+ * i18n check coverage per language, placeholder and plural problems, a
30
+ * stale messages.pot, and texts that cannot be translated
31
+ * (composed in TypeScript, not literals, not English, Slint
32
+ * text without @tr); warnings only, an unreadable .po fails
33
+ *
34
+ * `check` prints the same i18n warnings when the plugin has locales/;
35
+ * `build` compiles locales/<lang>.po to dist/locales/<lang>.json (the .po
36
+ * files themselves are not packed).
37
+ *
17
38
  * Every command first checks manifest.json against the player's rules
18
39
  * (display fields, paths inside the package, unknown fields); `check` also
19
40
  * prints hints, such as an id without the `<owner>.` prefix the official
@@ -62,6 +83,10 @@ const sdkRoot = resolve(import.meta.dirname, "..");
62
83
  const { manifestProblems } = await import(
63
84
  pathToFileURL(join(sdkRoot, "dist/manifest.js")).href
64
85
  );
86
+ const i18n = await import(pathToFileURL(join(sdkRoot, "dist/i18n.js")).href);
87
+ const i18nTools = await import(
88
+ pathToFileURL(join(sdkRoot, "dist/i18n-tools.js")).href
89
+ );
65
90
  const SDK = "@vestra-player/sdk";
66
91
 
67
92
  /** Never part of a production package. */
@@ -79,7 +104,8 @@ const SKIP_FILES = new Set([
79
104
  "package.json",
80
105
  "package-lock.json",
81
106
  ]);
82
- const SKIP_EXTENSIONS = new Set([".md", ".log", ".zip", ".vxp"]);
107
+ // Translation sources: build packs the compiled dist/locales/<lang>.json.
108
+ const SKIP_EXTENSIONS = new Set([".md", ".log", ".zip", ".vxp", ".po", ".pot"]);
83
109
  const isTypeScript = (file) => file.endsWith(".ts") && !file.endsWith(".d.ts");
84
110
  const isDeclaration = (file) => /\.d\.(m|c)?ts$/.test(file);
85
111
 
@@ -299,6 +325,113 @@ async function check(target) {
299
325
  await rm(scratch, { recursive: true, force: true });
300
326
  }
301
327
  console.log(`${target.name}: type check passed (${sources.length} files)`);
328
+ // Translation warnings never fail the check.
329
+ if (present(join(target.directory, "locales"))) {
330
+ try {
331
+ const { warnings, errors } = await i18nReport(target);
332
+ for (const line of [...warnings, ...errors])
333
+ console.log(`${target.name}: i18n: ${line}`);
334
+ } catch (error) {
335
+ console.log(`${target.name}: i18n: ${error.message}`);
336
+ }
337
+ }
338
+ }
339
+
340
+ /** The plugin's own production .ts / .mts and .slint files. */
341
+ async function sourceFiles(target) {
342
+ const all = await files(target.directory);
343
+ return {
344
+ typescript: all.filter(
345
+ (file) =>
346
+ (isTypeScript(file) || file.endsWith(".mts")) && !isDeclaration(file),
347
+ ),
348
+ slint: all.filter((file) => file.endsWith(".slint")),
349
+ };
350
+ }
351
+
352
+ /** Compiler options of the type check (its tsconfig), for extraction. */
353
+ async function extraction(target) {
354
+ const ts = await typescript();
355
+ const { typescript: sources, slint } = await sourceFiles(target);
356
+ const config = checkConfig(
357
+ target,
358
+ sources.map((file) => join(target.directory, file)),
359
+ );
360
+ const parsed = ts.parseJsonConfigFileContent(
361
+ config,
362
+ ts.sys,
363
+ target.directory,
364
+ );
365
+ return i18nTools.extract({
366
+ directory: target.directory,
367
+ manifest: target.manifest,
368
+ slint,
369
+ typescript: sources,
370
+ compilerOptions: parsed.options,
371
+ });
372
+ }
373
+
374
+ async function i18nReport(target) {
375
+ const { messages, findings } = await extraction(target);
376
+ return i18nTools.checkCatalogs(target.directory, messages, findings);
377
+ }
378
+
379
+ async function i18nExtract(target, locales) {
380
+ for (const locale of locales) {
381
+ if (locale === "en")
382
+ fail("--locale en: English is the source language, it has no .po file");
383
+ if (!i18n.LOCALES.includes(locale))
384
+ fail(
385
+ `--locale ${locale}: the player's languages are ${i18n.LOCALES.filter((l) => l !== "en").join(", ")}`,
386
+ );
387
+ }
388
+ dependencies(target);
389
+ const { messages, findings } = await extraction(target);
390
+ const directory = join(target.directory, "locales");
391
+ await mkdir(directory, { recursive: true });
392
+ await writeFile(
393
+ join(directory, "messages.pot"),
394
+ i18nTools.template(target.manifest, messages),
395
+ );
396
+ console.log(
397
+ `${target.name}: locales/messages.pot: ${messages.length} messages`,
398
+ );
399
+ const languages = new Set(locales);
400
+ for (const name of await readdir(directory))
401
+ if (name.endsWith(".po")) languages.add(name.slice(0, -3));
402
+ for (const language of [...languages].sort()) {
403
+ if (language === "en" || !i18n.LOCALES.includes(language)) {
404
+ console.log(
405
+ `${target.name}: locales/${language}.po: skipped (not a player language)`,
406
+ );
407
+ continue;
408
+ }
409
+ const file = join(directory, `${language}.po`);
410
+ let existing = null;
411
+ if (present(file)) existing = await readFile(file, "utf8");
412
+ let merged;
413
+ try {
414
+ merged = i18nTools.merge(existing, language, messages);
415
+ } catch (error) {
416
+ fail(`${target.name}: locales/${language}.po: ${error.message}`);
417
+ }
418
+ await writeFile(file, merged.text);
419
+ console.log(
420
+ `${target.name}: locales/${language}.po: ${merged.translated}/${merged.total} translated, ${merged.added} new, ${merged.obsolete} obsolete${existing === null ? " (created)" : ""}`,
421
+ );
422
+ }
423
+ for (const finding of findings)
424
+ console.log(
425
+ `${target.name}: i18n: ${finding.file}:${finding.line}: ${finding.message}`,
426
+ );
427
+ }
428
+
429
+ async function i18nCheck(target) {
430
+ dependencies(target);
431
+ const { warnings, errors } = await i18nReport(target);
432
+ for (const line of warnings) console.log(`${target.name}: i18n: ${line}`);
433
+ if (errors.length) fail(`${target.name}: ${errors.join("; ")}`);
434
+ if (!warnings.length) console.log(`${target.name}: i18n: no problems`);
302
435
  }
303
436
 
304
437
  function version(target, flag) {
@@ -455,6 +588,14 @@ async function build(target, flag) {
455
588
  entry.built,
456
589
  await readFile(join(out, entry.built), "utf8"),
457
590
  );
591
+ let localeWarnings;
592
+ try {
593
+ localeWarnings = i18nTools.compileLocales(target.directory, out);
594
+ } catch (error) {
595
+ fail(`${target.name}: ${error.message}`);
596
+ }
597
+ for (const line of localeWarnings)
598
+ console.log(`${target.name}: warning: ${line}`);
458
599
  await writeFile(join(out, "package.json"), '{"type":"module"}\n');
459
600
  await writeFile(
460
601
  join(out, "manifest.json"),
@@ -558,8 +699,11 @@ async function pack(target, flag, outDirectory) {
558
699
  );
559
700
  }
560
701
 
561
- const USAGE =
562
- "usage: vestra-plugin <check|build|test|pack> [plugin-dir...] [--version x.y.z] [--out dir]";
702
+ const USAGE = [
703
+ "usage: vestra-plugin <check|build|test|pack> [plugin-dir...] [--version x.y.z] [--out dir]",
704
+ " vestra-plugin i18n extract [plugin-dir...] [--locale <lang>]...",
705
+ " vestra-plugin i18n check [plugin-dir...]",
706
+ ].join("\n");
563
707
 
564
708
  async function main() {
565
709
  const args = process.argv.slice(2);
@@ -575,10 +719,25 @@ async function main() {
575
719
  console.log(USAGE);
576
720
  return;
577
721
  }
578
- if (!["check", "build", "test", "pack"].includes(command)) {
722
+ let subcommand = null;
723
+ if (command === "i18n") {
724
+ subcommand = args.shift();
725
+ if (!["extract", "check"].includes(subcommand)) {
726
+ console.error(USAGE);
727
+ process.exit(2);
728
+ }
729
+ } else if (!["check", "build", "test", "pack"].includes(command)) {
579
730
  console.error(USAGE);
580
731
  process.exit(2);
581
732
  }
733
+ const locales = [];
734
+ for (let index; (index = args.indexOf("--locale")) >= 0;) {
735
+ const [, value] = args.splice(index, 2);
736
+ if (value === undefined) fail("--locale needs a value");
737
+ locales.push(value);
738
+ }
739
+ if (locales.length && subcommand !== "extract")
740
+ fail("--locale applies to i18n extract");
582
741
  const flag = option("--version");
583
742
  const out = option("--out");
584
743
  const directories = args.length ? args : ["."];
@@ -588,7 +747,9 @@ async function main() {
588
747
  for (const directory of directories) {
589
748
  try {
590
749
  const target = await plugin(directory);
591
- if (command === "check") await check(target);
750
+ if (subcommand === "extract") await i18nExtract(target, locales);
751
+ else if (subcommand === "check") await i18nCheck(target);
752
+ else if (command === "check") await check(target);
592
753
  else if (command === "build") await build(target, flag);
593
754
  else if (command === "pack") await pack(target, flag, out);
594
755
  else await test(target);
@@ -599,7 +760,9 @@ async function main() {
599
760
  }
600
761
  }
601
762
  if (failed.length) {
602
- console.error(`${command} failed for: ${failed.join(", ")}`);
763
+ console.error(
764
+ `${subcommand ? `i18n ${subcommand}` : command} failed for: ${failed.join(", ")}`,
765
+ );
603
766
  process.exit(1);
604
767
  }
605
768
  }
@@ -1,5 +1,6 @@
1
1
  /** Finite candidate command registry. Host authenticates owner; no plugin supplies an owner id. */
2
2
  import type { Transport } from "./wire.ts";
3
+ import type { ListJson, TextJson } from "./i18n.ts";
3
4
  /**
4
5
  * The player's command registry caps, the same values as the player's own
5
6
  * table (the player's tests check that they agree). They are runaway guards,
@@ -47,12 +48,14 @@ export type CommandState = {
47
48
  enabled: boolean;
48
49
  checked: boolean | null;
49
50
  busy: boolean;
50
- reason: string | null;
51
+ /** Source text, or a text value (player 0.4.3). */
52
+ reason: string | TextJson | ListJson | null;
51
53
  };
52
54
  export type CommandDeclaration = {
53
- title: string;
54
- /** Complete primary menu path INCLUDING its leaf; empty = not in the main menu. */
55
- path: string[];
55
+ /** Source text, or a text value (player 0.4.3). */
56
+ title: string | TextJson;
57
+ /** Complete primary menu path INCLUDING its leaf; empty = not in the main menu. Segments may be text values (player 0.4.3). */
58
+ path: (string | TextJson)[];
56
59
  /** Sibling order, ascending; a submenu takes its first command's order (a built-in menu keeps a fixed one). */
57
60
  order: number;
58
61
  /** Separator label: adjacent siblings of different groups get a line. */
@@ -0,0 +1,67 @@
1
+ import ts from "typescript";
2
+ /** A translatable text found in the sources. */
3
+ export interface Message {
4
+ context?: string;
5
+ id: string;
6
+ plural?: string;
7
+ /** `#.` notes (from `TRANSLATORS:` comments). */
8
+ notes: string[];
9
+ /** `path:line` (or `manifest.json`), relative to the plugin, sorted. */
10
+ references: string[];
11
+ }
12
+ /** Something extraction or the heuristics want the author to look at. */
13
+ export interface Finding {
14
+ /** Relative path, forward slashes. */
15
+ file: string;
16
+ line: number;
17
+ kind: "dynamic" | "composed" | "not-english" | "slint-text";
18
+ message: string;
19
+ }
20
+ export interface ExtractOptions {
21
+ /** The plugin directory. */
22
+ directory: string;
23
+ /** The plugin's `manifest.json` (display fields are extracted). */
24
+ manifest: Record<string, unknown>;
25
+ /** The plugin's `.slint` files, relative to `directory`. */
26
+ slint: readonly string[];
27
+ /** The plugin's production `.ts` / `.mts` files, relative to `directory`. */
28
+ typescript: readonly string[];
29
+ /** Compiler options for a program over those files (as `check` uses). */
30
+ compilerOptions?: ts.CompilerOptions;
31
+ }
32
+ export declare const pluralForms: (language: string) => "nplurals=1; plural=0;" | "nplurals=2; plural=(n != 1);";
33
+ /** Find every translatable text of a plugin, in reference order. */
34
+ export declare function extract(options: ExtractOptions): {
35
+ messages: Message[];
36
+ findings: Finding[];
37
+ };
38
+ /** `locales/messages.pot` for these messages. */
39
+ export declare function template(manifest: Record<string, unknown>, messages: readonly Message[]): string;
40
+ /** The header of a new `<lang>.po`. */
41
+ export declare const newHeader: (language: string) => string;
42
+ /**
43
+ * Merge the template into a language's file: translations and fuzzy flags
44
+ * of present messages stay, new ones come untranslated, notes and
45
+ * references follow the template, vanished translated messages become
46
+ * obsolete (`#~`, at the end) and come back with their translation.
47
+ */
48
+ export declare function merge(existing: string | null, language: string, messages: readonly Message[]): {
49
+ text: string;
50
+ total: number;
51
+ translated: number;
52
+ added: number;
53
+ obsolete: number;
54
+ };
55
+ /** Problems found by `check` in one plugin's catalogs, as text lines. */
56
+ export declare function checkCatalogs(directory: string, messages: readonly Message[], findings: readonly Finding[]): {
57
+ warnings: string[];
58
+ errors: string[];
59
+ };
60
+ /**
61
+ * Compile every `locales/<lang>.po` of a player language to
62
+ * `<out>/locales/<lang>.json`. Returns the warnings (files the player never
63
+ * reads); throws on a `.po` that does not parse.
64
+ */
65
+ export declare function compileLocales(directory: string, out: string): string[];
66
+ /** The relative path of a file from the plugin directory, forward slashes. */
67
+ export declare const relativePath: (directory: string, file: string) => string;