@strato-admin/i18n-cli 0.1.0 → 0.3.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/dist/compile.js +142 -0
- package/dist/extract.js +558 -0
- package/dist/strato-i18n/src/formatter.d.ts +10 -0
- package/dist/strato-i18n/src/formatter.js +62 -0
- package/dist/strato-i18n/src/hash.d.ts +10 -0
- package/dist/strato-i18n/src/hash.js +26 -0
- package/dist/strato-i18n/src/icuI18nProvider.d.ts +2 -0
- package/dist/strato-i18n/src/icuI18nProvider.js +92 -0
- package/dist/strato-i18n/src/index.d.ts +3 -0
- package/dist/strato-i18n/src/index.js +19 -0
- package/dist/strato-i18n-cli/src/cli/compile.d.ts +2 -0
- package/dist/strato-i18n-cli/src/cli/compile.js +142 -0
- package/dist/strato-i18n-cli/src/cli/extract.d.ts +2 -0
- package/dist/strato-i18n-cli/src/cli/extract.js +558 -0
- package/package.json +6 -3
- package/src/cli/compile.ts +65 -25
- package/src/cli/extract.ts +298 -62
- package/src/i18n/extracted-messages.json +8 -8
- package/dist/cli/compile.js +0 -73
- package/dist/cli/extract.js +0 -317
- /package/dist/{cli/compile.d.ts → compile.d.ts} +0 -0
- /package/dist/{cli/extract.d.ts → extract.d.ts} +0 -0
package/src/cli/compile.ts
CHANGED
|
@@ -3,22 +3,44 @@ import * as fs from 'fs';
|
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import { globSync } from 'glob';
|
|
5
5
|
import * as gettextParser from 'gettext-parser';
|
|
6
|
+
import { normalizeMessage } from '@strato-admin/i18n';
|
|
6
7
|
|
|
7
|
-
function
|
|
8
|
+
function parseArgs() {
|
|
8
9
|
const args = process.argv.slice(2);
|
|
9
|
-
|
|
10
|
+
let outFile: string | undefined = undefined;
|
|
11
|
+
const positionalArgs: string[] = [];
|
|
12
|
+
|
|
13
|
+
for (let i = 0; i < args.length; i++) {
|
|
14
|
+
if ((args[i] === '--out-file' || args[i] === '-o') && i + 1 < args.length) {
|
|
15
|
+
outFile = args[i + 1];
|
|
16
|
+
i++;
|
|
17
|
+
} else if (args[i].startsWith('--out-file=')) {
|
|
18
|
+
outFile = args[i].split('=')[1];
|
|
19
|
+
} else {
|
|
20
|
+
positionalArgs.push(args[i]);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const pattern = positionalArgs[0] || 'locales';
|
|
25
|
+
return { pattern, outFile };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function main() {
|
|
29
|
+
const { pattern, outFile: explicitOutFile } = parseArgs();
|
|
10
30
|
|
|
11
31
|
let files: string[] = [];
|
|
12
32
|
|
|
13
33
|
if (fs.existsSync(pattern) && fs.statSync(pattern).isDirectory()) {
|
|
14
34
|
// Original behavior: if a directory is passed, find .json and .po files inside it
|
|
15
|
-
files = fs
|
|
16
|
-
.
|
|
17
|
-
.
|
|
35
|
+
files = fs
|
|
36
|
+
.readdirSync(pattern)
|
|
37
|
+
.filter((file) => (file.endsWith('.json') || file.endsWith('.po')) && !file.endsWith('.compiled.json'))
|
|
38
|
+
.map((file) => path.join(pattern, file));
|
|
18
39
|
} else {
|
|
19
40
|
// New behavior: support glob patterns
|
|
20
|
-
files = globSync(pattern, { absolute: true })
|
|
21
|
-
|
|
41
|
+
files = globSync(pattern, { absolute: true }).filter(
|
|
42
|
+
(file) => (file.endsWith('.json') || file.endsWith('.po')) && !file.endsWith('.compiled.json'),
|
|
43
|
+
);
|
|
22
44
|
}
|
|
23
45
|
|
|
24
46
|
if (files.length === 0) {
|
|
@@ -26,29 +48,37 @@ function main() {
|
|
|
26
48
|
return;
|
|
27
49
|
}
|
|
28
50
|
|
|
51
|
+
// If explicitOutFile is provided, we only expect ONE input file or we combine them?
|
|
52
|
+
// FormatJS compile behavior: formatjs compile <file> --out-file <outFile>
|
|
53
|
+
if (explicitOutFile && files.length > 1) {
|
|
54
|
+
console.warn(`Warning: Multiple input files found but only one --out-file specified. Using the first one.`);
|
|
55
|
+
files = [files[0]];
|
|
56
|
+
}
|
|
57
|
+
|
|
29
58
|
let processedCount = 0;
|
|
30
59
|
|
|
31
|
-
files.forEach(filePath => {
|
|
32
|
-
const compiledFilePath = filePath.replace(/\.(json|po)$/, '.compiled.json');
|
|
60
|
+
files.forEach((filePath) => {
|
|
61
|
+
const compiledFilePath = explicitOutFile || filePath.replace(/\.(json|po)$/, '.compiled.json');
|
|
33
62
|
const fileName = path.basename(filePath);
|
|
34
63
|
|
|
35
|
-
let translations: Record<string,
|
|
64
|
+
let translations: Record<string, any> = {};
|
|
36
65
|
|
|
37
66
|
try {
|
|
38
67
|
if (filePath.endsWith('.po')) {
|
|
39
68
|
const parsed = gettextParser.po.parse(fs.readFileSync(filePath));
|
|
40
|
-
|
|
69
|
+
|
|
41
70
|
Object.entries(parsed.translations).forEach(([context, entries]) => {
|
|
42
71
|
Object.entries(entries).forEach(([msgid, data]: [string, any]) => {
|
|
43
72
|
if (msgid === '') return; // skip header
|
|
44
|
-
|
|
45
|
-
// Find the hash:
|
|
46
|
-
|
|
47
|
-
const
|
|
73
|
+
|
|
74
|
+
// Find the hash: #. id: comment is authoritative (covers explicit ids and
|
|
75
|
+
// context-mangled hashes); fall back to msgctxt, then raw msgid.
|
|
76
|
+
const commentHash = data.comments?.extracted?.match(/id: (\S+)/)?.[1];
|
|
77
|
+
const hash = commentHash || context || msgid;
|
|
48
78
|
|
|
49
79
|
translations[hash] = {
|
|
50
80
|
defaultMessage: data.msgid || data.comments?.extracted || '',
|
|
51
|
-
translation: data.msgstr[0] || ''
|
|
81
|
+
translation: data.msgstr[0] || '',
|
|
52
82
|
};
|
|
53
83
|
});
|
|
54
84
|
});
|
|
@@ -61,24 +91,34 @@ function main() {
|
|
|
61
91
|
}
|
|
62
92
|
|
|
63
93
|
const compiledMapping: Record<string, string> = {};
|
|
94
|
+
const sortedKeys = Object.keys(translations).sort();
|
|
64
95
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
96
|
+
sortedKeys.forEach((hash) => {
|
|
97
|
+
const data = translations[hash];
|
|
98
|
+
// Support both strato-i18n-cli format and formatjs format (with translation or defaultMessage as translation)
|
|
99
|
+
if (typeof data === 'string') {
|
|
100
|
+
compiledMapping[hash] = normalizeMessage(data);
|
|
101
|
+
} else if (data.translation && data.translation.trim() !== '') {
|
|
102
|
+
compiledMapping[hash] = normalizeMessage(data.translation);
|
|
69
103
|
} else {
|
|
70
|
-
|
|
104
|
+
// Fallback to defaultMessage
|
|
105
|
+
compiledMapping[hash] = normalizeMessage(data.defaultMessage || '');
|
|
71
106
|
}
|
|
72
107
|
});
|
|
73
108
|
|
|
109
|
+
const parentDir = path.dirname(compiledFilePath);
|
|
110
|
+
if (!fs.existsSync(parentDir)) {
|
|
111
|
+
fs.mkdirSync(parentDir, { recursive: true });
|
|
112
|
+
}
|
|
113
|
+
|
|
74
114
|
fs.writeFileSync(compiledFilePath, JSON.stringify(compiledMapping, null, 2));
|
|
75
|
-
console.log(
|
|
115
|
+
console.log(
|
|
116
|
+
`Compiled ${fileName} -> ${path.basename(compiledFilePath)} (${Object.keys(compiledMapping).length} messages)`,
|
|
117
|
+
);
|
|
76
118
|
processedCount++;
|
|
77
119
|
});
|
|
78
120
|
|
|
79
121
|
console.log(`Successfully compiled ${processedCount} files.`);
|
|
80
122
|
}
|
|
81
123
|
|
|
82
|
-
|
|
83
|
-
main();
|
|
84
|
-
}
|
|
124
|
+
main();
|
package/src/cli/extract.ts
CHANGED
|
@@ -6,7 +6,11 @@ import { parse } from '@babel/parser';
|
|
|
6
6
|
import traverse from '@babel/traverse';
|
|
7
7
|
import * as t from '@babel/types';
|
|
8
8
|
import * as gettextParser from 'gettext-parser';
|
|
9
|
-
import { generateMessageId } from '@strato-admin/i18n';
|
|
9
|
+
import { generateMessageId, normalizeMessage, prettyPrintICU } from '@strato-admin/i18n';
|
|
10
|
+
import { minimatch } from 'minimatch';
|
|
11
|
+
|
|
12
|
+
// Components whose JSX children text is the translatable string (source-as-key)
|
|
13
|
+
const CHILDREN_AS_KEY_COMPONENTS = new Set(['Message', 'RecordMessage']);
|
|
10
14
|
|
|
11
15
|
// List of Strato Admin components to extract from
|
|
12
16
|
const DEFAULT_STRATO_COMPONENTS = new Set([
|
|
@@ -44,11 +48,24 @@ const DEFAULT_STRATO_COMPONENTS = new Set([
|
|
|
44
48
|
// List of translatable props
|
|
45
49
|
const DEFAULT_TRANSLATABLE_PROPS = new Set([
|
|
46
50
|
'label',
|
|
51
|
+
'listLabel',
|
|
52
|
+
'createLabel',
|
|
53
|
+
'editLabel',
|
|
54
|
+
'detailLabel',
|
|
47
55
|
'title',
|
|
56
|
+
'listTitle',
|
|
57
|
+
'createTitle',
|
|
58
|
+
'editTitle',
|
|
59
|
+
'detailTitle',
|
|
48
60
|
'placeholder',
|
|
49
61
|
'emptyText',
|
|
50
62
|
'helperText',
|
|
51
63
|
'description',
|
|
64
|
+
'listDescription',
|
|
65
|
+
'createDescription',
|
|
66
|
+
'editDescription',
|
|
67
|
+
'detailDescription',
|
|
68
|
+
'saveButtonLabel',
|
|
52
69
|
'successMessage',
|
|
53
70
|
'errorMessage',
|
|
54
71
|
]);
|
|
@@ -100,10 +117,13 @@ function parseArgs() {
|
|
|
100
117
|
const args = process.argv.slice(2);
|
|
101
118
|
let format: string | undefined = undefined;
|
|
102
119
|
let config: string | undefined = undefined;
|
|
120
|
+
let outFile: string | undefined = undefined;
|
|
121
|
+
let locale: string | undefined = undefined;
|
|
122
|
+
const ignorePatterns: string[] = [];
|
|
103
123
|
const positionalArgs: string[] = [];
|
|
104
124
|
|
|
105
125
|
for (let i = 0; i < args.length; i++) {
|
|
106
|
-
if (args[i] === '--format' && i + 1 < args.length) {
|
|
126
|
+
if ((args[i] === '--format' || args[i] === '-f') && i + 1 < args.length) {
|
|
107
127
|
format = args[i + 1];
|
|
108
128
|
i++;
|
|
109
129
|
} else if (args[i].startsWith('--format=')) {
|
|
@@ -113,6 +133,21 @@ function parseArgs() {
|
|
|
113
133
|
i++;
|
|
114
134
|
} else if (args[i].startsWith('--config=')) {
|
|
115
135
|
config = args[i].split('=')[1];
|
|
136
|
+
} else if ((args[i] === '--out-file' || args[i] === '-o') && i + 1 < args.length) {
|
|
137
|
+
outFile = args[i + 1];
|
|
138
|
+
i++;
|
|
139
|
+
} else if (args[i].startsWith('--out-file=')) {
|
|
140
|
+
outFile = args[i].split('=')[1];
|
|
141
|
+
} else if ((args[i] === '--locale' || args[i] === '-l') && i + 1 < args.length) {
|
|
142
|
+
locale = args[i + 1];
|
|
143
|
+
i++;
|
|
144
|
+
} else if (args[i].startsWith('--locale=')) {
|
|
145
|
+
locale = args[i].split('=')[1];
|
|
146
|
+
} else if ((args[i] === '--ignore' || args[i] === '-i') && i + 1 < args.length) {
|
|
147
|
+
ignorePatterns.push(args[i + 1]);
|
|
148
|
+
i++;
|
|
149
|
+
} else if (args[i].startsWith('--ignore=')) {
|
|
150
|
+
ignorePatterns.push(args[i].split('=')[1]);
|
|
116
151
|
} else {
|
|
117
152
|
positionalArgs.push(args[i]);
|
|
118
153
|
}
|
|
@@ -139,17 +174,77 @@ function parseArgs() {
|
|
|
139
174
|
localeArgs = ['en'];
|
|
140
175
|
}
|
|
141
176
|
|
|
142
|
-
return { srcPattern, outDir, localeArgs, format, config };
|
|
177
|
+
return { srcPattern, outDir, localeArgs, format, config, outFile, ignorePatterns, locale };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
interface ExtractedMessage {
|
|
181
|
+
msgid: string;
|
|
182
|
+
msgctxt?: string;
|
|
183
|
+
/** Pre-computed hash or explicit id, written to `#. id:` and used as the compiled JSON key. */
|
|
184
|
+
precomputedHash?: string;
|
|
185
|
+
locations: Set<string>;
|
|
186
|
+
translatorComment?: string;
|
|
143
187
|
}
|
|
144
188
|
|
|
145
189
|
function main() {
|
|
146
|
-
const {
|
|
190
|
+
const {
|
|
191
|
+
srcPattern,
|
|
192
|
+
outDir,
|
|
193
|
+
localeArgs,
|
|
194
|
+
format: formatArg,
|
|
195
|
+
config: configPath,
|
|
196
|
+
outFile: explicitOutFile,
|
|
197
|
+
ignorePatterns,
|
|
198
|
+
locale: explicitLocale,
|
|
199
|
+
} = parseArgs();
|
|
147
200
|
const { components, translatableProps } = loadConfig(configPath);
|
|
148
201
|
|
|
149
202
|
console.log(`Extracting messages from ${srcPattern} (using Babel)...`);
|
|
203
|
+
if (ignorePatterns.length > 0) {
|
|
204
|
+
console.log(`Ignoring patterns: ${ignorePatterns.join(', ')}`);
|
|
205
|
+
}
|
|
150
206
|
|
|
151
|
-
|
|
152
|
-
|
|
207
|
+
let files = globSync(srcPattern, { absolute: true });
|
|
208
|
+
|
|
209
|
+
if (ignorePatterns.length > 0) {
|
|
210
|
+
files = files.filter((file) => {
|
|
211
|
+
const relativeFile = path.relative(process.cwd(), file);
|
|
212
|
+
const isIgnored = ignorePatterns.some((pattern) => minimatch(relativeFile, pattern));
|
|
213
|
+
return !isIgnored;
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
console.log(`Processing ${files.length} files...`);
|
|
218
|
+
if (files.length < 10) {
|
|
219
|
+
console.log('Files:', files);
|
|
220
|
+
} else {
|
|
221
|
+
console.log('Sample files:', files.slice(0, 5));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const extractedMessages = new Map<string, ExtractedMessage>();
|
|
225
|
+
|
|
226
|
+
const addExtractedMessage = (
|
|
227
|
+
msgid: string,
|
|
228
|
+
msgctxt: string | undefined,
|
|
229
|
+
location: string,
|
|
230
|
+
translatorComment?: string,
|
|
231
|
+
precomputedHash?: string,
|
|
232
|
+
) => {
|
|
233
|
+
const normalizedMsgid = normalizeMessage(msgid);
|
|
234
|
+
const prettyMsgid = prettyPrintICU(msgid);
|
|
235
|
+
const key = precomputedHash ? `hash:${precomputedHash}` : msgctxt ? `ctx:${msgctxt}` : `msg:${normalizedMsgid}`;
|
|
236
|
+
|
|
237
|
+
if (!extractedMessages.has(key)) {
|
|
238
|
+
extractedMessages.set(key, {
|
|
239
|
+
msgid: prettyMsgid,
|
|
240
|
+
msgctxt,
|
|
241
|
+
precomputedHash,
|
|
242
|
+
locations: new Set(),
|
|
243
|
+
translatorComment,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
extractedMessages.get(key)!.locations.add(location);
|
|
247
|
+
};
|
|
153
248
|
|
|
154
249
|
files.forEach((file) => {
|
|
155
250
|
try {
|
|
@@ -159,15 +254,62 @@ function main() {
|
|
|
159
254
|
plugins: ['typescript', 'jsx', 'decorators-legacy'],
|
|
160
255
|
});
|
|
161
256
|
|
|
257
|
+
const relativeFile = path.relative(process.cwd(), file);
|
|
258
|
+
|
|
162
259
|
traverse(ast, {
|
|
163
|
-
|
|
164
|
-
const
|
|
260
|
+
CallExpression(p) {
|
|
261
|
+
const { callee, arguments: args } = p.node;
|
|
262
|
+
if (
|
|
263
|
+
t.isIdentifier(callee) &&
|
|
264
|
+
(callee.name === 'translate' || callee.name === 'translateLabel') &&
|
|
265
|
+
args.length > 0
|
|
266
|
+
) {
|
|
267
|
+
const firstArg = args[0];
|
|
268
|
+
let firstArgValue: string | null = null;
|
|
269
|
+
if (t.isStringLiteral(firstArg)) {
|
|
270
|
+
firstArgValue = firstArg.value;
|
|
271
|
+
} else if (t.isTemplateLiteral(firstArg) && firstArg.quasis.length === 1) {
|
|
272
|
+
firstArgValue = firstArg.quasis[0].value.cooked || firstArg.quasis[0].value.raw;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (firstArgValue) {
|
|
276
|
+
let msgid = firstArgValue;
|
|
277
|
+
let msgctxt: string | undefined = undefined;
|
|
278
|
+
|
|
279
|
+
// Check for second argument { _: "Default Text" }
|
|
280
|
+
if (args.length > 1) {
|
|
281
|
+
const secondArg = args[1];
|
|
282
|
+
if (t.isObjectExpression(secondArg)) {
|
|
283
|
+
const defaultProp = secondArg.properties.find((prop) => {
|
|
284
|
+
const isMatch = t.isObjectProperty(prop) && t.isIdentifier(prop.key) && prop.key.name === '_';
|
|
285
|
+
return isMatch;
|
|
286
|
+
});
|
|
287
|
+
if (defaultProp && t.isObjectProperty(defaultProp)) {
|
|
288
|
+
if (t.isStringLiteral(defaultProp.value)) {
|
|
289
|
+
msgid = defaultProp.value.value;
|
|
290
|
+
msgctxt = firstArgValue; // The first arg is the explicit ID
|
|
291
|
+
} else if (t.isTemplateLiteral(defaultProp.value) && defaultProp.value.quasis.length === 1) {
|
|
292
|
+
msgid = defaultProp.value.quasis[0].value.cooked || defaultProp.value.quasis[0].value.raw;
|
|
293
|
+
msgctxt = firstArgValue;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const line = p.node.loc?.start.line || 0;
|
|
300
|
+
const location = `${relativeFile}:${line}`;
|
|
301
|
+
addExtractedMessage(msgid, msgctxt, location);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
JSXOpeningElement(p) {
|
|
306
|
+
const tagName = getJSXElementName(p.node.name);
|
|
165
307
|
|
|
166
308
|
const baseNameMatch =
|
|
167
309
|
components.has(tagName) || Array.from(components).some((c) => tagName.startsWith(c + '.') || tagName === c);
|
|
168
310
|
|
|
169
311
|
if (baseNameMatch) {
|
|
170
|
-
|
|
312
|
+
p.node.attributes.forEach((attr) => {
|
|
171
313
|
if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name) && translatableProps.has(attr.name.name)) {
|
|
172
314
|
let textValue: string | null = null;
|
|
173
315
|
|
|
@@ -180,19 +322,71 @@ function main() {
|
|
|
180
322
|
if (t.isStringLiteral(expr)) {
|
|
181
323
|
textValue = expr.value;
|
|
182
324
|
} else if (expr.quasis.length === 1) {
|
|
183
|
-
textValue = expr.quasis[0].value.raw;
|
|
325
|
+
textValue = expr.quasis[0].value.cooked || expr.quasis[0].value.raw;
|
|
184
326
|
}
|
|
185
327
|
}
|
|
186
328
|
}
|
|
187
329
|
}
|
|
188
330
|
|
|
189
331
|
if (textValue && textValue.trim() !== '') {
|
|
190
|
-
|
|
332
|
+
const line = attr.loc?.start.line || 0;
|
|
333
|
+
const location = `${relativeFile}:${line}`;
|
|
334
|
+
addExtractedMessage(textValue, undefined, location);
|
|
191
335
|
}
|
|
192
336
|
}
|
|
193
337
|
});
|
|
194
338
|
}
|
|
195
339
|
},
|
|
340
|
+
JSXElement(p) {
|
|
341
|
+
const tagName = getJSXElementName(p.node.openingElement.name);
|
|
342
|
+
if (!CHILDREN_AS_KEY_COMPONENTS.has(tagName)) return;
|
|
343
|
+
|
|
344
|
+
// Extract text from children: JSXText or StringLiteral in JSXExpressionContainer
|
|
345
|
+
let textValue: string | null = null;
|
|
346
|
+
for (const child of p.node.children) {
|
|
347
|
+
if (t.isJSXText(child) && child.value.trim()) {
|
|
348
|
+
textValue = child.value.trim();
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
if (t.isJSXExpressionContainer(child) && t.isStringLiteral(child.expression)) {
|
|
352
|
+
textValue = child.expression.value;
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (!textValue) return;
|
|
357
|
+
|
|
358
|
+
// Extract id, context, and comment from opening element attributes
|
|
359
|
+
let explicitId: string | undefined;
|
|
360
|
+
let msgctxt: string | undefined;
|
|
361
|
+
let translatorComment: string | undefined;
|
|
362
|
+
for (const attr of p.node.openingElement.attributes) {
|
|
363
|
+
if (!t.isJSXAttribute(attr) || !t.isJSXIdentifier(attr.name)) continue;
|
|
364
|
+
const val = t.isStringLiteral(attr.value)
|
|
365
|
+
? attr.value.value
|
|
366
|
+
: t.isJSXExpressionContainer(attr.value) && t.isStringLiteral(attr.value.expression)
|
|
367
|
+
? attr.value.expression.value
|
|
368
|
+
: undefined;
|
|
369
|
+
if (val === undefined) continue;
|
|
370
|
+
if (attr.name.name === 'id') explicitId = val;
|
|
371
|
+
if (attr.name.name === 'context') msgctxt = val;
|
|
372
|
+
if (attr.name.name === 'comment') translatorComment = val;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Compute the hash written to `#. id:` and used as the compiled JSON key:
|
|
376
|
+
// id present → literal id (e.g. "action.archive"), no msgctxt
|
|
377
|
+
// context only → hash(context + \x04 + message), msgctxt = context
|
|
378
|
+
// neither → hash(message), no msgctxt
|
|
379
|
+
let precomputedHash: string | undefined;
|
|
380
|
+
if (explicitId) {
|
|
381
|
+
precomputedHash = explicitId;
|
|
382
|
+
msgctxt = undefined; // id supersedes context; no msgctxt in PO
|
|
383
|
+
} else if (msgctxt) {
|
|
384
|
+
precomputedHash = generateMessageId(`${msgctxt}\x04${normalizeMessage(textValue)}`);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const line = p.node.loc?.start.line || 0;
|
|
388
|
+
addExtractedMessage(textValue, msgctxt, `${relativeFile}:${line}`, translatorComment, precomputedHash);
|
|
389
|
+
},
|
|
196
390
|
});
|
|
197
391
|
} catch (e: any) {
|
|
198
392
|
console.error(`Failed to parse ${file}:`, e.message);
|
|
@@ -203,45 +397,51 @@ function main() {
|
|
|
203
397
|
|
|
204
398
|
const targets: { outFile: string; locale: string; format: string }[] = [];
|
|
205
399
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
if (arg.includes('.') || arg.includes('/') || arg.includes('\\')) {
|
|
227
|
-
outFile = arg;
|
|
228
|
-
const ext = path.extname(arg).slice(1);
|
|
229
|
-
format = formatArg || (ext === 'po' ? 'po' : 'json');
|
|
230
|
-
|
|
231
|
-
locale = path.basename(arg, '.' + ext);
|
|
232
|
-
if (locale === 'messages' || locale === 'translations' || locale === 'LC_MESSAGES') {
|
|
233
|
-
const parts = path.resolve(arg).split(path.sep);
|
|
234
|
-
locale = parts[parts.length - 2];
|
|
235
|
-
}
|
|
400
|
+
if (explicitOutFile) {
|
|
401
|
+
const ext = path.extname(explicitOutFile).slice(1);
|
|
402
|
+
const format = formatArg || (ext === 'po' ? 'po' : 'json');
|
|
403
|
+
targets.push({ outFile: explicitOutFile, locale: explicitLocale || 'en', format });
|
|
404
|
+
} else {
|
|
405
|
+
localeArgs.forEach((arg) => {
|
|
406
|
+
if (arg.includes('*')) {
|
|
407
|
+
const matchedFiles = globSync(arg, { absolute: true });
|
|
408
|
+
matchedFiles.forEach((file) => {
|
|
409
|
+
const ext = path.extname(file).slice(1);
|
|
410
|
+
const format = formatArg || (ext === 'po' ? 'po' : 'json');
|
|
411
|
+
|
|
412
|
+
// Guess locale from path: locales/en.po -> en, locales/en/messages.po -> en
|
|
413
|
+
let locale = path.basename(file, '.' + ext);
|
|
414
|
+
if (locale === 'messages' || locale === 'translations' || locale === 'LC_MESSAGES') {
|
|
415
|
+
const parts = file.split(path.sep);
|
|
416
|
+
locale = parts[parts.length - 2];
|
|
417
|
+
}
|
|
418
|
+
targets.push({ outFile: file, locale, format });
|
|
419
|
+
});
|
|
236
420
|
} else {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
421
|
+
let outFile: string;
|
|
422
|
+
let locale: string;
|
|
423
|
+
let format: string;
|
|
424
|
+
|
|
425
|
+
if (arg.includes('.') || arg.includes('/') || arg.includes('\\')) {
|
|
426
|
+
outFile = arg;
|
|
427
|
+
const ext = path.extname(arg).slice(1);
|
|
428
|
+
format = formatArg || (ext === 'po' ? 'po' : 'json');
|
|
429
|
+
|
|
430
|
+
locale = path.basename(arg, '.' + ext);
|
|
431
|
+
if (locale === 'messages' || locale === 'translations' || locale === 'LC_MESSAGES') {
|
|
432
|
+
const parts = path.resolve(arg).split(path.sep);
|
|
433
|
+
locale = parts[parts.length - 2];
|
|
434
|
+
}
|
|
435
|
+
} else {
|
|
436
|
+
format = formatArg || 'json';
|
|
437
|
+
const extension = format === 'po' ? 'po' : 'json';
|
|
438
|
+
outFile = path.join(outDir, `${arg}.${extension}`);
|
|
439
|
+
locale = arg;
|
|
440
|
+
}
|
|
441
|
+
targets.push({ outFile, locale, format });
|
|
241
442
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
});
|
|
443
|
+
});
|
|
444
|
+
}
|
|
245
445
|
|
|
246
446
|
if (targets.length === 0) {
|
|
247
447
|
console.error('No target files found or specified.');
|
|
@@ -249,7 +449,7 @@ function main() {
|
|
|
249
449
|
}
|
|
250
450
|
|
|
251
451
|
targets.forEach(({ outFile, locale, format }) => {
|
|
252
|
-
let existingTranslations: Record<string,
|
|
452
|
+
let existingTranslations: Record<string, any> = {};
|
|
253
453
|
|
|
254
454
|
if (fs.existsSync(outFile)) {
|
|
255
455
|
try {
|
|
@@ -285,26 +485,49 @@ function main() {
|
|
|
285
485
|
}
|
|
286
486
|
}
|
|
287
487
|
|
|
288
|
-
const updatedTranslations: Record<
|
|
488
|
+
const updatedTranslations: Record<
|
|
489
|
+
string,
|
|
490
|
+
{
|
|
491
|
+
defaultMessage: string;
|
|
492
|
+
translation: string;
|
|
493
|
+
locations?: string[];
|
|
494
|
+
msgctxt?: string;
|
|
495
|
+
translatorComment?: string;
|
|
496
|
+
}
|
|
497
|
+
> = {};
|
|
289
498
|
let addedCount = 0;
|
|
290
499
|
|
|
291
|
-
extractedMessages.forEach((
|
|
292
|
-
const
|
|
293
|
-
if (existingTranslations[
|
|
294
|
-
|
|
295
|
-
updatedTranslations[
|
|
500
|
+
extractedMessages.forEach((data) => {
|
|
501
|
+
const hash = data.precomputedHash ?? (data.msgctxt ? data.msgctxt : generateMessageId(data.msgid));
|
|
502
|
+
if (existingTranslations[hash]) {
|
|
503
|
+
const existing = existingTranslations[hash];
|
|
504
|
+
updatedTranslations[hash] = {
|
|
505
|
+
defaultMessage: data.msgid,
|
|
506
|
+
translation: existing.translation || (existing.description ? '' : ''),
|
|
507
|
+
locations: Array.from(data.locations),
|
|
508
|
+
msgctxt: data.msgctxt,
|
|
509
|
+
translatorComment: data.translatorComment,
|
|
510
|
+
};
|
|
296
511
|
} else {
|
|
297
|
-
updatedTranslations[
|
|
298
|
-
defaultMessage:
|
|
512
|
+
updatedTranslations[hash] = {
|
|
513
|
+
defaultMessage: data.msgid,
|
|
299
514
|
translation: '',
|
|
515
|
+
locations: Array.from(data.locations),
|
|
516
|
+
msgctxt: data.msgctxt,
|
|
517
|
+
translatorComment: data.translatorComment,
|
|
300
518
|
};
|
|
301
519
|
addedCount++;
|
|
302
520
|
}
|
|
303
521
|
});
|
|
304
522
|
|
|
523
|
+
// Keep hardcoded keys (like ra.*)
|
|
305
524
|
Object.keys(existingTranslations).forEach((key) => {
|
|
306
525
|
if (!updatedTranslations[key]) {
|
|
307
|
-
|
|
526
|
+
const existing = existingTranslations[key];
|
|
527
|
+
updatedTranslations[key] = {
|
|
528
|
+
defaultMessage: existing.defaultMessage || '',
|
|
529
|
+
translation: existing.translation || '',
|
|
530
|
+
};
|
|
308
531
|
}
|
|
309
532
|
});
|
|
310
533
|
|
|
@@ -323,11 +546,26 @@ function main() {
|
|
|
323
546
|
};
|
|
324
547
|
|
|
325
548
|
Object.entries(updatedTranslations).forEach(([hash, data]) => {
|
|
326
|
-
|
|
549
|
+
const context = data.msgctxt || '';
|
|
550
|
+
if (!poData.translations[context]) {
|
|
551
|
+
poData.translations[context] = {};
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// To achieve multi-line PO visual without \n, we must ensure the strings
|
|
555
|
+
// themselves don't have newlines before gettext-parser sees them.
|
|
556
|
+
// However, we WANT the structured look.
|
|
557
|
+
// If we want gettext-parser to wrap, we usually can't control it.
|
|
558
|
+
// Instead, we will use our previously successful "compiledPo.replace" approach
|
|
559
|
+
// but with a better regex that actually works on the serialized output.
|
|
560
|
+
|
|
561
|
+
poData.translations[context][data.defaultMessage] = {
|
|
327
562
|
msgid: data.defaultMessage,
|
|
563
|
+
msgctxt: data.msgctxt ? data.msgctxt : undefined,
|
|
328
564
|
msgstr: [data.translation],
|
|
329
565
|
comments: {
|
|
566
|
+
...(data.translatorComment ? { translator: data.translatorComment } : {}),
|
|
330
567
|
extracted: `id: ${hash}`,
|
|
568
|
+
reference: data.locations?.join('\n'),
|
|
331
569
|
},
|
|
332
570
|
};
|
|
333
571
|
});
|
|
@@ -342,6 +580,4 @@ function main() {
|
|
|
342
580
|
});
|
|
343
581
|
}
|
|
344
582
|
|
|
345
|
-
|
|
346
|
-
main();
|
|
347
|
-
}
|
|
583
|
+
main();
|
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
2
|
+
"strato.action.add": {
|
|
3
3
|
"defaultMessage": "Add",
|
|
4
4
|
"description": "Label for the button to add a new record"
|
|
5
5
|
},
|
|
6
|
-
"
|
|
6
|
+
"strato.action.add_filter": {
|
|
7
7
|
"defaultMessage": "Add filter"
|
|
8
8
|
},
|
|
9
|
-
"
|
|
9
|
+
"strato.action.cancel": {
|
|
10
10
|
"defaultMessage": "Cancel"
|
|
11
11
|
},
|
|
12
|
-
"
|
|
12
|
+
"strato.action.delete": {
|
|
13
13
|
"defaultMessage": "Delete"
|
|
14
14
|
},
|
|
15
|
-
"
|
|
15
|
+
"strato.action.edit": {
|
|
16
16
|
"defaultMessage": "Edit"
|
|
17
17
|
},
|
|
18
|
-
"
|
|
18
|
+
"strato.action.remove_filter": {
|
|
19
19
|
"defaultMessage": "Remove filter"
|
|
20
20
|
},
|
|
21
|
-
"
|
|
21
|
+
"strato.action.save": {
|
|
22
22
|
"defaultMessage": "Save",
|
|
23
23
|
"description": "Label for the button to save a record"
|
|
24
24
|
},
|
|
25
|
-
"
|
|
25
|
+
"strato.action.search": {
|
|
26
26
|
"defaultMessage": "Search"
|
|
27
27
|
}
|
|
28
28
|
}
|