@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 +35 -0
- package/README.md +1 -1
- package/bin/vestra-plugin.mjs +4 -2
- package/dist/commands.d.ts +7 -4
- package/dist/i18n-tools.js +120 -12
- package/dist/i18n.d.ts +92 -0
- package/dist/i18n.js +299 -0
- package/dist/player-runtime.d.ts +2 -1
- package/dist/player-runtime.js +2 -1
- package/dist/plugin/bindings.js +1 -0
- package/dist/plugin/commands.js +24 -4
- package/dist/plugin/index.js +9 -1
- package/dist/plugin/language.d.ts +6 -2
- package/dist/plugin/language.js +74 -0
- package/dist/plugin/osd.d.ts +10 -2
- package/dist/plugin/osd.js +35 -16
- package/dist/plugin/subtitles.js +9 -1
- package/dist/plugin/testing.d.ts +69 -11
- package/dist/plugin/testing.js +234 -58
- package/dist/plugin/timeline.js +3 -1
- package/dist/plugin/types.d.ts +338 -43
- package/dist/plugin/ui.js +26 -10
- package/dist/plugin/values.d.ts +55 -0
- package/dist/plugin/values.js +117 -0
- package/dist/source-resolvers.d.ts +0 -1
- package/dist/source-resolvers.js +9 -1
- package/dist/subtitles.d.ts +3 -1
- package/dist/wire.d.ts +17 -9
- package/dist/wire.js +2 -1
- package/llms-full.txt +298 -49
- package/llms.txt +42 -42
- package/package.json +1 -1
- package/slint/vestra/widgets.slint +6 -2
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
|
|
86
|
+
| `vestra-plugin i18n check [dir...]` | Translation coverage and untranslatable text; fails on stale catalogs. |
|
|
87
87
|
|
|
88
88
|
## Compatibility
|
|
89
89
|
|
package/bin/vestra-plugin.mjs
CHANGED
|
@@ -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)
|
|
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
|
-
|
|
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
|
|
package/dist/commands.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
path
|
|
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. */
|
package/dist/i18n-tools.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {};
|