@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/llms-full.txt CHANGED
@@ -1,4 +1,4 @@
1
- # @vestra-player/sdk 1.5.7: full reference
1
+ # @vestra-player/sdk 1.5.9: full reference
2
2
 
3
3
  > Everything needed to write a Vestra Player plugin (plugin API `vestra/1`): the guides, every public declaration of `@vestra-player/sdk` and `@vestra-player/sdk/testing` with its documentation, and the complete source of the example plugins. Generated from the SDK's sources by scripts/llms.mjs; the short index is llms.txt.
4
4
 
@@ -124,7 +124,7 @@ A manifest with every field. `id`, `apiVersion`, `entry` and `capabilities` are
124
124
  }
125
125
  ```
126
126
 
127
- Unknown fields, unknown capabilities and paths that leave the package directory (including through symbolic links) are rejected. The id's characters are ASCII letters, digits, `.`, `-` and `_`; the official plugin repository additionally requires `<GitHub user or organization>.<plugin name>`. The `vestra.` prefix (and the id `vestra`, in any case) is reserved for the plugins built into the player: from player 0.4.1 the player refuses to install or trial such a package (`reserved`). Every other plugin is a third-party plugin, including those hosted in the official repository.
127
+ Unknown fields, unknown capabilities and paths that leave the package directory (including through symbolic links) are rejected. The id's characters are ASCII letters, digits, `.`, `-` and `_`; the official plugin repository additionally requires `<GitHub user or organization>.<plugin name>`. The `vestra.` prefix (and the id `vestra`, in any case) is reserved for the plugins built into the player: from player 0.4.1 the player refuses to install such a package (`reserved`). Every other plugin is a third-party plugin, including those hosted in the official repository.
128
128
 
129
129
  | Capability | Opens |
130
130
  | -------------------------------------- | ------------------------------------------------------------------------------------------------------- |
@@ -998,9 +998,8 @@ change).
998
998
  | `timeout` | A time limit ran out (`openStream` start-up, `tools.run`, `system.*` after 10 s). |
999
999
  | `stream`, `device` | `openStream`: the producer failed / the audio device is not ready. |
1000
1000
  | `backend-uncertain` | The player's backend blocked a filter stage; call `retry()`. |
1001
- | `cleanup` | PackageTrial.stop could not confirm cleanup; call it again. |
1002
1001
  | `limit` | A player-wide limit was reached (for example the number of commands). |
1003
- | `reserved` | Packages.validate, PackageCandidate.trial, PackageCandidate.install: the id is `vestra` or starts with `vestra.` (any case), which only the player's built-in plugins use (player 0.4.1). |
1002
+ | `reserved` | Packages.validate, PackageCandidate.install: the id is `vestra` or starts with `vestra.` (any case), which only the player's built-in plugins use (player 0.4.1). |
1004
1003
  | `versionMismatch` | The package needs a newer player (`minPlayerVersion`) or another `apiVersion`. |
1005
1004
  | `ui-prepare` | The player could not prepare a mounted panel or a settings page (its Slint file does not compile, a bound property is not declared, or another plugin's panel holds the slot); `message` has the diagnostics. |
1006
1005
  | `ui` | The player could not apply a property to a tool window or a settings page, or could not build the Options window (a property the Slint file does not declare, a value of the wrong type); `message` has Slint's reason. |
@@ -1080,7 +1079,7 @@ interface Scope extends Disposable {
1080
1079
  readonly media: MediaCapture;
1081
1080
  /** Page-URL to stream-URL resolvers (`source.resolve`). */
1082
1081
  readonly resolvers: Resolvers;
1083
- /** The installed plugins; validation, trials, installs (`plugins.manage` for changes). */
1082
+ /** The installed plugins; validation, installs (`plugins.manage` for changes). */
1084
1083
  readonly packages: Packages;
1085
1084
  /** On-screen messages over the video (`ui.osd`). */
1086
1085
  readonly osd: Osd;
@@ -1110,8 +1109,10 @@ interface Scope extends Disposable {
1110
1109
  readonly timeline: Timeline;
1111
1110
  /** Files dropped on the video area, by extension (`ui.drop`). */
1112
1111
  readonly drops: Drops;
1113
- /** The player's palette. */
1112
+ /** The player's palette and the saved interface language. */
1114
1113
  readonly appearance: Appearance;
1114
+ /** The interface language of this run (read-only). */
1115
+ readonly i18n: I18n;
1115
1116
  /** Pages in the player's options window (`ui.settings`). */
1116
1117
  readonly settings: Settings;
1117
1118
  /** A child scope; disposing it releases only what was created through it. The child is itself owned by this scope. */
@@ -1174,8 +1175,7 @@ interface Plugin extends Scope {
1174
1175
  readonly id: string;
1175
1176
  /**
1176
1177
  * A writable directory for this plugin's own files (settings, remembered
1177
- * folders). Kept across restarts, disabling and upgrades; a trial gets a
1178
- * temporary one. Write files atomically: to a temporary name, then rename.
1178
+ * folders). Kept across restarts, disabling and upgrades. Write files atomically: to a temporary name, then rename.
1179
1179
  * Throws `unavailable` when the player provided none.
1180
1180
  *
1181
1181
  * Location: `~/Library/Application Support/Vestra/plugin-data/<id>/` on
@@ -1218,9 +1218,9 @@ type Bindable<T> = T | (() => T);
1218
1218
  ### MenuId
1219
1219
 
1220
1220
  A built-in top-level menu of the player, named in a command path as
1221
- `@<id>` (see CommandOptions.path): Open (打开), Playback (播放),
1222
- Subtitles (字幕), Video (视频), Audio (声音), Screen (屏幕), Settings (设置).
1223
- The list is `MENU_IDS` in `@vestra-player/sdk/testing`.
1221
+ `@<id>` or by its English key (see CommandOptions.path): Open,
1222
+ Playback, Subtitles, Video, Audio, Screen, Settings. The list is
1223
+ `MENU_IDS` in `@vestra-player/sdk/testing`.
1224
1224
 
1225
1225
  ```ts
1226
1226
  type MenuId = "open" | "playback" | "subtitles" | "video" | "audio" | "screen" | "settings";
@@ -1241,7 +1241,7 @@ Built-in menus keep their place. The seven built-in top-level menus
1241
1241
  (MenuId) sit at fixed orders, whatever is inside them: Open 110,
1242
1242
  Playback 200, Subtitles 300, Video 400, Audio 500, Screen 710, Settings
1243
1243
  830 (player 0.3.0). This holds whether the path names the menu by id
1244
- (`["@video", "Blur"]`) or by its title (`["视频", "Blur"]`), so a plugin's
1244
+ (`["@video", "Blur"]`) or by its key (`["Video", "Blur"]`), so a plugin's
1245
1245
  item never moves the player's menu. Directly inside a built-in menu,
1246
1246
  `order` 0 (the default) sorts after every other order: an item without
1247
1247
  `order` goes at the end, after the player's own items (which all use 100
@@ -1265,16 +1265,23 @@ interface CommandOptions {
1265
1265
  * one segment is a top-level item. Omit it for a command that is not in the
1266
1266
  * main menu (bindings and panel menus only); `title` is then required.
1267
1267
  *
1268
- * Segments are menu titles, matched exactly. The player's own menus have
1269
- * Chinese titles, so `["Video", "Blur"]` would add a second top-level menu
1270
- * "Video". To join a built-in menu, start the path with its id, `@` plus a
1271
- * MenuId: `@open`, `@playback`, `@subtitles`, `@video`, `@audio`,
1272
- * `@screen` or `@settings`. Only the first segment is read as an id; an
1273
- * unknown id, or an id with nothing after it, fails with `invalid`. Needs
1274
- * player 0.3.0 (set `"minPlayerVersion": "0.3.0"`).
1268
+ * Segments are English source text, and menus merge by them: commands
1269
+ * of different plugins with the same leading segments share a submenu.
1270
+ * The player shows each segment and the title in the interface language
1271
+ * from the declaring plugin's catalog (`locales/<lang>.po`, see the
1272
+ * i18n guide); the player's own menus
1273
+ * (Open, Playback, Subtitles, Video, Audio, Screen, Settings, and
1274
+ * Playback ▸ Speed, Playback ▸ Seek) always show the player's words.
1275
+ * A built-in menu can also be named by its id, `@` plus a MenuId:
1276
+ * `@open`, `@playback`, `@subtitles`, `@video`, `@audio`, `@screen` or
1277
+ * `@settings`. Only the first segment is read as an id; an unknown id, or
1278
+ * an id with nothing after it, fails with `invalid`. Players up to 0.4.2
1279
+ * titled their menus in Chinese (视频…): there `["Video", "Blur"]` is a
1280
+ * menu of its own, so plugins for those players write `@video`; player
1281
+ * 0.4.3 still accepts the old Chinese titles of its own menus.
1275
1282
  */
1276
1283
  path?: string[];
1277
- /** Menu label; defaults to the last path segment. */
1284
+ /** Menu label (English source text, shown translated); defaults to the last path segment. */
1278
1285
  title?: string;
1279
1286
  /**
1280
1287
  * Sibling order in the menu, ascending (integer, default 0). A submenu sits where its first command sits, except a built-in menu, which keeps its fixed place. Directly inside a built-in menu, 0 (the default) sorts last. Equal orders: plugin id, then declaration order. Range ±1,000,000. See CommandOptions for the menu order rules.
@@ -1903,6 +1910,64 @@ interface Appearance {
1903
1910
  * @throws capability (rejects) The manifest does not declare `ui.theme`.
1904
1911
  */
1905
1912
  set(theme: ThemeId): Promise<void>;
1913
+ /** The saved interface language: `"system"` (follow the system's languages, the default) or a Locale. */
1914
+ readonly language: LanguagePreference;
1915
+ /**
1916
+ * Save the interface language for the next start.
1917
+ * @capability ui.theme
1918
+ * @player 0.4.3
1919
+ * @param language - `"system"` or a Locale.
1920
+ * @returns Resolves once the player saved it; Appearance.language has the new value.
1921
+ * @throws invalid (rejects) `language` is not `"system"` or a Locale.
1922
+ * @throws capability (rejects) The manifest does not declare `ui.theme`.
1923
+ * @throws unsupported (rejects) A player before 0.4.3.
1924
+ */
1925
+ setLanguage(language: LanguagePreference): Promise<void>;
1926
+ }
1927
+ ```
1928
+
1929
+ ### Locale
1930
+
1931
+ An interface language of the player: English (`en`, the source language
1932
+ of every plugin text) or a translation. The list is `LOCALES` in
1933
+ `@vestra-player/sdk/testing`.
1934
+
1935
+ ```ts
1936
+ type Locale = "en" | "zh-Hans" | "ja" | "ko" | "de" | "fr" | "es";
1937
+ ```
1938
+
1939
+ ### LanguagePreference
1940
+
1941
+ The saved interface language (Appearance.language): follow the
1942
+ system, or one Locale.
1943
+
1944
+ ```ts
1945
+ type LanguagePreference = "system" | Locale;
1946
+ ```
1947
+
1948
+ ### TemplateArgs
1949
+
1950
+ Arguments of a template sentence (Osd.show,
1951
+ OsdPanel.set, ConfirmOptions.args): an array for `{}` and
1952
+ `{0}`, `{1}` placeholders, or an object for `{name}` placeholders. At most
1953
+ 16 strings and finite numbers; whole numbers print without a fraction.
1954
+
1955
+ ```ts
1956
+ type TemplateArgs = readonly (string | number)[] | Readonly<Record<string, string | number>>;
1957
+ ```
1958
+
1959
+ ### I18n
1960
+
1961
+ The interface language of this run. `plugin.i18n`.
1962
+
1963
+ - @capability None.
1964
+
1965
+ ```ts
1966
+ interface I18n {
1967
+ /**
1968
+ * The interface language of this run, chosen at start-up from the saved Appearance.language or the system's languages; English when neither names a Locale. Players before 0.4.3 report `"en"`.
1969
+ */
1970
+ readonly locale: Locale;
1906
1971
  }
1907
1972
  ```
1908
1973
 
@@ -2281,6 +2346,10 @@ interface ConfirmOptions {
2281
2346
  destructive?: boolean;
2282
2347
  /** A checkbox under the text, such as "Don't ask again"; 1–40 characters. */
2283
2348
  checkbox?: string;
2349
+ /**
2350
+ * Values for the placeholders of `message` and `detail` (player 0.4.3). Every text of the dialog is English source text that the player shows translated from the plugin's catalog; `message` and `detail` are templates like Osd.show's. The length limits apply to the shown text.
2351
+ */
2352
+ args?: TemplateArgs;
2284
2353
  }
2285
2354
  ```
2286
2355
 
@@ -2903,9 +2972,10 @@ interface OsdPanel extends Disposable {
2903
2972
  /**
2904
2973
  * Show the block with this text, change it, or hide it.
2905
2974
  * @capability ui.osd
2906
- * @param text - The text, with `\n` between lines: at most 32 lines and 4096 bytes. `null` hides the block.
2975
+ * @param text - The text, with `\n` between lines: at most 32 lines and 4096 bytes once translated and formatted. `null` hides the block. A template, like Osd.show's.
2976
+ * @param args - Values for its placeholders (player 0.4.3).
2907
2977
  */
2908
- set(text: string | null): void;
2978
+ set(text: string | null, args?: TemplateArgs): void;
2909
2979
  }
2910
2980
  ```
2911
2981
 
@@ -2921,13 +2991,14 @@ interface Osd {
2921
2991
  /**
2922
2992
  * Show a one-line message at the top left of the video for 2 seconds.
2923
2993
  * @capability ui.osd
2924
- * @param text - 1–200 characters; line breaks become spaces.
2994
+ * @param text - The template, 1–200 characters once translated and formatted; line breaks become spaces.
2995
+ * @param args - Values for its placeholders (player 0.4.3; older players show the template as it is).
2925
2996
  * @returns Resolves once the message is on screen.
2926
- * @throws invalid (rejects) `text` is empty, only white space, or longer than 200 characters.
2997
+ * @throws invalid (rejects) The shown text is empty, only white space, or longer than 200 characters, or `args` is not a TemplateArgs.
2927
2998
  * @throws unavailable (rejects) The player runs without a user interface.
2928
2999
  * @throws capability (rejects) The manifest does not declare `ui.osd`.
2929
3000
  */
2930
- show(text: string): Promise<void>;
3001
+ show(text: string, args?: TemplateArgs): Promise<void>;
2931
3002
  /**
2932
3003
  * Create this plugin's information block.
2933
3004
  * @capability ui.osd
@@ -3450,6 +3521,20 @@ interface Resolvers {
3450
3521
  * Register a resolver. When anyone opens a matching URL or file the player calls `resolve`; `signal` aborts when the open is cancelled or replaced. One scheme + host pair, and one extension, has one resolver (`conflict` otherwise). A rejected `resolve` fails the open with the error's message.
3451
3522
  */
3452
3523
  add(options: ResolverOptions): Resolver;
3524
+ /**
3525
+ * The extensions some plugin's file resolver handles (SDK 1.5.8, player 0.4.2): lower case, without the dot, sorted, each once; not who registered them. Reading needs no capability; `[]` from older players. Without `events` it is the list from when the plugin first read it. Use it to accept such files beside the player's own media types, for example in a playlist's drop or folder scan.
3526
+ * @capability None.
3527
+ * @player 0.4.2
3528
+ */
3529
+ readonly fileTypes: readonly string[];
3530
+ /**
3531
+ * Resolvers.fileTypes changed: a file resolver was added, or its plugin stopped. Needs `events`; without it the listener never fires.
3532
+ * @param listener - Receives the new list.
3533
+ * @returns A handle that removes the listener.
3534
+ * @capability `events`
3535
+ * @player 0.4.2
3536
+ */
3537
+ onFileTypesChange(listener: (types: string[]) => void): Disposable;
3453
3538
  }
3454
3539
  ```
3455
3540
 
@@ -3555,8 +3640,6 @@ interface PackageCandidate {
3555
3640
  readonly manifest: PackageManifest;
3556
3641
  /** The checks the player ran. */
3557
3642
  readonly checks: string[];
3558
- /** Load it temporarily alongside the installed plugins. A reserved id (`vestra.*`) fails with `reserved`. */
3559
- trial(): Promise<PackageTrial>;
3560
3643
  /**
3561
3644
  * Install it. The same source upgrades the installed plugin (its data stays); another source fails with `conflict` unless `replace` is set; a built-in id fails with `unsupported` (built-ins update with the player). A reserved id (`vestra` or `vestra.*`, which every built-in uses) fails with `reserved` (player 0.4.1; already when validated), as only the player installs built-in plugins.
3562
3645
  */
@@ -3564,21 +3647,6 @@ interface PackageCandidate {
3564
3647
  }
3565
3648
  ```
3566
3649
 
3567
- ### PackageTrial
3568
-
3569
- A trial load of a package. Disposing it stops the trial and waits for cleanup.
3570
-
3571
- ```ts
3572
- interface PackageTrial extends Disposable {
3573
- /** The trial's status as the player reports it. */
3574
- readonly status: string;
3575
- /**
3576
- * Stop and wait until the host confirmed cleanup. Rejects with code `cleanup` when the host stopped the trial without confirming cleanup; calling `stop()` again then asks the host again.
3577
- */
3578
- stop(): Promise<void>;
3579
- }
3580
- ```
3581
-
3582
3650
  ### PackageChangeStatus
3583
3651
 
3584
3652
  The progress of an install, rollback or uninstall.
@@ -3617,8 +3685,8 @@ interface PluginInfo {
3617
3685
  id: string;
3618
3686
  /** Version, `null` when unknown. */
3619
3687
  version: string | null;
3620
- /** From the install library, a `--plugin` development load, or a trial. */
3621
- origin: "installed" | "development" | "trial";
3688
+ /** From the install library, or a `--plugin` development load. */
3689
+ origin: "installed" | "development";
3622
3690
  /** Run state. */
3623
3691
  state: "starting" | "running" | "disabled" | "failed" | "stopped";
3624
3692
  /** Why it failed (`state: "failed"`). */
@@ -3631,7 +3699,7 @@ interface PluginInfo {
3631
3699
  previousVersion: string | null;
3632
3700
  /** Shipped with the player: it can be disabled, not uninstalled or replaced. `false` from players before 0.2.0. */
3633
3701
  builtin: boolean;
3634
- /** Where an installed plugin came from; `null` for development loads and trials. */
3702
+ /** Where an installed plugin came from; `null` for development loads. */
3635
3703
  source: PackageSource | null;
3636
3704
  /** Its data directory (`plugin.dataDirectory` of that plugin); `null` when the player has none. The path's layout is not part of the API. */
3637
3705
  dataDirectory: string | null;
@@ -3639,6 +3707,28 @@ interface PluginInfo {
3639
3707
  * What a running plugin was refused because another plugin holds it: a panel position (`slot-occupied`, with `slot`) or the next/previous sequence (`sequence-taken`). Neither fails activation; `error` has the same as text. Empty from players before 0.4.x (after 0.4.0). Always set by the SDK; optional so that code building `PluginInfo` values itself (tests) keeps compiling.
3640
3708
  */
3641
3709
  warnings?: readonly PluginWarning[];
3710
+ /**
3711
+ * The manifest's display texts in the interface language, translated with that package's catalog; absent from players before 0.4.3 (read `manifest.json` there).
3712
+ */
3713
+ display?: PluginDisplay;
3714
+ }
3715
+ ```
3716
+
3717
+ ### PluginDisplay
3718
+
3719
+ PluginInfo.display: a package's `name`, `punchline`,
3720
+ `description` and `tags` as the player shows them.
3721
+
3722
+ ```ts
3723
+ interface PluginDisplay {
3724
+ /** `name`, translated; `null` when the manifest has none. */
3725
+ readonly name: string | null;
3726
+ /** `punchline`, translated; `null` when the manifest has none. */
3727
+ readonly punchline: string | null;
3728
+ /** `description`, translated; `null` when the manifest has none. */
3729
+ readonly description: string | null;
3730
+ /** `tags`, each translated. */
3731
+ readonly tags: readonly string[];
3642
3732
  }
3643
3733
  ```
3644
3734
 
@@ -3667,7 +3757,13 @@ need no capability; everything else needs `plugins.manage`.
3667
3757
  ```ts
3668
3758
  interface Packages {
3669
3759
  /**
3670
- * Validate a package directory (`plugins.manage`). A package whose id is reserved for the built-in plugins (`vestra` or `vestra.*`) fails with `reserved` (player 0.4.1).
3760
+ * Validate a package directory (`plugins.manage`). A package whose id is
3761
+ * reserved for the built-in plugins (`vestra` or `vestra.*`) fails with
3762
+ * `reserved` (player 0.4.1).
3763
+ *
3764
+ * The player keeps the 32 most recent candidates of all plugins; an
3765
+ * older one's `install` fails with `stale` (validate again). Before
3766
+ * player 0.4.2 the 33rd validation of a run failed with `busy` instead.
3671
3767
  */
3672
3768
  validate(directory: string): Promise<PackageCandidate>;
3673
3769
  /**
@@ -3681,11 +3777,11 @@ interface Packages {
3681
3777
  /** The new list after any start, stop, failure, install or uninstall. No capability. */
3682
3778
  onChange(listener: (plugins: PluginInfo[]) => void): Disposable;
3683
3779
  /**
3684
- * Stop or start a plugin (`plugins.manage`). An installed plugin stays disabled across launches; a development load only for this run. `invalid` to disable oneself, `unsupported` for a trial, `missing` unknown. `busy` while it is being installed.
3780
+ * Stop or start a plugin (`plugins.manage`). An installed plugin stays disabled across launches; a development load only for this run. `invalid` to disable oneself, `missing` unknown. `busy` while it is being installed.
3685
3781
  */
3686
3782
  setEnabled(id: string, enabled: boolean): Promise<void>;
3687
3783
  /**
3688
- * Remove an installed plugin (`plugins.manage`); `removeData` also deletes its data directory, otherwise reinstalling it from the same source reconnects to its data. Its user bindings stay (as orphans). `unsupported` for built-ins, development loads and trials, `missing` unknown, `invalid` for oneself.
3784
+ * Remove an installed plugin (`plugins.manage`); `removeData` also deletes its data directory, otherwise reinstalling it from the same source reconnects to its data. Its user bindings stay (as orphans). `unsupported` for built-ins and development loads, `missing` unknown, `invalid` for oneself.
3689
3785
  */
3690
3786
  uninstall(id: string, options?: {
3691
3787
  removeData?: boolean;
@@ -4243,6 +4339,15 @@ TestHost.coreAction (the player's core actions).
4243
4339
  interface FakeCommand {
4244
4340
  /** What the plugin declared: id, menu path, title, order, group, default bindings, focus and repeat. Wire-level shape; it may gain fields in minor versions. */
4245
4341
  declaration: CommandDeclaration;
4342
+ /**
4343
+ * What the menu shows, as the player shows it in the test host's TestHost.locale: the title and each path segment translated with the plugin's catalog (the player's own menus in the player's words), the keys normalized (`@video` is "Video").
4344
+ */
4345
+ readonly display: {
4346
+ /** The menu text of the item. */
4347
+ title: string;
4348
+ /** Each segment of the path as shown; empty without a path. */
4349
+ path: string[];
4350
+ };
4246
4351
  /** The command state the player last received (after the update pass). */
4247
4352
  state: {
4248
4353
  /** `false` when the plugin disabled the command. */
@@ -4380,10 +4485,62 @@ class TestHost implements Transport {
4380
4485
  */
4381
4486
  sidebar: LayoutSlot | null;
4382
4487
  };
4383
- /** Every `osd.show` text, newest last. */
4488
+ /**
4489
+ * Every `osd.show` text as the player shows it (translated with the plugin's catalog and formatted with its args), newest last. The template and args the plugin sent are in `host.sent("osd.show")`.
4490
+ */
4384
4491
  readonly osd: string[];
4385
- /** The text of this plugin's osd panel, `null` when hidden. */
4492
+ /** The text of this plugin's osd panel as shown, `null` when hidden. */
4386
4493
  osdPanel: string | null;
4494
+ /**
4495
+ * The interface language the test host plays (`plugin.i18n.locale`), `"en"`
4496
+ * by default. Set it before startPlugin (or pass its `locale`
4497
+ * option): the plugin's catalog for it is read from the package's
4498
+ * `locales/` then, and the menus (FakeCommand.display), OSD
4499
+ * messages, dialog texts, settings page and panel titles show its
4500
+ * translations, the way the player shows them.
4501
+ *
4502
+ * The player's own texts (core actions' messages and menus, its
4503
+ * refusals) are English, or the player's Simplified Chinese for
4504
+ * `"zh-Hans"`. While `locale` was never set they stay in Chinese, as
4505
+ * players up to 0.4.2 showed them, so tests written for SDK 1.5.8 keep
4506
+ * passing; set `locale` to test what a user sees now.
4507
+ */
4508
+ get locale(): Locale;
4509
+ set locale(locale: Locale);
4510
+ /**
4511
+ * Read the plugin's translations for TestHost.locale from a package directory, as the player would (`locales/<locale>.json` of a built package, else the source `locales/<locale>.po`); `startPlugin` does this. None for English; a missing file means none; a broken `.po` throws.
4512
+ * @param directory - The package directory (holding `locales/`).
4513
+ */
4514
+ loadCatalog(directory: string): void;
4515
+ /**
4516
+ * A text of the plugin as the player shows it: translated with the plugin's catalog (TestHost.loadCatalog), then formatted with `args` when given.
4517
+ * @param text - English source text or template.
4518
+ * @param args - Values for its placeholders.
4519
+ * @returns The shown text.
4520
+ */
4521
+ translate(text: string, args?: TemplateArgs): string;
4522
+ /** The saved interface language (`plugin.appearance.language`), changed by `appearance.setLanguage` or TestHost.setLanguage. */
4523
+ language: LanguagePreference;
4524
+ /**
4525
+ * Simulate another plugin (or the user) saving the interface language: pushes `appearance.changed`.
4526
+ * @param language - `"system"` or a locale.
4527
+ */
4528
+ setLanguage(language: LanguagePreference): void;
4529
+ /**
4530
+ * Every `dialogs.confirm` as the player shows it: its texts translated (message and detail formatted with `args`), newest last. The options the plugin sent are in TestHost.confirms.
4531
+ */
4532
+ readonly confirmTexts: {
4533
+ /** The question. */
4534
+ message: string;
4535
+ /** The text under it, if any. */
4536
+ detail?: string;
4537
+ /** The confirm button. */
4538
+ confirm: string;
4539
+ /** The cancel button (the player's "Cancel" by default). */
4540
+ cancel: string;
4541
+ /** The checkbox, if any. */
4542
+ checkbox?: string;
4543
+ }[];
4387
4544
  /** The sequence provider's flags, `null` without a provider. */
4388
4545
  sequence: {
4389
4546
  /** The provider's `canNext`, as last evaluated. */
@@ -4721,6 +4878,12 @@ class TestHost implements Transport {
4721
4878
  /** Exact host names it resolves (a web resolver), with `schemes`. */
4722
4879
  hosts?: string[];
4723
4880
  }[];
4881
+ /** What `plugin.resolvers.fileTypes` reports: the extensions of this plugin's file resolvers and of TestHost.otherResolvers, sorted, each once. */
4882
+ fileTypes(): string[];
4883
+ /**
4884
+ * Push `resolvers.fileTypes` when TestHost.fileTypes changed: called after this plugin's resolvers come and go; call it yourself after changing TestHost.otherResolvers (another plugin started or stopped).
4885
+ */
4886
+ updateFileTypes(): void;
4724
4887
  /**
4725
4888
  * File opens waiting for this plugin's file resolver (`extensions`), by request id: the path and how it was opened. Opening a matching path (`player.open`, TestHost.openElsewhere) sends `source.resolveRequest`; the answer selects the stream, fails the open or, once the source changed, is `stale`. Any later source change cancels it (`source.resolveCancel`). Web resolvers are not simulated: emit `source.resolveRequest` yourself.
4726
4889
  */
@@ -4905,7 +5068,7 @@ class TestHost implements Transport {
4905
5068
  /** Its version, `null` when unknown. */
4906
5069
  version: string | null;
4907
5070
  /** Where it was loaded from. */
4908
- origin: "installed" | "development" | "trial";
5071
+ origin: "installed" | "development";
4909
5072
  /** Its run state. */
4910
5073
  state: "starting" | "running" | "disabled" | "failed" | "stopped";
4911
5074
  /** Why it failed (`state: "failed"`). */
@@ -5035,6 +5198,10 @@ function startPlugin(activate: (plugin: Plugin) => unknown, options?: {
5035
5198
  host?: TestHost;
5036
5199
  /** Use an existing directory as `plugin.dataDirectory` (to test a restart). */
5037
5200
  dataDirectory?: string;
5201
+ /**
5202
+ * The interface language to play (TestHost.locale): the plugin's `locales/<locale>.po` (or `.json`) in `directory` is loaded and its menus, messages and titles show translated. Default: the host's.
5203
+ */
5204
+ locale?: Locale;
5038
5205
  }): Promise<{
5039
5206
  /** The test host the plugin talks to. */
5040
5207
  host: TestHost;
@@ -5059,7 +5226,7 @@ own sensible display cap well below `perPlugin`.
5059
5226
 
5060
5227
  ```ts
5061
5228
  const COMMAND_LIMITS: {
5062
- /** Commands one plugin instance may have at the same time (160). A trial package is its own instance. */
5229
+ /** Commands one plugin instance may have at the same time (160). */
5063
5230
  readonly perPlugin: 160;
5064
5231
  /** Commands of every plugin plus the player's own, together (1024). */
5065
5232
  readonly total: 1024;
@@ -5080,6 +5247,16 @@ agrees with the player's.
5080
5247
  const MENU_IDS: readonly ["open", "playback", "subtitles", "video", "audio", "screen", "settings"];
5081
5248
  ```
5082
5249
 
5250
+ ### LOCALES
5251
+
5252
+ The player's interface languages: English (the source language) first,
5253
+ then the translations, as the player's own table (its tests check that
5254
+ they agree).
5255
+
5256
+ ```ts
5257
+ const LOCALES: readonly ["en", "zh-Hans", "ja", "ko", "de", "fr", "es"];
5258
+ ```
5259
+
5083
5260
  ### HostError
5084
5261
 
5085
5262
  A definite refusal from the player (or the plugin runner), with a stable
@@ -5214,6 +5391,8 @@ interface Transport {
5214
5391
  on(event: string, listener: (data: unknown) => void): () => void;
5215
5392
  /** Replaces the real local services (test host). */
5216
5393
  local?: LocalServices;
5394
+ /** The interface language of this run (`plugin.i18n.locale`): the runner passes the player's `VESTRA_LOCALE`, the test host its `locale`. */
5395
+ locale?: string;
5217
5396
  }
5218
5397
  ```
5219
5398
 
@@ -7117,3 +7296,171 @@ test("the window shows sample.md, with the package as its base", async () => {
7117
7296
  await stop();
7118
7297
  });
7119
7298
  ```
7299
+
7300
+ ### Example: i18n
7301
+
7302
+ A plugin in the player's language: **Tools → Counter…** opens a window with one button that counts clicks. Every text is English in the source; `locales/zh-Hans.po` translates it into Simplified Chinese, and the player shows the translation when its interface language is Simplified Chinese.
7303
+
7304
+ | Capabilities | API |
7305
+ | --- | --- |
7306
+ | `commands`, `ui.window`, `ui.osd` | `commands.add`, `windows.create`, `osd.show(template, args)`; Slint `@tr`; `vestra-plugin i18n extract` and `check`; tests with `startPlugin({ locale })` |
7307
+
7308
+ #### Walkthrough
7309
+
7310
+ A plugin in the player's language. **Tools ▸ Counter…** opens a window with one button; each click counts and shows the count on the video. All texts are English in the source, and `locales/zh-Hans.po` translates them: with the player in Simplified Chinese the menu reads **工具 ▸ 计数器…** and the window **2 次点击**. The plugin contains no translation code.
7311
+
7312
+ #### The code
7313
+
7314
+ (code: `main.ts`, below)
7315
+
7316
+ - The menu path is English. The player shows each segment translated with the plugin's catalog; menus of all plugins merge by these English words.
7317
+ - The window gets data (`count`), not text.
7318
+ - plugin.osd.show takes a template and its arguments. The player translates `"Count: {}"`, then puts `count` into `{}`. A sentence built in TypeScript (`` `Count: ${count}` ``) could not be translated.
7319
+
7320
+ #### The window
7321
+
7322
+ (code: `ui/counter.slint`, below)
7323
+
7324
+ Every text is in `@tr("…")`. The player replaces the English literal with the translation when it loads the file; Slint then fills in the arguments. `"{n} click" | "{n} clicks" % root.count` has two forms: the first for 1, the second for every other number (`{n}` is the number).
7325
+
7326
+ #### The translation
7327
+
7328
+ `vestra-plugin i18n extract` collected the texts into `locales/messages.pot`; `extract --locale zh-Hans` started `locales/zh-Hans.po`, and each `msgstr` was then filled in:
7329
+
7330
+ (code: `locales/zh-Hans.po`, below)
7331
+
7332
+ Chinese has one plural form, so `msgstr[0]` is enough. `vestra-plugin i18n check` reports what is missing; `vestra-plugin build` compiles the file into `dist/locales/zh-Hans.json`, which the player reads.
7333
+
7334
+ #### The test
7335
+
7336
+ (code: `tests/main.test.ts`, below)
7337
+
7338
+ `startPlugin(activate, { directory, locale })` loads the catalog of that language. `host.command(id).display` is the menu as the user sees it, `declaration` what the code declared; `host.osd` lists the messages as shown.
7339
+
7340
+ #### examples/i18n/manifest.json
7341
+
7342
+ ```json
7343
+ {
7344
+ "id": "examples.i18n",
7345
+ "version": "0.1.0",
7346
+ "apiVersion": "vestra/1",
7347
+ "entry": "main.ts",
7348
+ "minPlayerVersion": "0.4.3",
7349
+ "capabilities": ["commands", "ui.window", "ui.osd"],
7350
+ "name": "Counter",
7351
+ "author": "Vestra Player SDK examples",
7352
+ "punchline": "A click counter in the player's language",
7353
+ "description": "Tools ▸ Counter… opens a window with one button that counts clicks.\nSDK example: a plugin translated into Simplified Chinese.",
7354
+ "tags": ["example"]
7355
+ }
7356
+ ```
7357
+
7358
+ #### examples/i18n/main.ts
7359
+
7360
+ ```ts
7361
+ /**
7362
+ * Counter: Tools ▸ Counter… opens a window with one button that counts
7363
+ * clicks, in the player's language. The texts are English; the player
7364
+ * translates them with locales/<lang>.po. Walkthrough: docs/examples/i18n.md.
7365
+ */
7366
+ import type { Plugin } from "@vestra-player/sdk";
7367
+
7368
+ export default function activate(plugin: Plugin) {
7369
+ let count = 0;
7370
+
7371
+ // The window's texts are in its Slint file (@tr); TypeScript passes data.
7372
+ const window = plugin.windows.create("ui/counter.slint");
7373
+ window.bind(() => ({ count }));
7374
+ window.on("add", () => {
7375
+ count += 1;
7376
+ // A template and its arguments, never a sentence built in TypeScript.
7377
+ return plugin.osd.show("Count: {}", [count]);
7378
+ });
7379
+
7380
+ // Declared texts are English; the player shows them translated.
7381
+ plugin.commands.add({
7382
+ id: "counter",
7383
+ path: ["Tools", "Counter…"],
7384
+ run: () => window.show(),
7385
+ });
7386
+ }
7387
+ ```
7388
+
7389
+ #### examples/i18n/ui/counter.slint
7390
+
7391
+ ```slint
7392
+ // The Counter window. Every text is @tr("English"): the player puts the
7393
+ // translation in its place when it loads the file. Walkthrough:
7394
+ // docs/examples/i18n.md.
7395
+ import { Theme } from "@vestra/theme.slint";
7396
+ import { Button } from "@vestra/widgets.slint";
7397
+
7398
+ export component CounterWindow inherits Window {
7399
+ title: @tr("Counter");
7400
+ width: 240px;
7401
+ background: Theme.bg-panel;
7402
+
7403
+ in property <int> count;
7404
+ callback add();
7405
+
7406
+ VerticalLayout {
7407
+ padding: Theme.dialog-padding;
7408
+ spacing: 8px;
7409
+ Text {
7410
+ // One form for 1, the other for every other number.
7411
+ text: @tr("{n} click" | "{n} clicks" % root.count);
7412
+ color: Theme.text;
7413
+ font-size: Theme.font-body;
7414
+ }
7415
+ Button {
7416
+ text: @tr("Add one");
7417
+ clicked => { root.add(); }
7418
+ }
7419
+ }
7420
+ }
7421
+ ```
7422
+
7423
+ #### examples/i18n/tests/main.test.ts
7424
+
7425
+ ```ts
7426
+ import assert from "node:assert/strict";
7427
+ import test from "node:test";
7428
+ import { startPlugin } from "@vestra-player/sdk/testing";
7429
+ import activate from "../main.ts";
7430
+
7431
+ const directory = new URL("..", import.meta.url); // manifest.json and locales/
7432
+
7433
+ test("English: the texts as written", async () => {
7434
+ const { host, stop } = await startPlugin(activate, {
7435
+ directory,
7436
+ locale: "en",
7437
+ });
7438
+ const counter = host.command("counter");
7439
+ assert.deepEqual(counter.display.path, ["Tools", "Counter…"]);
7440
+ await counter.run();
7441
+ host.window("counter").emit("add");
7442
+ await host.settle();
7443
+ assert.deepEqual(host.osd, ["Count: 1"]);
7444
+ await stop();
7445
+ });
7446
+
7447
+ test("Simplified Chinese: the menu and the message from locales/zh-Hans.po", async () => {
7448
+ const { host, plugin, stop } = await startPlugin(activate, {
7449
+ directory,
7450
+ locale: "zh-Hans",
7451
+ });
7452
+ assert.equal(plugin.i18n.locale, "zh-Hans");
7453
+ const counter = host.command("counter");
7454
+ // The key stays English; the menu shows the translation.
7455
+ assert.deepEqual(counter.declaration.path, ["Tools", "Counter…"]);
7456
+ assert.deepEqual(counter.display.path, ["工具", "计数器…"]);
7457
+ await counter.run();
7458
+ const window = host.window("counter");
7459
+ window.emit("add");
7460
+ window.emit("add");
7461
+ await host.settle();
7462
+ assert.equal(window.props["count"], 2);
7463
+ assert.deepEqual(host.osd, ["计数:1", "计数:2"]);
7464
+ await stop();
7465
+ });
7466
+ ```