@vestra-player/sdk 1.5.7 → 1.5.9
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 +22 -0
- package/README.md +4 -1
- package/bin/vestra-plugin.mjs +194 -17
- package/dist/commands.d.ts +1 -2
- package/dist/commands.js +1 -2
- package/dist/i18n-tools.d.ts +67 -0
- package/dist/i18n-tools.js +731 -0
- package/dist/i18n.d.ts +130 -0
- package/dist/i18n.js +673 -0
- package/dist/player-runtime.d.ts +1 -0
- package/dist/player-runtime.js +1 -0
- package/dist/plugin/appearance.js +27 -1
- package/dist/plugin/language.d.ts +4 -0
- package/dist/plugin/language.js +5 -0
- package/dist/plugin/osd.d.ts +3 -0
- package/dist/plugin/osd.js +45 -9
- package/dist/plugin/packages.js +11 -65
- package/dist/plugin/resolvers.js +64 -0
- package/dist/plugin/runtime.d.ts +3 -1
- package/dist/plugin/runtime.js +6 -0
- package/dist/plugin/scope.d.ts +1 -0
- package/dist/plugin/scope.js +2 -0
- package/dist/plugin/testing.d.ts +155 -4
- package/dist/plugin/testing.js +332 -50
- package/dist/plugin/types.d.ts +247 -46
- package/dist/plugin/ui.js +4 -1
- package/dist/plugin/window.d.ts +4 -0
- package/dist/plugin/window.js +8 -0
- package/dist/wire.d.ts +29 -14
- package/dist/wire.js +12 -12
- package/llms-full.txt +399 -52
- package/llms.txt +44 -40
- package/package.json +1 -1
- package/slint/vestra/markdown.slint +5 -5
- package/slint/vestra/widgets.slint +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.9
|
|
4
|
+
|
|
5
|
+
**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`.
|
|
6
|
+
|
|
7
|
+
- **`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.
|
|
8
|
+
- **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.
|
|
9
|
+
- **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.
|
|
10
|
+
- **`appearance.language`** (`LanguagePreference`: `"system"` or a locale) and **`appearance.setLanguage(language)`** (needs `ui.theme`): the saved choice, used from the next start.
|
|
11
|
+
- **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.
|
|
12
|
+
- **`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"`).
|
|
13
|
+
- `LOCALES` in `@vestra-player/sdk/testing`.
|
|
14
|
+
- 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).
|
|
15
|
+
|
|
16
|
+
## 1.5.8
|
|
17
|
+
|
|
18
|
+
**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).
|
|
19
|
+
|
|
20
|
+
- **Removed: `PackageCandidate.trial()`, the `PackageTrial` type, `PluginInfo.origin` `"trial"` and the error code `cleanup`** (only `PackageTrial.stop` used it). The player (0.4.2) no longer answers `packages.trial`, `packages.trialState`, `packages.trialStateByRequest` or `packages.stopTrial`, and never lists a plugin with origin `"trial"`. Trial loads only supported plugins using `commands`, `ui.window` and `events`, listed the plugin under another id (`<id>.trial.<n>`), and `trial()` returned before activation ended, so the feature was removed instead of extended. Check a package with `validate` and install it; `rollback` restores the previous version.
|
|
21
|
+
- **`plugin.resolvers.fileTypes`** (player 0.4.2): the extensions some plugin's file resolver handles, lower case, sorted, each once, without who registered them; `[]` from older players. Reading needs no capability. **`plugin.resolvers.onFileTypesChange(listener)`** follows it when a file resolver comes or goes (needs `events`). For a playlist that accepts ROMs while an emulator plugin runs. `TestHost`: `host.fileTypes()` (this plugin's file resolvers plus `otherResolvers`) and `host.updateFileTypes()` after changing `otherResolvers`.
|
|
22
|
+
- **`vestra-plugin` runs under Node's permission model** (`--permission`, with `--allow-child-process` for the type checker): looking for `node_modules/@types`, `tsconfig.json` and `.git` in parent folders it may not read threw `ERR_ACCESS_DENIED`; such a folder now counts as absent.
|
|
23
|
+
- `Packages.validate`: the player (0.4.2) keeps the 32 most recent candidates and drops the oldest, whose `install` then fails with `stale`; before, the 33rd validation of a run failed with `busy` until the player restarted.
|
|
24
|
+
|
|
3
25
|
## 1.5.7
|
|
4
26
|
|
|
5
27
|
**Replaced or closed streams stop with `null`.** A fix; no API change, `apiVersion` stays `vestra/1`, any player.
|
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
|
|
package/bin/vestra-plugin.mjs
CHANGED
|
@@ -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
|
|
@@ -34,7 +55,7 @@
|
|
|
34
55
|
*/
|
|
35
56
|
import { spawnSync } from "node:child_process";
|
|
36
57
|
import { createHash } from "node:crypto";
|
|
37
|
-
import { existsSync } from "node:fs";
|
|
58
|
+
import { existsSync as exists } from "node:fs";
|
|
38
59
|
import {
|
|
39
60
|
cp,
|
|
40
61
|
mkdir,
|
|
@@ -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
|
-
|
|
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
|
|
|
@@ -110,6 +136,20 @@ async function json(path, fallback) {
|
|
|
110
136
|
}
|
|
111
137
|
|
|
112
138
|
/** A module from the plugin's project first, else the SDK's own dependencies. */
|
|
139
|
+
/**
|
|
140
|
+
* Whether a path exists. Under Node's permission model (`--permission`)
|
|
141
|
+
* `existsSync` throws for a path outside the allowed ones; such a path
|
|
142
|
+
* counts as absent, as for a user who cannot read it.
|
|
143
|
+
*/
|
|
144
|
+
function present(path) {
|
|
145
|
+
try {
|
|
146
|
+
return exists(path);
|
|
147
|
+
} catch (error) {
|
|
148
|
+
if (error?.code === "ERR_ACCESS_DENIED") return false;
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
113
153
|
function resolveFrom(directory, specifier) {
|
|
114
154
|
for (const base of [directory, sdkRoot]) {
|
|
115
155
|
try {
|
|
@@ -139,7 +179,7 @@ async function typescript() {
|
|
|
139
179
|
async function plugin(path) {
|
|
140
180
|
const directory = resolve(path);
|
|
141
181
|
const name = relative(process.cwd(), directory) || basename(directory);
|
|
142
|
-
if (!
|
|
182
|
+
if (!present(join(directory, "manifest.json")))
|
|
143
183
|
fail(`${name}: no manifest.json in ${directory}`);
|
|
144
184
|
const manifest = await json(join(directory, "manifest.json"));
|
|
145
185
|
const { problems, hints } = manifestProblems(manifest, directory);
|
|
@@ -165,7 +205,7 @@ function displayFiles(target) {
|
|
|
165
205
|
const field = name === "README.md" ? "readme" : "changelog";
|
|
166
206
|
if (
|
|
167
207
|
target.manifest[field] === undefined &&
|
|
168
|
-
|
|
208
|
+
present(join(target.directory, name))
|
|
169
209
|
)
|
|
170
210
|
files.add(name);
|
|
171
211
|
}
|
|
@@ -200,7 +240,7 @@ function dependencies(target) {
|
|
|
200
240
|
};
|
|
201
241
|
if (
|
|
202
242
|
!Object.keys(deps).length ||
|
|
203
|
-
|
|
243
|
+
present(join(target.directory, "node_modules"))
|
|
204
244
|
)
|
|
205
245
|
return;
|
|
206
246
|
console.log(`${target.name}: installing dependencies (npm ci)`);
|
|
@@ -221,7 +261,7 @@ function* upwards(directory) {
|
|
|
221
261
|
for (let current = directory; ;) {
|
|
222
262
|
yield current;
|
|
223
263
|
const parent = dirname(current);
|
|
224
|
-
if (parent === current ||
|
|
264
|
+
if (parent === current || present(join(current, ".git"))) return;
|
|
225
265
|
current = parent;
|
|
226
266
|
}
|
|
227
267
|
}
|
|
@@ -235,13 +275,13 @@ function checkConfig(target, sources) {
|
|
|
235
275
|
const roots = [];
|
|
236
276
|
for (const directory of upwards(target.directory)) {
|
|
237
277
|
const types = join(directory, "node_modules/@types");
|
|
238
|
-
if (
|
|
278
|
+
if (present(types)) roots.push(types);
|
|
239
279
|
}
|
|
240
280
|
const node = resolveFrom(target.directory, "@types/node/package.json");
|
|
241
281
|
if (node) roots.push(dirname(dirname(node)));
|
|
242
282
|
const base = [...upwards(target.directory)]
|
|
243
283
|
.map((directory) => join(directory, "tsconfig.json"))
|
|
244
|
-
.find((file) =>
|
|
284
|
+
.find((file) => present(file));
|
|
245
285
|
return {
|
|
246
286
|
...(base ? { extends: base } : {}),
|
|
247
287
|
compilerOptions: {
|
|
@@ -262,7 +302,7 @@ async function check(target) {
|
|
|
262
302
|
const own = (await files(target.directory)).filter(
|
|
263
303
|
(file) => isTypeScript(file) || file.endsWith(".mts"),
|
|
264
304
|
);
|
|
265
|
-
const tests =
|
|
305
|
+
const tests = present(join(target.directory, "tests"))
|
|
266
306
|
? (await readdir(join(target.directory, "tests")))
|
|
267
307
|
.filter(isTypeScript)
|
|
268
308
|
.map((file) => join("tests", file))
|
|
@@ -285,6 +325,113 @@ async function check(target) {
|
|
|
285
325
|
await rm(scratch, { recursive: true, force: true });
|
|
286
326
|
}
|
|
287
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`);
|
|
288
435
|
}
|
|
289
436
|
|
|
290
437
|
function version(target, flag) {
|
|
@@ -308,7 +455,7 @@ function entries(target) {
|
|
|
308
455
|
const entry = target.manifest.entry.replace(/^dist\//, "");
|
|
309
456
|
const candidates = [entry.replace(/\.js$/, ".ts"), entry];
|
|
310
457
|
const found = candidates.find((file) =>
|
|
311
|
-
|
|
458
|
+
present(join(target.directory, file)),
|
|
312
459
|
);
|
|
313
460
|
if (!found)
|
|
314
461
|
fail(`${target.name}: entry ${target.manifest.entry} has no source file`);
|
|
@@ -441,6 +588,14 @@ async function build(target, flag) {
|
|
|
441
588
|
entry.built,
|
|
442
589
|
await readFile(join(out, entry.built), "utf8"),
|
|
443
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}`);
|
|
444
599
|
await writeFile(join(out, "package.json"), '{"type":"module"}\n');
|
|
445
600
|
await writeFile(
|
|
446
601
|
join(out, "manifest.json"),
|
|
@@ -454,7 +609,7 @@ async function build(target, flag) {
|
|
|
454
609
|
|
|
455
610
|
async function test(target) {
|
|
456
611
|
const directory = join(target.directory, "tests");
|
|
457
|
-
const tests =
|
|
612
|
+
const tests = present(directory)
|
|
458
613
|
? (await readdir(directory))
|
|
459
614
|
.filter((file) => /\.test\.(ts|mjs)$/.test(file))
|
|
460
615
|
.sort()
|
|
@@ -544,8 +699,11 @@ async function pack(target, flag, outDirectory) {
|
|
|
544
699
|
);
|
|
545
700
|
}
|
|
546
701
|
|
|
547
|
-
const USAGE =
|
|
548
|
-
"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");
|
|
549
707
|
|
|
550
708
|
async function main() {
|
|
551
709
|
const args = process.argv.slice(2);
|
|
@@ -561,10 +719,25 @@ async function main() {
|
|
|
561
719
|
console.log(USAGE);
|
|
562
720
|
return;
|
|
563
721
|
}
|
|
564
|
-
|
|
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)) {
|
|
565
730
|
console.error(USAGE);
|
|
566
731
|
process.exit(2);
|
|
567
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");
|
|
568
741
|
const flag = option("--version");
|
|
569
742
|
const out = option("--out");
|
|
570
743
|
const directories = args.length ? args : ["."];
|
|
@@ -574,7 +747,9 @@ async function main() {
|
|
|
574
747
|
for (const directory of directories) {
|
|
575
748
|
try {
|
|
576
749
|
const target = await plugin(directory);
|
|
577
|
-
if (
|
|
750
|
+
if (subcommand === "extract") await i18nExtract(target, locales);
|
|
751
|
+
else if (subcommand === "check") await i18nCheck(target);
|
|
752
|
+
else if (command === "check") await check(target);
|
|
578
753
|
else if (command === "build") await build(target, flag);
|
|
579
754
|
else if (command === "pack") await pack(target, flag, out);
|
|
580
755
|
else await test(target);
|
|
@@ -585,7 +760,9 @@ async function main() {
|
|
|
585
760
|
}
|
|
586
761
|
}
|
|
587
762
|
if (failed.length) {
|
|
588
|
-
console.error(
|
|
763
|
+
console.error(
|
|
764
|
+
`${subcommand ? `i18n ${subcommand}` : command} failed for: ${failed.join(", ")}`,
|
|
765
|
+
);
|
|
589
766
|
process.exit(1);
|
|
590
767
|
}
|
|
591
768
|
}
|
package/dist/commands.d.ts
CHANGED
|
@@ -20,8 +20,7 @@ import type { Transport } from "./wire.ts";
|
|
|
20
20
|
*/
|
|
21
21
|
export declare const COMMAND_LIMITS: {
|
|
22
22
|
/**
|
|
23
|
-
* Commands one plugin instance may have at the same time (160).
|
|
24
|
-
* package is its own instance.
|
|
23
|
+
* Commands one plugin instance may have at the same time (160).
|
|
25
24
|
*/
|
|
26
25
|
readonly perPlugin: 160;
|
|
27
26
|
/** Commands of every plugin plus the player's own, together (1024). */
|
package/dist/commands.js
CHANGED
|
@@ -18,8 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
export const COMMAND_LIMITS = {
|
|
20
20
|
/**
|
|
21
|
-
* Commands one plugin instance may have at the same time (160).
|
|
22
|
-
* package is its own instance.
|
|
21
|
+
* Commands one plugin instance may have at the same time (160).
|
|
23
22
|
*/
|
|
24
23
|
perPlugin: 160,
|
|
25
24
|
/** Commands of every plugin plus the player's own, together (1024). */
|
|
@@ -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;
|