@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 +7 -0
- package/dist/i18n-tools.js +24 -7
- package/dist/i18n.d.ts +9 -2
- package/dist/i18n.js +22 -6
- package/llms-full.txt +1 -1
- package/llms.txt +1 -1
- package/package.json +1 -1
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`.
|
package/dist/i18n-tools.js
CHANGED
|
@@ -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
|
-
|
|
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 <
|
|
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
|
-
/**
|
|
37
|
-
|
|
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
|
-
/**
|
|
176
|
-
|
|
177
|
-
|
|
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.
|
|
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.
|
|
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
|
|