@vestra-player/sdk 1.5.9 → 1.5.12

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/CHANGELOG.md CHANGED
@@ -1,5 +1,40 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.5.12
4
+
5
+ **Keys of disabled commands pass on; `i18n check` fails on stale catalogs; translator notes in tables; the Button follows its text.** A fix release; `apiVersion` stays `vestra/1`.
6
+
7
+ - **`TestHost.press` matches the player (0.4.3) for disabled commands.** A disabled command no longer swallows its key: the key goes to the next command bound to it (the one it shadowed in a conflict, then the global scope after a panel binding). With only disabled commands left, the first one with a `reason` shows the reason on the OSD and eats the key; without a reason the key reaches the focused control. `press` now resolves with what happened, `"ran"`, `"reason"` (see `host.osd`) or `"passed"`, instead of rejecting for a disabled command; it still rejects when nothing is bound and when the command that runs is busy. A test that expected `press` to reject for a disabled command now gets `"passed"` or `"reason"`.
8
+ - **`vestra-plugin i18n check` exits with 1** when `locales/messages.pot` is out of date, missing while `.po` files exist, or a `.po` lacks some of the template's messages, so CI catches a forgotten `extract`. Before, it printed these and exited 0. The other findings stay warnings, and `vestra-plugin check` still only prints them.
9
+ - **`TRANSLATORS:` notes on `as const` table entries.** A comment before a property or element of an object or array literal under `as const` (at any depth) now reaches the translators like one at a call site; before, texts extracted through a table lost their notes.
10
+ - **`slint/vestra/widgets.slint`** (the copy of the player's library): `Button`'s preferred width follows its label (the minimum width is unchanged), so a longer translated label widens the button instead of overflowing it.
11
+
12
+ ## 1.5.11
13
+
14
+ **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).
15
+
16
+ - **`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.
17
+ - **`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`).
18
+ - **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 })`.
19
+ - **`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.
20
+ - **`plugin.i18n.resolve(value)`**: the translated string, for text that leaves the player only (clipboard, files). Never for the interface.
21
+ - **`OsdPanel.set(text, args, { maxColumns })`** (`OsdPanelOptions`): the player cuts each line after translation (a wide character is two columns).
22
+ - **`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`.
23
+ - **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.
24
+ - **`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".
25
+ - **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.
26
+ - **`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.
27
+
28
+ ## 1.5.10
29
+
30
+ **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.
31
+
32
+ - **`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.
33
+ - **`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`).
34
+ - **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.
35
+ - **`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).
36
+ - 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.
37
+
3
38
  ## 1.5.9
4
39
 
5
40
  **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`.
package/README.md CHANGED
@@ -83,7 +83,7 @@ Complete, tested plugins in [`examples/`](https://github.com/vestrasys/vestra-pl
83
83
  | `vestra-plugin build [dir...] [--version x.y.z]` | Writes `dist/`: types stripped, files copied, manifest versioned. |
84
84
  | `vestra-plugin pack [dir...] [--version x.y.z] [--out dir]` | Builds, then packs `dist/` as `<id>-<version>.vxp`. |
85
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). |
86
+ | `vestra-plugin i18n check [dir...]` | Translation coverage and untranslatable text; fails on stale catalogs. |
87
87
 
88
88
  ## Compatibility
89
89
 
@@ -29,7 +29,8 @@
29
29
  * i18n check coverage per language, placeholder and plural problems, a
30
30
  * stale messages.pot, and texts that cannot be translated
31
31
  * (composed in TypeScript, not literals, not English, Slint
32
- * text without @tr); warnings only, an unreadable .po fails
32
+ * text without @tr) as warnings; fails (exit 1) on a stale
33
+ * messages.pot, a .po missing messages, an unreadable file
33
34
  *
34
35
  * `check` prints the same i18n warnings when the plugin has locales/;
35
36
  * `build` compiles locales/<lang>.po to dist/locales/<lang>.json (the .po
@@ -430,7 +431,8 @@ async function i18nCheck(target) {
430
431
  dependencies(target);
431
432
  const { warnings, errors } = await i18nReport(target);
432
433
  for (const line of warnings) console.log(`${target.name}: i18n: ${line}`);
433
- if (errors.length) fail(`${target.name}: ${errors.join("; ")}`);
434
+ // A stale messages.pot, a .po missing messages, an unreadable file.
435
+ if (errors.length) fail(`${target.name}: i18n: ${errors.join("; ")}`);
434
436
  if (!warnings.length) console.log(`${target.name}: i18n: no problems`);
435
437
  }
436
438
 
@@ -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. */
@@ -26,7 +26,7 @@ export const pluralForms = (language) => ONE_FORM.has(language)
26
26
  : "nplurals=2; plural=(n != 1);";
27
27
  const slash = (path) => path.split(sep).join("/");
28
28
  const CJK = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u;
29
- const COMPOSED = 'user-visible text composed in TypeScript: pass a template with args (osd.show("Speed {}×", [rate])) or move it into Slint';
29
+ const COMPOSED = 'user-visible text composed in TypeScript: pass a template with args (osd.show("Speed {}×", [rate]), plugin.i18n.text("Track {}", [n])) or move it into Slint';
30
30
  const DYNAMIC = (what) => `${what} is not a literal; its text cannot be extracted (use a string literal or an \`as const\` table)`;
31
31
  const SITES = {
32
32
  "Commands.add": { object: 0, fields: ["title", "reason"], path: true },
@@ -43,6 +43,9 @@ const SITES = {
43
43
  "Dialogs.openFiles": { object: 0, fields: ["title"] },
44
44
  "Dialogs.openDirectory": { object: 0, fields: ["title"] },
45
45
  "Dialogs.saveFile": { object: 0, fields: ["title"] },
46
+ "I18n.text": { argument: 0 },
47
+ "I18n.error": { argument: 0 },
48
+ "I18n.resolve": { argument: 0 },
46
49
  };
47
50
  /** Without the SDK's types: the module property names in front of the method. */
48
51
  const BY_NAME = {
@@ -55,6 +58,9 @@ const BY_NAME = {
55
58
  "dialogs.openFiles": "Dialogs.openFiles",
56
59
  "dialogs.openDirectory": "Dialogs.openDirectory",
57
60
  "dialogs.saveFile": "Dialogs.saveFile",
61
+ "i18n.text": "I18n.text",
62
+ "i18n.error": "I18n.error",
63
+ "i18n.resolve": "I18n.resolve",
58
64
  };
59
65
  const none = () => ({ values: [], dynamic: false, composed: false });
60
66
  const join2 = (a, b) => ({
@@ -94,6 +100,34 @@ function ignored(node, text) {
94
100
  .slice(node.getFullStart(), node.getStart())
95
101
  .includes("i18n-ignore");
96
102
  }
103
+ /**
104
+ * A string that is an entry (a property value or an element) of an object
105
+ * or array literal inside `… as const`, at any depth.
106
+ */
107
+ function inConstTable(node) {
108
+ let current;
109
+ if (ts.isPropertyAssignment(node.parent) && node.parent.initializer === node)
110
+ current = node.parent.parent;
111
+ else if (ts.isArrayLiteralExpression(node.parent))
112
+ current = node.parent;
113
+ else
114
+ return false;
115
+ for (;;) {
116
+ const parent = current.parent;
117
+ if (ts.isAsExpression(parent) &&
118
+ ts.isTypeReferenceNode(parent.type) &&
119
+ ts.isIdentifier(parent.type.typeName) &&
120
+ parent.type.typeName.text === "const")
121
+ return true;
122
+ if (ts.isPropertyAssignment(parent))
123
+ current = parent.parent;
124
+ else if (ts.isArrayLiteralExpression(parent) ||
125
+ ts.isParenthesizedExpression(parent))
126
+ current = parent;
127
+ else
128
+ return false;
129
+ }
130
+ }
97
131
  /** The statement (or class member) a node belongs to. */
98
132
  function statementOf(node) {
99
133
  let current = node;
@@ -105,6 +139,11 @@ function statementOf(node) {
105
139
  }
106
140
  class Extractor {
107
141
  found = [];
142
+ /**
143
+ * `TRANSLATORS:` notes on entries of `as const` tables, by text: a table
144
+ * entry is extracted through its literal type, which has no comments.
145
+ */
146
+ tableNotes = new Map();
108
147
  findings = [];
109
148
  #checker;
110
149
  constructor(checker) {
@@ -125,6 +164,11 @@ class Extractor {
125
164
  for (const part of parts) {
126
165
  if (part.isStringLiteral())
127
166
  result.values.push(part.value);
167
+ // A text value (`plugin.i18n.text(…)`): its source is extracted where
168
+ // it is made.
169
+ else if (part.getProperty("$vestraText") ||
170
+ part.getProperty("$vestraList"))
171
+ continue;
128
172
  else if (part.flags &
129
173
  (ts.TypeFlags.Null |
130
174
  ts.TypeFlags.Undefined |
@@ -249,10 +293,22 @@ class Extractor {
249
293
  /** `Interface.method` of a call to the SDK, if it is one of `SITES`. */
250
294
  #site(call) {
251
295
  const callee = call.expression;
252
- if (!ts.isPropertyAccessExpression(callee))
296
+ const declaration = this.#checker?.getResolvedSignature(call)?.declaration;
297
+ if (!ts.isPropertyAccessExpression(callee)) {
298
+ // `const { text } = plugin.i18n; text("…")`: by the declaration.
299
+ if (declaration &&
300
+ !ts.isJSDocSignature(declaration) &&
301
+ declaration.parent &&
302
+ ts.isInterfaceDeclaration(declaration.parent) &&
303
+ declaration.name &&
304
+ ts.isIdentifier(declaration.name) &&
305
+ /\/plugin\/types\.(d\.)?ts$/.test(slash(declaration.getSourceFile().fileName))) {
306
+ const name = `${declaration.parent.name.text}.${declaration.name.text}`;
307
+ return name in SITES ? name : undefined;
308
+ }
253
309
  return undefined;
310
+ }
254
311
  const method = callee.name.text;
255
- const declaration = this.#checker?.getResolvedSignature(call)?.declaration;
256
312
  if (declaration &&
257
313
  !ts.isJSDocSignature(declaration) &&
258
314
  declaration.parent &&
@@ -279,6 +335,27 @@ class Extractor {
279
335
  const visit = (node) => {
280
336
  if (ts.isCallExpression(node))
281
337
  this.#call(node, file, source, text);
338
+ if ((ts.isStringLiteral(node) ||
339
+ ts.isNoSubstitutionTemplateLiteral(node)) &&
340
+ inConstTable(node)) {
341
+ // The comment sits before the entry: the property, or the element.
342
+ const entry = ts.isPropertyAssignment(node.parent) ? node.parent : node;
343
+ const notes = notesOf(entry, text);
344
+ if (notes.length) {
345
+ const known = this.tableNotes.get(node.text) ?? [];
346
+ for (const note of notes)
347
+ if (!known.includes(note))
348
+ known.push(note);
349
+ this.tableNotes.set(node.text, known);
350
+ }
351
+ }
352
+ // A text value written as data: `{ $vestraText: "…" }`.
353
+ if (ts.isPropertyAssignment(node) &&
354
+ (ts.isIdentifier(node.name) || ts.isStringLiteral(node.name)) &&
355
+ node.name.text === "$vestraText" &&
356
+ !ignored(node, text) &&
357
+ !ignored(statementOf(node), text))
358
+ this.#emit(this.values(node.initializer), node.initializer, file, source, "the source of a text", () => true, [...notesOf(statementOf(node), text), ...notesOf(node, text)]);
282
359
  if ((ts.isStringLiteral(node) ||
283
360
  ts.isNoSubstitutionTemplateLiteral(node)) &&
284
361
  CJK.test(node.text) &&
@@ -330,6 +407,32 @@ class Extractor {
330
407
  return;
331
408
  }
332
409
  for (const property of object.properties) {
410
+ if (ts.isSpreadAssignment(property)) {
411
+ // `{ ...extra }`: the spread object's fields, by their types; a
412
+ // field that is not a literal (or a text value) is reported.
413
+ const checker = this.#checker;
414
+ const at = source.getLineAndCharacterOfPosition(property.getStart(source)).line +
415
+ 1;
416
+ if (!checker) {
417
+ this.finding(file, at, "dynamic", `fields spread into ${name} cannot be read without types; write ${fields.join(", ")} explicitly`);
418
+ continue;
419
+ }
420
+ if (ignored(property, text))
421
+ continue;
422
+ const type = checker.getTypeAtLocation(property.expression);
423
+ for (const field of fields) {
424
+ const symbol = type.getProperty(field);
425
+ if (!symbol)
426
+ continue;
427
+ if (field === "path") {
428
+ this.finding(file, at, "dynamic", `a path spread into ${name} cannot be extracted; write path explicitly`);
429
+ continue;
430
+ }
431
+ const fieldType = checker.getTypeOfSymbolAtLocation(symbol, property.expression);
432
+ this.#emit(this.#fromType(fieldType), property, file, source, `${field} spread into ${name}`, () => true, statementNotes);
433
+ }
434
+ continue;
435
+ }
333
436
  const key = property.name &&
334
437
  (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name))
335
438
  ? property.name.text
@@ -533,7 +636,10 @@ export function extract(options) {
533
636
  }
534
637
  if (occurrence.plural !== undefined && message.plural === undefined)
535
638
  message.plural = occurrence.plural;
536
- for (const note of occurrence.notes)
639
+ const tableNotes = occurrence.context === undefined
640
+ ? (extractor.tableNotes.get(occurrence.id) ?? [])
641
+ : [];
642
+ for (const note of [...occurrence.notes, ...tableNotes])
537
643
  if (!message.notes.includes(note))
538
644
  message.notes.push(note);
539
645
  if (!message.references.includes(reference))
@@ -627,24 +733,26 @@ export function checkCatalogs(directory, messages, findings) {
627
733
  const pot = join(locales, "messages.pot");
628
734
  const keys = (entries) => new Set(entries.map((e) => `${keyOf(e.context, e.id)}\u0000${e.plural ?? ""}`));
629
735
  const wanted = keys(messages);
736
+ const files = existsSync(locales)
737
+ ? readdirSync(locales)
738
+ .filter((name) => name.endsWith(".po"))
739
+ .sort()
740
+ : [];
741
+ // A stale template or a .po without some of its messages fails
742
+ // `i18n check` (an error); `check` prints it like the warnings.
630
743
  if (!existsSync(pot))
631
- warnings.push("locales/messages.pot is missing: run vestra-plugin i18n extract");
744
+ (files.length ? errors : warnings).push("locales/messages.pot is missing: run vestra-plugin i18n extract");
632
745
  else {
633
746
  try {
634
747
  const have = keys(parsePo(readFileSync(pot, "utf8")).entries);
635
748
  const same = have.size === wanted.size && [...wanted].every((key) => have.has(key));
636
749
  if (!same)
637
- warnings.push("locales/messages.pot is out of date: run vestra-plugin i18n extract");
750
+ errors.push("locales/messages.pot is out of date: run vestra-plugin i18n extract");
638
751
  }
639
752
  catch (error) {
640
753
  errors.push(`locales/messages.pot: ${error.message}`);
641
754
  }
642
755
  }
643
- const files = existsSync(locales)
644
- ? readdirSync(locales)
645
- .filter((name) => name.endsWith(".po"))
646
- .sort()
647
- : [];
648
756
  for (const name of files) {
649
757
  const language = name.slice(0, -3);
650
758
  if (language === "en") {
@@ -673,7 +781,7 @@ export function checkCatalogs(directory, messages, findings) {
673
781
  const inFile = keys(live);
674
782
  const missing = [...wanted].filter((key) => !inFile.has(key)).length;
675
783
  if (missing)
676
- warnings.push(`locales/${name}: ${missing} ${missing === 1 ? "message is" : "messages are"} not in this file: run vestra-plugin i18n extract`);
784
+ errors.push(`locales/${name}: ${missing} ${missing === 1 ? "message is" : "messages are"} not in this file: run vestra-plugin i18n extract`);
677
785
  for (const entry of live) {
678
786
  entry.strings.forEach((text, form) => {
679
787
  if (text === "")
package/dist/i18n.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { ListStyle, Locale } from "./plugin/types.ts";
1
2
  /**
2
3
  * The player's interface languages: English (the source language) first,
3
4
  * then the translations, as the player's own table (its tests check that
@@ -78,6 +79,83 @@ export declare function format(template: string, args: TemplateArgsValue): strin
78
79
  * number (`{n}` of a Slint plural is `"n"`). Translations must keep them.
79
80
  */
80
81
  export declare function placeholders(text: string): string[];
82
+ /**
83
+ * The ways a `TextList` is joined (`ListStyle`), as the player
84
+ * joins them in each interface language (`LIST_SEPARATORS`).
85
+ */
86
+ export declare const LIST_STYLES: readonly ListStyle[];
87
+ /**
88
+ * The separator of each list style per interface language, as the player's
89
+ * table (`i18n::text::SEPARATORS`; its tests check that they agree).
90
+ */
91
+ export declare const LIST_SEPARATORS: Readonly<Record<Locale, Readonly<Record<ListStyle, string>>>>;
92
+ /**
93
+ * The player's limits on one text value (a
94
+ * {@link @vestra-player/sdk!Text | Text} or
95
+ * {@link @vestra-player/sdk!TextList | TextList} with everything nested in it), as the player's
96
+ * `i18n::text` (its tests check that they agree).
97
+ */
98
+ export declare const TEXT_LIMITS: {
99
+ /** Nesting depth of texts and lists; the value itself is depth 0 (8). */
100
+ readonly depth: 8;
101
+ /** Texts, lists and arguments in all, the value itself included (1024). */
102
+ readonly nodes: 1024;
103
+ /** Arguments of one text (16). */
104
+ readonly args: 16;
105
+ /** Items of one list (256). */
106
+ readonly items: 256;
107
+ /** UTF-8 bytes of a source text or a string argument (4096). */
108
+ readonly stringBytes: 4096;
109
+ };
110
+ /** A translatable text on the wire: English source text and its arguments. */
111
+ export type TextJson = {
112
+ $vestraText: string;
113
+ args?: ArgJson[] | {
114
+ [name: string]: ArgJson;
115
+ };
116
+ };
117
+ /** A list on the wire: items joined the interface language's way. */
118
+ export type ListJson = {
119
+ $vestraList: ArgJson[];
120
+ style?: ListStyle;
121
+ };
122
+ /** An argument of a text: data (strings and numbers) or another text. */
123
+ export type ArgJson = string | number | TextJson | ListJson;
124
+ /** A text or list value (by its marker key). */
125
+ export declare function isTextValue(value: unknown): value is TextJson | ListJson;
126
+ /**
127
+ * Why the player would refuse a text value or template arguments, or
128
+ * `null`. `args` mode checks template arguments (an array or object of
129
+ * arguments) instead of one value. The same rules and messages as the
130
+ * player's `i18n::text::check`.
131
+ */
132
+ export declare function textProblem(value: unknown, mode?: "value" | "args"): string | null;
133
+ type TemplateArgsLike = readonly ArgJson[] | Readonly<Record<string, ArgJson>> | null;
134
+ /**
135
+ * A text value as the player shows it: the source translated with the
136
+ * plugin's catalog and formatted with its (rendered) arguments; a list's
137
+ * items joined with the locale's separator.
138
+ */
139
+ export declare function renderText(value: TextJson | ListJson, catalog: Catalog, locale: string): string;
140
+ /**
141
+ * Template arguments with their texts rendered (for `format`): what the
142
+ * player fills a template with.
143
+ */
144
+ export declare function renderTemplateArgs(args: TemplateArgsLike | undefined, catalog: Catalog, locale: string): TemplateArgsValue | undefined;
145
+ /** The English rendering of a text value: the key a menu merges by. */
146
+ export declare function textKey(value: TextJson | ListJson): string;
147
+ /**
148
+ * A property value with every text value in it replaced by its shown
149
+ * string, as the player does before it hands the value to Slint. Returns
150
+ * a problem (`textProblem`) instead when a text value is malformed.
151
+ */
152
+ export declare function resolveTexts<T>(value: T, catalog: Catalog, locale: string): {
153
+ value: T;
154
+ } | {
155
+ problem: string;
156
+ };
157
+ /** Whether a value holds a text value anywhere (a cheap pre-check). */
158
+ export declare function hasTexts(value: unknown): boolean;
81
159
  /** One `@tr("context" => "id" | "plural" % n, args…)` call. */
82
160
  export interface TrCall {
83
161
  context?: string;
@@ -128,3 +206,17 @@ export declare function menuKeys(path: readonly string[]): string[];
128
206
  * agree).
129
207
  */
130
208
  export declare const HOST_ZH_HANS: Readonly<Record<string, string>>;
209
+ /**
210
+ * The catalog of a package directory for `locale`, as the player loads it:
211
+ * the compiled `locales/<lang>.json` of a built package, else the source
212
+ * `locales/<lang>.po`. Empty for English and without a file; a broken file
213
+ * throws.
214
+ */
215
+ export declare function loadPackageCatalog(directory: string, locale: string): Catalog;
216
+ /**
217
+ * Each line of `text` cut to `max` columns, ending with `…` when cut, as
218
+ * the player cuts an OSD panel's lines (`OsdPanelOptions.maxColumns`; the
219
+ * player measures with Unicode's East Asian Width, this is close to it).
220
+ */
221
+ export declare function fitColumns(text: string, max: number): string;
222
+ export {};