@ng-linguo/extract 0.9.0
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/README.md +177 -0
- package/linguo.config.schema.json +53 -0
- package/package.json +38 -0
- package/src/cli.d.ts +2 -0
- package/src/cli.js +287 -0
- package/src/cli.js.map +1 -0
- package/src/index.d.ts +10 -0
- package/src/index.js +18 -0
- package/src/index.js.map +1 -0
- package/src/interactive.d.ts +12 -0
- package/src/interactive.js +679 -0
- package/src/interactive.js.map +1 -0
- package/src/lib/apply.d.ts +20 -0
- package/src/lib/apply.js +43 -0
- package/src/lib/apply.js.map +1 -0
- package/src/lib/clipboard.d.ts +17 -0
- package/src/lib/clipboard.js +96 -0
- package/src/lib/clipboard.js.map +1 -0
- package/src/lib/compile.d.ts +12 -0
- package/src/lib/compile.js +29 -0
- package/src/lib/compile.js.map +1 -0
- package/src/lib/config.d.ts +104 -0
- package/src/lib/config.js +185 -0
- package/src/lib/config.js.map +1 -0
- package/src/lib/merge.d.ts +13 -0
- package/src/lib/merge.js +34 -0
- package/src/lib/merge.js.map +1 -0
- package/src/lib/normalize.d.ts +15 -0
- package/src/lib/normalize.js +21 -0
- package/src/lib/normalize.js.map +1 -0
- package/src/lib/po.d.ts +25 -0
- package/src/lib/po.js +110 -0
- package/src/lib/po.js.map +1 -0
- package/src/lib/prompt.d.ts +33 -0
- package/src/lib/prompt.js +80 -0
- package/src/lib/prompt.js.map +1 -0
- package/src/lib/runner.d.ts +62 -0
- package/src/lib/runner.js +102 -0
- package/src/lib/runner.js.map +1 -0
- package/src/lib/scan.d.ts +31 -0
- package/src/lib/scan.js +183 -0
- package/src/lib/scan.js.map +1 -0
- package/src/lib/translation-prompt.txt +214 -0
- package/src/lib/translator.d.ts +83 -0
- package/src/lib/translator.js +91 -0
- package/src/lib/translator.js.map +1 -0
package/src/lib/scan.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.extractMessages = extractMessages;
|
|
4
|
+
const normalize_1 = require("./normalize");
|
|
5
|
+
// The `t` pipe: `'Play' | t` with an optional options object (`| t: { ... }`).
|
|
6
|
+
// Group 2 is the key; group 3 is the options object (one level of nesting), from
|
|
7
|
+
// which the context is read.
|
|
8
|
+
const PIPE_PATTERN = /(['"])((?:\\.|(?!\1).)*?)\1\s*\|\s*t\b(?:\s*:\s*(\{(?:[^{}]|\{[^{}]*\})*\}))?/g;
|
|
9
|
+
// `mark('...')` — the extraction marker for messages defined outside a template
|
|
10
|
+
// (e.g. a component field). Not preceded by `.` so `foo.mark(...)` is ignored.
|
|
11
|
+
// Group 2 is the key; group 3 is the optional options object (`mark('…', { … })`,
|
|
12
|
+
// one level of nesting), from which the context is read.
|
|
13
|
+
const MARK_PATTERN = /(?<!\.)\bmark\s*\(\s*(['"])((?:\\.|(?!\1).)*?)\1(?:\s*,\s*(\{(?:[^{}]|\{[^{}]*\})*\}))?/g;
|
|
14
|
+
// The `t('...', { ... })` helper call (from `injectTranslate()`). The lookbehind
|
|
15
|
+
// keeps it from matching `obj.t(`, `$t(`, or the tail of an identifier. Group 2
|
|
16
|
+
// is the key; group 3 is the options object, from which the context is read.
|
|
17
|
+
const T_CALL_PATTERN = /(?<![\w.$])t\s*\(\s*(['"])((?:\\.|(?!\1).)*?)\1(?:\s*,\s*(\{(?:[^{}]|\{[^{}]*\})*\}))?/g;
|
|
18
|
+
// An element carrying the `[t]` directive via a static `t="message"` attribute.
|
|
19
|
+
// `(?<![\w-])t\s*=` matches a standalone `t=` attribute (not `alt`, `data-t`,
|
|
20
|
+
// `tParams`, `tContext`, `tFor`, nor bound `[t]="…"`). Group 2 is the message.
|
|
21
|
+
const DIRECTIVE_TAG_PATTERN = /<[a-zA-Z][\w-]*\b[^>]*?(?<![\w-])t\s*=\s*"(?:\\.|[^"])*"[^>]*>/g;
|
|
22
|
+
const DIRECTIVE_MESSAGE_PATTERN = /(?<![\w-])t\s*=\s*"((?:\\.|[^"])*)"/;
|
|
23
|
+
const DIRECTIVE_CONTEXT_PATTERN = /(?<![\w-])tContext\s*=\s*"((?:\\.|[^"])*)"/;
|
|
24
|
+
// Reads `context: '…'` out of a pipe options object.
|
|
25
|
+
const CONTEXT_IN_OPTIONS = /\bcontext\s*:\s*(['"])((?:\\.|(?!\1).)*?)\1/;
|
|
26
|
+
// Joins context and key for the dedup map (gettext EOT glue).
|
|
27
|
+
const GLUE = String.fromCharCode(4);
|
|
28
|
+
// Source-comment directives that exclude regions from extraction — for
|
|
29
|
+
// documentation samples whose strings would otherwise scan as real messages.
|
|
30
|
+
// Matched as plain substrings, so they work the same in `//`, `/* */`, and
|
|
31
|
+
// `<!-- -->` comments without the scanner having to understand any of them.
|
|
32
|
+
const IGNORE_FILE = 'linguo-ignore-file';
|
|
33
|
+
const IGNORE_MARKER = /linguo-ignore-(next-line|start|end)/g;
|
|
34
|
+
/**
|
|
35
|
+
* Character ranges of `content` excluded from extraction by `linguo-ignore`
|
|
36
|
+
* directives:
|
|
37
|
+
*
|
|
38
|
+
* - `linguo-ignore-file` anywhere — skip the whole file.
|
|
39
|
+
* - `linguo-ignore-start` … `linguo-ignore-end` — skip everything between them;
|
|
40
|
+
* an unmatched `start` skips to end of file.
|
|
41
|
+
* - `linguo-ignore-next-line` — skip the single line after the directive.
|
|
42
|
+
*/
|
|
43
|
+
function ignoredRanges(content) {
|
|
44
|
+
if (content.includes(IGNORE_FILE)) {
|
|
45
|
+
return [{ start: 0, end: content.length }];
|
|
46
|
+
}
|
|
47
|
+
const ranges = [];
|
|
48
|
+
let blockStart = null;
|
|
49
|
+
IGNORE_MARKER.lastIndex = 0;
|
|
50
|
+
let match;
|
|
51
|
+
while ((match = IGNORE_MARKER.exec(content)) !== null) {
|
|
52
|
+
const kind = match[1];
|
|
53
|
+
if (kind === 'next-line') {
|
|
54
|
+
const lineEnd = content.indexOf('\n', match.index);
|
|
55
|
+
if (lineEnd === -1) {
|
|
56
|
+
continue; // directive on the last line — nothing follows to skip
|
|
57
|
+
}
|
|
58
|
+
const after = content.indexOf('\n', lineEnd + 1);
|
|
59
|
+
ranges.push({ start: lineEnd + 1, end: after === -1 ? content.length : after });
|
|
60
|
+
}
|
|
61
|
+
else if (kind === 'start') {
|
|
62
|
+
blockStart ??= match.index;
|
|
63
|
+
}
|
|
64
|
+
else if (kind === 'end' && blockStart !== null) {
|
|
65
|
+
ranges.push({ start: blockStart, end: match.index });
|
|
66
|
+
blockStart = null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (blockStart !== null) {
|
|
70
|
+
ranges.push({ start: blockStart, end: content.length });
|
|
71
|
+
}
|
|
72
|
+
return ranges;
|
|
73
|
+
}
|
|
74
|
+
function unescapeJs(value) {
|
|
75
|
+
return value.replace(/\\([\\'"`ntr])/g, (_match, char) => {
|
|
76
|
+
if (char === 'n')
|
|
77
|
+
return '\n';
|
|
78
|
+
if (char === 't')
|
|
79
|
+
return '\t';
|
|
80
|
+
if (char === 'r')
|
|
81
|
+
return '\r';
|
|
82
|
+
return char;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
function lineOf(content, index) {
|
|
86
|
+
let line = 1;
|
|
87
|
+
for (let i = 0; i < index && i < content.length; i += 1) {
|
|
88
|
+
if (content[i] === '\n') {
|
|
89
|
+
line += 1;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return line;
|
|
93
|
+
}
|
|
94
|
+
function record(into, found, reference) {
|
|
95
|
+
if (found.keyId === '') {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const id = found.context === '' ? found.keyId : `${found.context}${GLUE}${found.keyId}`;
|
|
99
|
+
const occurrence = into.get(id) ?? {
|
|
100
|
+
keyId: found.keyId,
|
|
101
|
+
context: found.context,
|
|
102
|
+
refs: new Set(),
|
|
103
|
+
};
|
|
104
|
+
occurrence.refs.add(reference);
|
|
105
|
+
into.set(id, occurrence);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Scan source files for translatable strings used by the `t` pipe
|
|
109
|
+
* (`'Play' | t: { context }`), the `[t]` directive (`t="message"` with an
|
|
110
|
+
* optional `tContext`), the `t('...', { context })` helper call, and the
|
|
111
|
+
* `mark()` marker, returning one {@link ExtractedMessage} per unique
|
|
112
|
+
* (context, key) pair with all of its source references.
|
|
113
|
+
*
|
|
114
|
+
* Pure and DOM-free, so it runs in CI on plain Node (CLAUDE.md §2.1). Entries
|
|
115
|
+
* are returned in order of discovery — files in the order given, and within a
|
|
116
|
+
* file by source position — so re-extraction produces a stable, readable order.
|
|
117
|
+
*
|
|
118
|
+
* Regions marked with `linguo-ignore` comment directives are skipped, so
|
|
119
|
+
* documentation samples containing `mark(`, `'…' | t`, or `t="…"` are not
|
|
120
|
+
* scanned as real messages (see {@link ignoredRanges}).
|
|
121
|
+
*/
|
|
122
|
+
function extractMessages(files) {
|
|
123
|
+
const occurrences = new Map();
|
|
124
|
+
for (const file of files) {
|
|
125
|
+
const found = [];
|
|
126
|
+
const fromOptions = (index, key, options) => {
|
|
127
|
+
const contextMatch = options ? CONTEXT_IN_OPTIONS.exec(options) : null;
|
|
128
|
+
found.push({
|
|
129
|
+
index,
|
|
130
|
+
keyId: (0, normalize_1.normalizeMessage)(unescapeJs(key)),
|
|
131
|
+
context: contextMatch ? unescapeJs(contextMatch[2] ?? '').trim() : '',
|
|
132
|
+
});
|
|
133
|
+
};
|
|
134
|
+
PIPE_PATTERN.lastIndex = 0;
|
|
135
|
+
let pipeMatch;
|
|
136
|
+
while ((pipeMatch = PIPE_PATTERN.exec(file.content)) !== null) {
|
|
137
|
+
fromOptions(pipeMatch.index, pipeMatch[2] ?? '', pipeMatch[3]);
|
|
138
|
+
}
|
|
139
|
+
MARK_PATTERN.lastIndex = 0;
|
|
140
|
+
let markMatch;
|
|
141
|
+
while ((markMatch = MARK_PATTERN.exec(file.content)) !== null) {
|
|
142
|
+
fromOptions(markMatch.index, markMatch[2] ?? '', markMatch[3]);
|
|
143
|
+
}
|
|
144
|
+
T_CALL_PATTERN.lastIndex = 0;
|
|
145
|
+
let callMatch;
|
|
146
|
+
while ((callMatch = T_CALL_PATTERN.exec(file.content)) !== null) {
|
|
147
|
+
fromOptions(callMatch.index, callMatch[2] ?? '', callMatch[3]);
|
|
148
|
+
}
|
|
149
|
+
DIRECTIVE_TAG_PATTERN.lastIndex = 0;
|
|
150
|
+
let tagMatch;
|
|
151
|
+
while ((tagMatch = DIRECTIVE_TAG_PATTERN.exec(file.content)) !== null) {
|
|
152
|
+
const tag = tagMatch[0];
|
|
153
|
+
const messageMatch = DIRECTIVE_MESSAGE_PATTERN.exec(tag);
|
|
154
|
+
if (!messageMatch) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const contextMatch = DIRECTIVE_CONTEXT_PATTERN.exec(tag);
|
|
158
|
+
found.push({
|
|
159
|
+
index: tagMatch.index,
|
|
160
|
+
keyId: (0, normalize_1.normalizeMessage)(messageMatch[1] ?? ''),
|
|
161
|
+
context: contextMatch ? (contextMatch[1]?.trim() ?? '') : '',
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
// Interleave pipe and directive matches by source position so discovery
|
|
165
|
+
// order reflects how the file actually reads.
|
|
166
|
+
found.sort((a, b) => a.index - b.index);
|
|
167
|
+
const ignored = ignoredRanges(file.content);
|
|
168
|
+
const isIgnored = (index) => ignored.some((range) => index >= range.start && index < range.end);
|
|
169
|
+
for (const item of found) {
|
|
170
|
+
if (isIgnored(item.index)) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
record(occurrences, item, `${file.path}:${lineOf(file.content, item.index)}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// Map preserves insertion order, so this is discovery order.
|
|
177
|
+
return [...occurrences.values()].map(({ keyId, context, refs }) => ({
|
|
178
|
+
keyId,
|
|
179
|
+
context,
|
|
180
|
+
references: [...refs].sort(),
|
|
181
|
+
}));
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=scan.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scan.js","sourceRoot":"","sources":["../../../../../packages/extract/src/lib/scan.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
You are a professional software localizer. Translate a gettext `.po`
|
|
2
|
+
catalog produced by **ng-linguo** (an Angular i18n library) from English
|
|
3
|
+
into {{TARGET_LANGUAGE}}.
|
|
4
|
+
|
|
5
|
+
Return the COMPLETE `.po` file and NOTHING else — no commentary, no code
|
|
6
|
+
fences, no explanations. Your output must be a valid `.po` file that I
|
|
7
|
+
can save directly over the input.
|
|
8
|
+
|
|
9
|
+
================================================================
|
|
10
|
+
1. WHAT YOU MAY AND MAY NOT CHANGE
|
|
11
|
+
================================================================
|
|
12
|
+
A `.po` file is a list of entries. A typical entry looks like:
|
|
13
|
+
|
|
14
|
+
#: apps/playground/src/app/app.html:10
|
|
15
|
+
msgctxt "audio player"
|
|
16
|
+
msgid "Play"
|
|
17
|
+
msgstr "<MISSING TRANSLATION> Play"
|
|
18
|
+
|
|
19
|
+
Rules:
|
|
20
|
+
- TRANSLATE ONLY the text inside `msgstr "..."`.
|
|
21
|
+
- NEVER change `msgid` — it is the source key. Copy it as-is.
|
|
22
|
+
- NEVER change, remove, or reorder `#:` reference lines (source
|
|
23
|
+
locations) or `msgctxt` lines. Keep every entry in its original order.
|
|
24
|
+
- Keep the header entry (the first block, `msgid ""` / `msgstr ""`)
|
|
25
|
+
exactly as-is.
|
|
26
|
+
- Output every entry. Do not drop, merge, or add entries.
|
|
27
|
+
|
|
28
|
+
================================================================
|
|
29
|
+
2. WHICH ENTRIES NEED WORK — the <MISSING TRANSLATION> marker
|
|
30
|
+
================================================================
|
|
31
|
+
Any `msgstr` beginning with `<MISSING TRANSLATION>` is an untranslated
|
|
32
|
+
entry seeded with the English source as a fallback.
|
|
33
|
+
|
|
34
|
+
- For each such entry: translate the source and write the result WITHOUT
|
|
35
|
+
the `<MISSING TRANSLATION>` prefix (delete the prefix entirely).
|
|
36
|
+
- If a `msgstr` does NOT start with that prefix, it is already
|
|
37
|
+
human-reviewed — LEAVE IT UNTOUCHED.
|
|
38
|
+
|
|
39
|
+
================================================================
|
|
40
|
+
3. CONTEXT — use it, never translate it
|
|
41
|
+
================================================================
|
|
42
|
+
`msgctxt "..."` carries the message's context. In ng-linguo, context is
|
|
43
|
+
a single property that does two jobs:
|
|
44
|
+
(a) it disambiguates identical source text — the same `msgid "Play"`
|
|
45
|
+
can appear several times with different `msgctxt` ("audio player"
|
|
46
|
+
vs "game") and MUST be translated differently per context;
|
|
47
|
+
(b) it doubles as a translator note describing intent.
|
|
48
|
+
|
|
49
|
+
So: READ the `msgctxt` to pick the right wording, but it is metadata —
|
|
50
|
+
do not translate it and do not copy it into the `msgstr`. Entries with
|
|
51
|
+
the same `msgid` but different `msgctxt` are independent and usually need
|
|
52
|
+
different translations.
|
|
53
|
+
|
|
54
|
+
================================================================
|
|
55
|
+
4. SLOT TAGS — `[name]...[/name]`
|
|
56
|
+
================================================================
|
|
57
|
+
Some sources contain slot tags (a BBCode-like bracket syntax), e.g.:
|
|
58
|
+
|
|
59
|
+
msgid "Read the [docs]documentation[/docs] to get started"
|
|
60
|
+
|
|
61
|
+
These tags map to interactive UI (links, buttons) at runtime.
|
|
62
|
+
|
|
63
|
+
- Preserve every tag EXACTLY: same name, same `[name]` ... `[/name]`
|
|
64
|
+
pairing, same order. Tag names are NOT translated.
|
|
65
|
+
- Translate ONLY the human text, including the text WRAPPED BY a tag.
|
|
66
|
+
- You MAY move a tag pair to wherever the wrapped phrase naturally lands
|
|
67
|
+
in {{TARGET_LANGUAGE}} word order, as long as the pair stays matched
|
|
68
|
+
and wraps the same concept.
|
|
69
|
+
- A literal `[` is written as a doubled `[[` — the ONLY escape in this
|
|
70
|
+
syntax. `[[` is NOT a slot tag; it renders as a single visible `[`.
|
|
71
|
+
Keep every `[[` exactly as a doubled bracket (never collapse it to one
|
|
72
|
+
`[`, never expand it) and translate the prose around it. So
|
|
73
|
+
`[[b]bold[[/b]` displays the literal characters `[b]bold[/b]`, and a
|
|
74
|
+
sentence built around that is describing the escape mechanism itself.
|
|
75
|
+
|
|
76
|
+
Example (→ German):
|
|
77
|
+
msgid "Read the [docs]documentation[/docs] to get started"
|
|
78
|
+
msgstr "Lies die [docs]Dokumentation[/docs], um loszulegen"
|
|
79
|
+
|
|
80
|
+
================================================================
|
|
81
|
+
5. ICU MESSAGEFORMAT 2 — the heart of translation quality
|
|
82
|
+
================================================================
|
|
83
|
+
ng-linguo formats messages with **MessageFormat 2 (MF2)**. Two forms
|
|
84
|
+
appear:
|
|
85
|
+
|
|
86
|
+
5a. SIMPLE PLACEHOLDERS — `{$name}`
|
|
87
|
+
msgid "Hello {$name}!"
|
|
88
|
+
A `{$var}` is a runtime value substituted into the sentence.
|
|
89
|
+
- Keep the placeholder verbatim, including the `$` and exact name.
|
|
90
|
+
- Reposition it to fit natural target word order.
|
|
91
|
+
- Do NOT invent, rename, or drop placeholders.
|
|
92
|
+
Example (→ Polish):
|
|
93
|
+
msgid "Hello {$name}!"
|
|
94
|
+
msgstr "Cześć {$name}!"
|
|
95
|
+
|
|
96
|
+
5b. SELECTION / PLURALS — `.input` / `.match` / variants
|
|
97
|
+
msgid ".input {$count :number} .match $count one {{{$count} file}} * {{{$count} files}}"
|
|
98
|
+
|
|
99
|
+
Anatomy:
|
|
100
|
+
.input {$count :number} declares a selector value (a number)
|
|
101
|
+
.match $count selects a variant based on it
|
|
102
|
+
one {{ ... }} variant chosen for the "one" plural category
|
|
103
|
+
* {{ ... }} the catch-all / "other" variant (REQUIRED)
|
|
104
|
+
{{ ... }} a "quoted pattern": its inner text is what
|
|
105
|
+
renders; `{$count}` inside it is a placeholder.
|
|
106
|
+
|
|
107
|
+
THIS IS WHERE QUALITY IS WON OR LOST. Different languages have
|
|
108
|
+
different CLDR plural categories. You MUST emit the categories that
|
|
109
|
+
{{TARGET_LANGUAGE}} actually uses — not copy English's set.
|
|
110
|
+
|
|
111
|
+
Rules:
|
|
112
|
+
- Keep `.input ... .match ...` and the selector exactly as written
|
|
113
|
+
(same `{$count :number}`, same `.match $count`).
|
|
114
|
+
- Replace the English variant KEYS with the correct CLDR plural
|
|
115
|
+
categories for {{TARGET_LANGUAGE}}, chosen from:
|
|
116
|
+
zero · one · two · few · many · other
|
|
117
|
+
Always include the `*` (other) catch-all variant.
|
|
118
|
+
- Translate ONLY the literal text inside each `{{ ... }}` pattern.
|
|
119
|
+
Keep `{$count}` (and any other `{$var}`) intact inside the pattern.
|
|
120
|
+
- Add or remove variants as the language requires (e.g. English has
|
|
121
|
+
one/other; Polish needs one/few/many/other).
|
|
122
|
+
|
|
123
|
+
Example (English `one` + `other` → Polish `one`/`few`/`many`/`other`):
|
|
124
|
+
msgid ".input {$count :number} .match $count one {{{$count} file}} * {{{$count} files}}"
|
|
125
|
+
msgstr ".input {$count :number} .match $count one {{{$count} plik}} few {{{$count} pliki}} many {{{$count} plików}} * {{{$count} pliku}}"
|
|
126
|
+
|
|
127
|
+
Example (German, like English — one/other):
|
|
128
|
+
msgstr ".input {$count :number} .match $count one {{{$count} Datei}} * {{{$count} Dateien}}"
|
|
129
|
+
|
|
130
|
+
If you are unsure which plural categories a language uses, follow the
|
|
131
|
+
Unicode CLDR plural rules for that language. Getting these categories
|
|
132
|
+
right is the single most important part of this task.
|
|
133
|
+
|
|
134
|
+
5c. GOING BEYOND THE SOURCE STRUCTURE
|
|
135
|
+
The English source is a baseline, not a cage. MF2 lets the target
|
|
136
|
+
read more naturally than a literal mirror of the source — use it.
|
|
137
|
+
|
|
138
|
+
(i) EXACT-VALUE MATCHING — special-case 0 (and sometimes 1).
|
|
139
|
+
`.match` keys may be literal numbers, and an exact literal wins
|
|
140
|
+
over a plural category. If {{TARGET_LANGUAGE}} phrases "none"
|
|
141
|
+
(or "one") with a different construction, ADD a literal variant.
|
|
142
|
+
This is always safe — it reuses the selector that already exists.
|
|
143
|
+
|
|
144
|
+
Example (→ English-style, but adding a natural zero case):
|
|
145
|
+
source variants: one {{...}} * {{...}}
|
|
146
|
+
improved: .match $count
|
|
147
|
+
0 {{No files}}
|
|
148
|
+
one {{One file}}
|
|
149
|
+
* {{{$count} files}}
|
|
150
|
+
|
|
151
|
+
(ii) ADDING A SELECTOR DIMENSION (gender, case, animacy …).
|
|
152
|
+
Many languages inflect on gender/case even where English does not.
|
|
153
|
+
You MAY add such a dimension ONLY IF a matching `.input {$var ...}`
|
|
154
|
+
is ALREADY DECLARED in the message (i.e. the app passes that value
|
|
155
|
+
at runtime). Never invent a selector for a variable that is not
|
|
156
|
+
already declared — at runtime it would be undefined and break.
|
|
157
|
+
|
|
158
|
+
When a selector IS available, switch to multiple selectors with
|
|
159
|
+
one key per selector on each variant line, and keep a `* * {{...}}`
|
|
160
|
+
catch-all:
|
|
161
|
+
.input {$count :number} .input {$gender :string}
|
|
162
|
+
.match $count $gender
|
|
163
|
+
one feminine {{...}}
|
|
164
|
+
one masculine {{...}}
|
|
165
|
+
* * {{...}}
|
|
166
|
+
|
|
167
|
+
If the variable you would need is NOT declared, translate within
|
|
168
|
+
the source's existing structure instead — do not add it.
|
|
169
|
+
|
|
170
|
+
================================================================
|
|
171
|
+
6. MECHANICAL .po RULES
|
|
172
|
+
================================================================
|
|
173
|
+
- A `msgstr` value is a double-quoted string. Escape a literal double
|
|
174
|
+
quote as `\"` and a newline as `\n` inside the quotes.
|
|
175
|
+
- Preserve any `""` continuation-line formatting if present (a value
|
|
176
|
+
split across multiple quoted lines concatenates).
|
|
177
|
+
- UTF-8 throughout. Use the target language's real characters (ą, ß, é …),
|
|
178
|
+
not ASCII transliterations.
|
|
179
|
+
|
|
180
|
+
================================================================
|
|
181
|
+
7. STYLE
|
|
182
|
+
================================================================
|
|
183
|
+
- Translate meaning, not words. Match the register a UI button / label /
|
|
184
|
+
sentence would use in {{TARGET_LANGUAGE}}.
|
|
185
|
+
- Aim for a result that reads as if ORIGINALLY WRITTEN in
|
|
186
|
+
{{TARGET_LANGUAGE}}, not transcoded from English. You are free to
|
|
187
|
+
restructure: reorder clauses, split one sentence into two or merge two
|
|
188
|
+
into one, change voice, drop filler connectives — whatever natural
|
|
189
|
+
phrasing wants. The source fixes the MEANING and the
|
|
190
|
+
placeholders/slots/tags, not the word order or sentence shape. Output
|
|
191
|
+
that is grammatical but stiff, word-for-word, or obviously translated is
|
|
192
|
+
a FAILURE here. Mirroring English clause order often produces broken
|
|
193
|
+
target grammar — rebuild the sentence instead.
|
|
194
|
+
- Infer each message's DOMAIN and register from its own content and the
|
|
195
|
+
app's other strings — it may be software, medical, legal, finance,
|
|
196
|
+
gaming, marketing, hospitality, anything. Then translate the way a
|
|
197
|
+
domain expert writing in {{TARGET_LANGUAGE}} would, using that field's
|
|
198
|
+
established terminology — not a literal everyday rendering, and not
|
|
199
|
+
jargon borrowed from the wrong field. Technical terms usually have a
|
|
200
|
+
settled target-language form — sometimes a native word, sometimes a
|
|
201
|
+
borrowed or anglicized term that practitioners actually say; use
|
|
202
|
+
whichever is REAL in the field, not a coined literal calque. You do NOT need a
|
|
203
|
+
`msgctxt` to do this — the source text itself is the signal (treat
|
|
204
|
+
`msgctxt`, when present, as a tie-breaker). Example: in "Escaped
|
|
205
|
+
[[b]bold[[/b] stays literal" the surrounding markup marks a software
|
|
206
|
+
context, so "Escaped" is character-escaping — pick the term a developer
|
|
207
|
+
uses, not the word for fleeing/running away.
|
|
208
|
+
- Respect the context (§3) when the same source has several entries.
|
|
209
|
+
- Keep capitalization and punctuation idiomatic for the target language.
|
|
210
|
+
|
|
211
|
+
================================================================
|
|
212
|
+
NOW TRANSLATE THIS FILE
|
|
213
|
+
================================================================
|
|
214
|
+
{{PO_FILE}}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a consumer's translate function receives. We build the prompt (explaining
|
|
3
|
+
* ng-linguo's context, slot tags and MessageFormat 2 rules) and parse the reply;
|
|
4
|
+
* the function's only job is to send `prompt` to an AI provider and return the
|
|
5
|
+
* model's raw answer — a complete `.po` block. This keeps every provider SDK and
|
|
6
|
+
* API secret on the consumer's side, out of this build-time tool.
|
|
7
|
+
*/
|
|
8
|
+
export interface TranslateRequest {
|
|
9
|
+
/** The ready-to-send prompt for the untranslated entries of one catalog. */
|
|
10
|
+
readonly prompt: string;
|
|
11
|
+
/** The catalog's locale code, e.g. `"pl"`. */
|
|
12
|
+
readonly targetLocale: string;
|
|
13
|
+
/** A human-readable label for the locale, e.g. `"Polish (pl)"`. */
|
|
14
|
+
readonly targetLabel: string;
|
|
15
|
+
/** The configured source locale the strings are translated from, e.g. `"en"`. */
|
|
16
|
+
readonly sourceLocale: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A consumer-supplied translation function. Linked to the build via the
|
|
20
|
+
* `translator` field in `linguo.config.json` (a path to a module that exports it
|
|
21
|
+
* as `translate` or as the default export). It receives a {@link TranslateRequest}
|
|
22
|
+
* and returns the model's reply — the translated `.po` text — sync or async.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```js
|
|
26
|
+
* // linguo.translator.mjs
|
|
27
|
+
* import OpenAI from 'openai';
|
|
28
|
+
* const client = new OpenAI(); // reads OPENAI_API_KEY
|
|
29
|
+
*
|
|
30
|
+
* export async function translate({ prompt }) {
|
|
31
|
+
* const res = await client.chat.completions.create({
|
|
32
|
+
* model: 'gpt-4o',
|
|
33
|
+
* messages: [{ role: 'user', content: prompt }],
|
|
34
|
+
* });
|
|
35
|
+
* return res.choices[0].message.content ?? '';
|
|
36
|
+
* }
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export type TranslateFunction = (request: TranslateRequest) => string | Promise<string>;
|
|
40
|
+
/**
|
|
41
|
+
* Extract the {@link TranslateFunction} from an imported translator module,
|
|
42
|
+
* accepting either a named `translate` export or a default function. Pure (no
|
|
43
|
+
* I/O), so it is unit-tested directly.
|
|
44
|
+
*
|
|
45
|
+
* @throws when no usable function is exported.
|
|
46
|
+
*/
|
|
47
|
+
export declare function resolveTranslatorExport(mod: unknown, source?: string): TranslateFunction;
|
|
48
|
+
/** How a module specifier is loaded; injectable so the loader is testable. */
|
|
49
|
+
export type ModuleImporter = (specifier: string) => Promise<unknown>;
|
|
50
|
+
/**
|
|
51
|
+
* Dynamically import the translator module at `resolvedPath` (an absolute path)
|
|
52
|
+
* and return its {@link TranslateFunction}. The path is converted to a `file://`
|
|
53
|
+
* URL so ESM `.mjs`/`.js` consumer modules load on every platform.
|
|
54
|
+
*
|
|
55
|
+
* @throws when the module cannot be loaded or exports no usable function.
|
|
56
|
+
*/
|
|
57
|
+
export declare function loadTranslator(resolvedPath: string, importer?: ModuleImporter): Promise<TranslateFunction>;
|
|
58
|
+
/** Outcome of {@link autoTranslateCatalog}. */
|
|
59
|
+
export interface AutoTranslateOutcome {
|
|
60
|
+
/** How many entries were sent to the translator (the untranslated ones). */
|
|
61
|
+
readonly untranslated: number;
|
|
62
|
+
/** How many entries received a usable new translation. */
|
|
63
|
+
readonly applied: number;
|
|
64
|
+
/** How many entries are still untranslated after merging the reply. */
|
|
65
|
+
readonly remaining: number;
|
|
66
|
+
/** The merged full catalog as `.po` text (unchanged when nothing applied). */
|
|
67
|
+
readonly po: string;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Translate one catalog's untranslated entries by calling a {@link TranslateFunction}.
|
|
71
|
+
* Builds the prompt from the missing entries only, invokes `translate`, and folds
|
|
72
|
+
* the reply back into the full catalog. Pure of file I/O — the caller reads and
|
|
73
|
+
* writes the `.po` — so it is unit-tested with a fake translate function.
|
|
74
|
+
*
|
|
75
|
+
* @throws when the translator returns an empty reply.
|
|
76
|
+
*/
|
|
77
|
+
export declare function autoTranslateCatalog(args: {
|
|
78
|
+
readonly translate: TranslateFunction;
|
|
79
|
+
readonly poText: string;
|
|
80
|
+
readonly targetLocale: string;
|
|
81
|
+
readonly targetLabel: string;
|
|
82
|
+
readonly sourceLocale: string;
|
|
83
|
+
}): Promise<AutoTranslateOutcome>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveTranslatorExport = resolveTranslatorExport;
|
|
4
|
+
exports.loadTranslator = loadTranslator;
|
|
5
|
+
exports.autoTranslateCatalog = autoTranslateCatalog;
|
|
6
|
+
const node_url_1 = require("node:url");
|
|
7
|
+
const apply_1 = require("./apply");
|
|
8
|
+
const po_1 = require("./po");
|
|
9
|
+
const prompt_1 = require("./prompt");
|
|
10
|
+
function pickExport(mod) {
|
|
11
|
+
if (typeof mod !== 'object' || mod === null) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
const record = mod;
|
|
15
|
+
if (typeof record['translate'] === 'function') {
|
|
16
|
+
return record['translate'];
|
|
17
|
+
}
|
|
18
|
+
const def = record['default'];
|
|
19
|
+
if (typeof def === 'function') {
|
|
20
|
+
return def;
|
|
21
|
+
}
|
|
22
|
+
// An ESM module whose default export is itself an object with `translate`.
|
|
23
|
+
if (typeof def === 'object' && def !== null) {
|
|
24
|
+
const defRecord = def;
|
|
25
|
+
if (typeof defRecord['translate'] === 'function') {
|
|
26
|
+
return defRecord['translate'];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Extract the {@link TranslateFunction} from an imported translator module,
|
|
33
|
+
* accepting either a named `translate` export or a default function. Pure (no
|
|
34
|
+
* I/O), so it is unit-tested directly.
|
|
35
|
+
*
|
|
36
|
+
* @throws when no usable function is exported.
|
|
37
|
+
*/
|
|
38
|
+
function resolveTranslatorExport(mod, source = 'translator module') {
|
|
39
|
+
const candidate = pickExport(mod);
|
|
40
|
+
if (typeof candidate !== 'function') {
|
|
41
|
+
throw new Error(`${source} must export a "translate" function (named export) or a default function.`);
|
|
42
|
+
}
|
|
43
|
+
return candidate;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Dynamically import the translator module at `resolvedPath` (an absolute path)
|
|
47
|
+
* and return its {@link TranslateFunction}. The path is converted to a `file://`
|
|
48
|
+
* URL so ESM `.mjs`/`.js` consumer modules load on every platform.
|
|
49
|
+
*
|
|
50
|
+
* @throws when the module cannot be loaded or exports no usable function.
|
|
51
|
+
*/
|
|
52
|
+
async function loadTranslator(resolvedPath, importer = (specifier) => import(specifier)) {
|
|
53
|
+
const href = (0, node_url_1.pathToFileURL)(resolvedPath).href;
|
|
54
|
+
let mod;
|
|
55
|
+
try {
|
|
56
|
+
mod = await importer(href);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
throw new Error(`could not load translator module at ${resolvedPath}: ` +
|
|
60
|
+
`${error instanceof Error ? error.message : String(error)}`);
|
|
61
|
+
}
|
|
62
|
+
return resolveTranslatorExport(mod, resolvedPath);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Translate one catalog's untranslated entries by calling a {@link TranslateFunction}.
|
|
66
|
+
* Builds the prompt from the missing entries only, invokes `translate`, and folds
|
|
67
|
+
* the reply back into the full catalog. Pure of file I/O — the caller reads and
|
|
68
|
+
* writes the `.po` — so it is unit-tested with a fake translate function.
|
|
69
|
+
*
|
|
70
|
+
* @throws when the translator returns an empty reply.
|
|
71
|
+
*/
|
|
72
|
+
async function autoTranslateCatalog(args) {
|
|
73
|
+
const untranslated = (0, po_1.parsePo)(args.poText).filter((e) => (0, apply_1.isUntranslated)(e.msgstr));
|
|
74
|
+
if (untranslated.length === 0) {
|
|
75
|
+
return { untranslated: 0, applied: 0, remaining: 0, po: args.poText };
|
|
76
|
+
}
|
|
77
|
+
const prompt = (0, prompt_1.buildTranslationPrompt)(args.targetLabel, (0, po_1.serializePo)(untranslated));
|
|
78
|
+
const reply = await args.translate({
|
|
79
|
+
prompt,
|
|
80
|
+
targetLocale: args.targetLocale,
|
|
81
|
+
targetLabel: args.targetLabel,
|
|
82
|
+
sourceLocale: args.sourceLocale,
|
|
83
|
+
});
|
|
84
|
+
if (typeof reply !== 'string' || reply.trim() === '') {
|
|
85
|
+
throw new Error(`translator returned an empty reply for ${args.targetLabel}`);
|
|
86
|
+
}
|
|
87
|
+
const { po, applied } = (0, apply_1.applyTranslations)(args.poText, reply);
|
|
88
|
+
const remaining = (0, po_1.parsePo)(po).filter((e) => (0, apply_1.isUntranslated)(e.msgstr)).length;
|
|
89
|
+
return { untranslated: untranslated.length, applied, remaining, po };
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=translator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"translator.js","sourceRoot":"","sources":["../../../../../packages/extract/src/lib/translator.ts"],"names":[],"mappings":"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"}
|