@vestra-player/sdk 1.5.12 → 1.5.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.5.13
4
+
5
+ **`i18n extract` keeps the translator's comments and writes the right number of plural forms.** A fix; `apiVersion` stays `vestra/1`.
6
+
7
+ - Merging into a `locales/<lang>.po` kept only the header's text: comment lines before the header (`# Reviewed by an AI reviewer on 2026-10-25`, `# Translator: …`, `#, fuzzy`) and the translator's own `# ` comments on entries were dropped. Both are now kept as written (`parsePo` returns `headerComments`, entries carry `comments`; `writePo` writes them).
8
+ - A plural entry has exactly `nplurals` `msgstr[n]` lines, from the file's `Plural-Forms` (or the language: one for `ja`, `ko`, `zh-Hans`). Before, every plural entry got an empty `msgstr[1]`, also in one-form languages; a merge now removes such an extra empty line (a filled one stays).
9
+
3
10
  ## 1.5.12
4
11
 
5
12
  **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`.
@@ -648,10 +648,17 @@ export function extract(options) {
648
648
  const findings = extractor.findings.sort((a, b) => a.file === b.file ? a.line - b.line : a.file < b.file ? -1 : 1);
649
649
  return { messages: [...messages.values()], findings };
650
650
  }
651
- const entryOf = (message) => {
651
+ /** `msgstr[n]` lines a plural entry has: the header's `nplurals`, else the language's. */
652
+ const formsOf = (language, header) => {
653
+ const declared = /nplurals\s*=\s*(\d+)/.exec(header ?? "")?.[1];
654
+ if (declared !== undefined && Number(declared) >= 1)
655
+ return Number(declared);
656
+ return ONE_FORM.has(language) ? 1 : 2;
657
+ };
658
+ const entryOf = (message, forms = 2) => {
652
659
  const entry = {
653
660
  id: message.id,
654
- strings: message.plural === undefined ? [""] : ["", ""],
661
+ strings: message.plural === undefined ? [""] : Array(forms).fill(""),
655
662
  fuzzy: false,
656
663
  notes: [...message.notes],
657
664
  references: [...message.references],
@@ -669,7 +676,7 @@ export function template(manifest, messages) {
669
676
  const project = [manifest.id, manifest.version]
670
677
  .filter((part) => typeof part === "string")
671
678
  .join(" ");
672
- return writePo(`Project-Id-Version: ${project}\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: 8bit\n`, messages.map(entryOf));
679
+ return writePo(`Project-Id-Version: ${project}\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: 8bit\n`, messages.map((message) => entryOf(message)));
673
680
  }
674
681
  /** The header of a new `<lang>.po`. */
675
682
  export const newHeader = (language) => `Language: ${language}\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: 8bit\nPlural-Forms: ${pluralForms(language)}\n`;
@@ -687,20 +694,30 @@ export function merge(existing, language, messages) {
687
694
  old.set(keyOf(entry.context, entry.id), entry);
688
695
  const result = [];
689
696
  const present = new Set();
697
+ // A plural entry has exactly nplurals msgstr[n] (one for ja, ko,
698
+ // zh-Hans); extra empty ones are dropped, filled ones kept.
699
+ const forms = formsOf(language, parsed?.header ?? newHeader(language));
690
700
  let added = 0;
691
701
  let translated = 0;
692
702
  for (const message of messages) {
693
703
  const key = keyOf(message.context, message.id);
694
704
  present.add(key);
695
- const entry = entryOf(message);
705
+ const entry = entryOf(message, forms);
696
706
  const before = old.get(key);
707
+ // The translator's own comments stay with their message.
708
+ if (before?.comments?.length)
709
+ entry.comments = [...before.comments];
697
710
  if (before && hasText(before)) {
698
711
  entry.strings = [...before.strings];
699
712
  if (message.plural === undefined)
700
713
  entry.strings = [entry.strings[0]];
701
- else
702
- while (entry.strings.length < 2)
714
+ else {
715
+ while (entry.strings.length < forms)
703
716
  entry.strings.push("");
717
+ while (entry.strings.length > forms &&
718
+ entry.strings[entry.strings.length - 1] === "")
719
+ entry.strings.pop();
720
+ }
704
721
  entry.fuzzy = before.fuzzy;
705
722
  entry.flags = [...before.flags];
706
723
  }
@@ -718,7 +735,7 @@ export function merge(existing, language, messages) {
718
735
  obsolete++;
719
736
  }
720
737
  return {
721
- text: writePo(parsed?.header ?? newHeader(language), result),
738
+ text: writePo(parsed?.header ?? newHeader(language), result, parsed?.headerComments ?? []),
722
739
  total: messages.length,
723
740
  translated,
724
741
  added,
package/dist/i18n.d.ts CHANGED
@@ -25,16 +25,23 @@ export interface PoEntry {
25
25
  flags: string[];
26
26
  /** A `#~` entry: no longer in the source. */
27
27
  obsolete: boolean;
28
+ /** `# ` lines: the translator's own comments, kept as written. */
29
+ comments?: string[];
28
30
  }
29
31
  /** The catalog key: `msgid`, or `msgctxt` U+0004 `msgid`. */
30
32
  export declare const keyOf: (context: string | undefined, id: string) => string;
31
33
  /** Parse a `.po` (or `.pot`) file. Throws with the line number on errors. */
32
34
  export declare function parsePo(text: string): {
33
35
  header: string;
36
+ /** The comment lines before the header entry, as written (`# Reviewed…`, `#, fuzzy`). */
37
+ headerComments: string[];
34
38
  entries: PoEntry[];
35
39
  };
36
- /** Write a `.po` file: the header, then the entries in the given order. */
37
- export declare function writePo(header: string, entries: readonly PoEntry[]): string;
40
+ /**
41
+ * Write a `.po` file: the header (after its comment lines, as written),
42
+ * then the entries in the given order.
43
+ */
44
+ export declare function writePo(header: string, entries: readonly PoEntry[], headerComments?: readonly string[]): string;
38
45
  /** A message of a compiled catalog: a translation, or `[one, other]`. */
39
46
  export type CompiledMessage = string | [string, string];
40
47
  /**
package/dist/i18n.js CHANGED
@@ -23,6 +23,9 @@ const translated = (entry) => !entry.fuzzy && !entry.obsolete && (entry.strings[
23
23
  export function parsePo(text) {
24
24
  const entries = [];
25
25
  let header = "";
26
+ let headerComments = [];
27
+ // Every comment line of the entry being read, as written.
28
+ let raw = [];
26
29
  const blank = () => ({
27
30
  id: "",
28
31
  strings: [],
@@ -39,11 +42,14 @@ export function parsePo(text) {
39
42
  let pending = blank();
40
43
  const finish = () => {
41
44
  if (started) {
42
- if (entry.id === "" && entry.context === undefined)
45
+ if (entry.id === "" && entry.context === undefined) {
43
46
  header = entry.strings[0] ?? "";
47
+ headerComments = raw;
48
+ }
44
49
  else
45
50
  entries.push(entry);
46
51
  }
52
+ raw = [];
47
53
  entry = blank();
48
54
  started = false;
49
55
  field = "none";
@@ -66,8 +72,11 @@ export function parsePo(text) {
66
72
  else if (line.startsWith("#")) {
67
73
  if (started)
68
74
  finish();
75
+ raw.push(lines[index].trimEnd());
69
76
  const body = line.slice(2).trim();
70
- if (line.startsWith("#."))
77
+ if (line === "#" || line.startsWith("# "))
78
+ (pending.comments ??= []).push(line.slice(2));
79
+ else if (line.startsWith("#."))
71
80
  pending.notes.push(body);
72
81
  else if (line.startsWith("#:"))
73
82
  pending.references.push(...body.split(/\s+/).filter(Boolean));
@@ -135,7 +144,7 @@ export function parsePo(text) {
135
144
  field = next;
136
145
  }
137
146
  finish();
138
- return { header, entries };
147
+ return { header, headerComments, entries };
139
148
  }
140
149
  function unquote(text, at) {
141
150
  if (text.length < 2 || !text.startsWith('"') || !text.endsWith('"'))
@@ -172,12 +181,19 @@ function field(keyword, text) {
172
181
  return `${keyword} ${poString(text)}`;
173
182
  return [`${keyword} ""`, ...parts.map(poString)].join("\n");
174
183
  }
175
- /** Write a `.po` file: the header, then the entries in the given order. */
176
- export function writePo(header, entries) {
177
- const blocks = [`msgid ""\n${field("msgstr", header)}`];
184
+ /**
185
+ * Write a `.po` file: the header (after its comment lines, as written),
186
+ * then the entries in the given order.
187
+ */
188
+ export function writePo(header, entries, headerComments = []) {
189
+ const blocks = [
190
+ [...headerComments, `msgid ""`, field("msgstr", header)].join("\n"),
191
+ ];
178
192
  for (const entry of entries) {
179
193
  const prefix = entry.obsolete ? "#~ " : "";
180
194
  const lines = [];
195
+ for (const comment of entry.comments ?? [])
196
+ lines.push(comment === "" ? "#" : `# ${comment}`);
181
197
  for (const note of entry.notes)
182
198
  lines.push(`#. ${note}`);
183
199
  if (entry.references.length && !entry.obsolete)
package/llms-full.txt CHANGED
@@ -1,4 +1,4 @@
1
- # @vestra-player/sdk 1.5.12: full reference
1
+ # @vestra-player/sdk 1.5.13: 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
 
package/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # Vestra Player SDK
2
2
 
3
- > @vestra-player/sdk 1.5.12: write plugins for Vestra Player in TypeScript (plugin API `vestra/1`). A plugin is a folder with `manifest.json` and an entry module whose default export `activate(plugin)` the player calls; it runs in its own Node.js process. llms-full.txt, next to this file, has every declaration, the guides and the examples; L<n> below are its line numbers.
3
+ > @vestra-player/sdk 1.5.13: write plugins for Vestra Player in TypeScript (plugin API `vestra/1`). A plugin is a folder with `manifest.json` and an entry module whose default export `activate(plugin)` the player calls; it runs in its own Node.js process. llms-full.txt, next to this file, has every declaration, the guides and the examples; L<n> below are its line numbers.
4
4
 
5
5
  **How plugins run**
6
6
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vestra-player/sdk",
3
- "version": "1.5.12",
3
+ "version": "1.5.13",
4
4
  "description": "Plugin API types, test host and plugin CLI for Vestra Player (apiVersion vestra/1)",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {