@strato-admin/i18n-cli 0.1.1 → 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.
@@ -0,0 +1,558 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ var __importDefault = (this && this.__importDefault) || function (mod) {
37
+ return (mod && mod.__esModule) ? mod : { "default": mod };
38
+ };
39
+ Object.defineProperty(exports, "__esModule", { value: true });
40
+ const fs = __importStar(require("fs"));
41
+ const path = __importStar(require("path"));
42
+ const glob_1 = require("glob");
43
+ const parser_1 = require("@babel/parser");
44
+ const traverse_1 = __importDefault(require("@babel/traverse"));
45
+ const t = __importStar(require("@babel/types"));
46
+ const gettextParser = __importStar(require("gettext-parser"));
47
+ const i18n_1 = require("@strato-admin/i18n");
48
+ const minimatch_1 = require("minimatch");
49
+ // Components whose JSX children text is the translatable string (source-as-key)
50
+ const CHILDREN_AS_KEY_COMPONENTS = new Set(['Message', 'RecordMessage']);
51
+ // List of Strato Admin components to extract from
52
+ const DEFAULT_STRATO_COMPONENTS = new Set([
53
+ 'ArrayField',
54
+ 'AttributeEditor',
55
+ 'AutocompleteInput',
56
+ 'BooleanField',
57
+ 'BulkDeleteButton',
58
+ 'Button',
59
+ 'Create',
60
+ 'CreateButton',
61
+ 'DateField',
62
+ 'Edit',
63
+ 'EditButton',
64
+ 'FormField',
65
+ 'List',
66
+ 'NumberField',
67
+ 'NumberInput',
68
+ 'ReferenceField',
69
+ 'ReferenceInput',
70
+ 'ReferenceManyField',
71
+ 'Resource',
72
+ 'ResourceSchema',
73
+ 'SaveButton',
74
+ 'SelectInput',
75
+ 'Show',
76
+ 'StatusIndicatorField.Label',
77
+ 'Table',
78
+ 'Table.Col',
79
+ 'TextAreaInput',
80
+ 'TextField',
81
+ 'TextInput',
82
+ ]);
83
+ // List of translatable props
84
+ const DEFAULT_TRANSLATABLE_PROPS = new Set([
85
+ 'label',
86
+ 'listLabel',
87
+ 'createLabel',
88
+ 'editLabel',
89
+ 'detailLabel',
90
+ 'title',
91
+ 'listTitle',
92
+ 'createTitle',
93
+ 'editTitle',
94
+ 'detailTitle',
95
+ 'placeholder',
96
+ 'emptyText',
97
+ 'helperText',
98
+ 'description',
99
+ 'listDescription',
100
+ 'createDescription',
101
+ 'editDescription',
102
+ 'detailDescription',
103
+ 'saveButtonLabel',
104
+ 'successMessage',
105
+ 'errorMessage',
106
+ ]);
107
+ function loadConfig(configPath) {
108
+ const components = new Set(DEFAULT_STRATO_COMPONENTS);
109
+ const translatableProps = new Set(DEFAULT_TRANSLATABLE_PROPS);
110
+ const resolvedConfigPath = configPath || path.join(process.cwd(), 'strato-i18n.config.json');
111
+ if (fs.existsSync(resolvedConfigPath)) {
112
+ try {
113
+ const config = JSON.parse(fs.readFileSync(resolvedConfigPath, 'utf8'));
114
+ if (config.components) {
115
+ config.components.forEach((c) => components.add(c));
116
+ }
117
+ if (config.translatableProps) {
118
+ config.translatableProps.forEach((p) => translatableProps.add(p));
119
+ }
120
+ console.log(`Loaded configuration from ${resolvedConfigPath}`);
121
+ }
122
+ catch (e) {
123
+ console.error(`Failed to parse configuration file at ${resolvedConfigPath}:`, e.message);
124
+ }
125
+ }
126
+ return { components, translatableProps };
127
+ }
128
+ function getJSXElementName(node) {
129
+ if (t.isJSXIdentifier(node)) {
130
+ return node.name;
131
+ }
132
+ if (t.isJSXMemberExpression(node)) {
133
+ return `${getJSXElementName(node.object)}.${node.property.name}`;
134
+ }
135
+ if (t.isJSXNamespacedName(node)) {
136
+ return `${node.namespace.name}:${node.name.name}`;
137
+ }
138
+ return '';
139
+ }
140
+ function parseArgs() {
141
+ const args = process.argv.slice(2);
142
+ let format = undefined;
143
+ let config = undefined;
144
+ let outFile = undefined;
145
+ let locale = undefined;
146
+ const ignorePatterns = [];
147
+ const positionalArgs = [];
148
+ for (let i = 0; i < args.length; i++) {
149
+ if ((args[i] === '--format' || args[i] === '-f') && i + 1 < args.length) {
150
+ format = args[i + 1];
151
+ i++;
152
+ }
153
+ else if (args[i].startsWith('--format=')) {
154
+ format = args[i].split('=')[1];
155
+ }
156
+ else if (args[i] === '--config' && i + 1 < args.length) {
157
+ config = args[i + 1];
158
+ i++;
159
+ }
160
+ else if (args[i].startsWith('--config=')) {
161
+ config = args[i].split('=')[1];
162
+ }
163
+ else if ((args[i] === '--out-file' || args[i] === '-o') && i + 1 < args.length) {
164
+ outFile = args[i + 1];
165
+ i++;
166
+ }
167
+ else if (args[i].startsWith('--out-file=')) {
168
+ outFile = args[i].split('=')[1];
169
+ }
170
+ else if ((args[i] === '--locale' || args[i] === '-l') && i + 1 < args.length) {
171
+ locale = args[i + 1];
172
+ i++;
173
+ }
174
+ else if (args[i].startsWith('--locale=')) {
175
+ locale = args[i].split('=')[1];
176
+ }
177
+ else if ((args[i] === '--ignore' || args[i] === '-i') && i + 1 < args.length) {
178
+ ignorePatterns.push(args[i + 1]);
179
+ i++;
180
+ }
181
+ else if (args[i].startsWith('--ignore=')) {
182
+ ignorePatterns.push(args[i].split('=')[1]);
183
+ }
184
+ else {
185
+ positionalArgs.push(args[i]);
186
+ }
187
+ }
188
+ const srcPattern = positionalArgs[0] || 'src/**/*.{ts,tsx}';
189
+ let outDir = 'locales';
190
+ let localeArgs = [];
191
+ if (positionalArgs.length > 1) {
192
+ if (positionalArgs[1].includes('*')) {
193
+ // First target is a glob, so we treat ALL subsequent args as targets
194
+ outDir = '.';
195
+ localeArgs = positionalArgs.slice(1);
196
+ }
197
+ else {
198
+ // First target is a directory
199
+ outDir = positionalArgs[1];
200
+ localeArgs = positionalArgs.slice(2);
201
+ if (localeArgs.length === 0) {
202
+ localeArgs = ['en'];
203
+ }
204
+ }
205
+ }
206
+ else {
207
+ localeArgs = ['en'];
208
+ }
209
+ return { srcPattern, outDir, localeArgs, format, config, outFile, ignorePatterns, locale };
210
+ }
211
+ function main() {
212
+ const { srcPattern, outDir, localeArgs, format: formatArg, config: configPath, outFile: explicitOutFile, ignorePatterns, locale: explicitLocale, } = parseArgs();
213
+ const { components, translatableProps } = loadConfig(configPath);
214
+ console.log(`Extracting messages from ${srcPattern} (using Babel)...`);
215
+ if (ignorePatterns.length > 0) {
216
+ console.log(`Ignoring patterns: ${ignorePatterns.join(', ')}`);
217
+ }
218
+ let files = (0, glob_1.globSync)(srcPattern, { absolute: true });
219
+ if (ignorePatterns.length > 0) {
220
+ files = files.filter((file) => {
221
+ const relativeFile = path.relative(process.cwd(), file);
222
+ const isIgnored = ignorePatterns.some((pattern) => (0, minimatch_1.minimatch)(relativeFile, pattern));
223
+ return !isIgnored;
224
+ });
225
+ }
226
+ console.log(`Processing ${files.length} files...`);
227
+ if (files.length < 10) {
228
+ console.log('Files:', files);
229
+ }
230
+ else {
231
+ console.log('Sample files:', files.slice(0, 5));
232
+ }
233
+ const extractedMessages = new Map();
234
+ const addExtractedMessage = (msgid, msgctxt, location, translatorComment, precomputedHash) => {
235
+ const normalizedMsgid = (0, i18n_1.normalizeMessage)(msgid);
236
+ const prettyMsgid = (0, i18n_1.prettyPrintICU)(msgid);
237
+ const key = precomputedHash ? `hash:${precomputedHash}` : msgctxt ? `ctx:${msgctxt}` : `msg:${normalizedMsgid}`;
238
+ if (!extractedMessages.has(key)) {
239
+ extractedMessages.set(key, {
240
+ msgid: prettyMsgid,
241
+ msgctxt,
242
+ precomputedHash,
243
+ locations: new Set(),
244
+ translatorComment,
245
+ });
246
+ }
247
+ extractedMessages.get(key).locations.add(location);
248
+ };
249
+ files.forEach((file) => {
250
+ try {
251
+ const content = fs.readFileSync(file, 'utf8');
252
+ const ast = (0, parser_1.parse)(content, {
253
+ sourceType: 'module',
254
+ plugins: ['typescript', 'jsx', 'decorators-legacy'],
255
+ });
256
+ const relativeFile = path.relative(process.cwd(), file);
257
+ (0, traverse_1.default)(ast, {
258
+ CallExpression(p) {
259
+ const { callee, arguments: args } = p.node;
260
+ if (t.isIdentifier(callee) &&
261
+ (callee.name === 'translate' || callee.name === 'translateLabel') &&
262
+ args.length > 0) {
263
+ const firstArg = args[0];
264
+ let firstArgValue = null;
265
+ if (t.isStringLiteral(firstArg)) {
266
+ firstArgValue = firstArg.value;
267
+ }
268
+ else if (t.isTemplateLiteral(firstArg) && firstArg.quasis.length === 1) {
269
+ firstArgValue = firstArg.quasis[0].value.cooked || firstArg.quasis[0].value.raw;
270
+ }
271
+ if (firstArgValue) {
272
+ let msgid = firstArgValue;
273
+ let msgctxt = undefined;
274
+ // Check for second argument { _: "Default Text" }
275
+ if (args.length > 1) {
276
+ const secondArg = args[1];
277
+ if (t.isObjectExpression(secondArg)) {
278
+ const defaultProp = secondArg.properties.find((prop) => {
279
+ const isMatch = t.isObjectProperty(prop) && t.isIdentifier(prop.key) && prop.key.name === '_';
280
+ return isMatch;
281
+ });
282
+ if (defaultProp && t.isObjectProperty(defaultProp)) {
283
+ if (t.isStringLiteral(defaultProp.value)) {
284
+ msgid = defaultProp.value.value;
285
+ msgctxt = firstArgValue; // The first arg is the explicit ID
286
+ }
287
+ else if (t.isTemplateLiteral(defaultProp.value) && defaultProp.value.quasis.length === 1) {
288
+ msgid = defaultProp.value.quasis[0].value.cooked || defaultProp.value.quasis[0].value.raw;
289
+ msgctxt = firstArgValue;
290
+ }
291
+ }
292
+ }
293
+ }
294
+ const line = p.node.loc?.start.line || 0;
295
+ const location = `${relativeFile}:${line}`;
296
+ addExtractedMessage(msgid, msgctxt, location);
297
+ }
298
+ }
299
+ },
300
+ JSXOpeningElement(p) {
301
+ const tagName = getJSXElementName(p.node.name);
302
+ const baseNameMatch = components.has(tagName) || Array.from(components).some((c) => tagName.startsWith(c + '.') || tagName === c);
303
+ if (baseNameMatch) {
304
+ p.node.attributes.forEach((attr) => {
305
+ if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name) && translatableProps.has(attr.name.name)) {
306
+ let textValue = null;
307
+ if (attr.value) {
308
+ if (t.isStringLiteral(attr.value)) {
309
+ textValue = attr.value.value;
310
+ }
311
+ else if (t.isJSXExpressionContainer(attr.value)) {
312
+ const expr = attr.value.expression;
313
+ if (t.isStringLiteral(expr) || t.isTemplateLiteral(expr)) {
314
+ if (t.isStringLiteral(expr)) {
315
+ textValue = expr.value;
316
+ }
317
+ else if (expr.quasis.length === 1) {
318
+ textValue = expr.quasis[0].value.cooked || expr.quasis[0].value.raw;
319
+ }
320
+ }
321
+ }
322
+ }
323
+ if (textValue && textValue.trim() !== '') {
324
+ const line = attr.loc?.start.line || 0;
325
+ const location = `${relativeFile}:${line}`;
326
+ addExtractedMessage(textValue, undefined, location);
327
+ }
328
+ }
329
+ });
330
+ }
331
+ },
332
+ JSXElement(p) {
333
+ const tagName = getJSXElementName(p.node.openingElement.name);
334
+ if (!CHILDREN_AS_KEY_COMPONENTS.has(tagName))
335
+ return;
336
+ // Extract text from children: JSXText or StringLiteral in JSXExpressionContainer
337
+ let textValue = null;
338
+ for (const child of p.node.children) {
339
+ if (t.isJSXText(child) && child.value.trim()) {
340
+ textValue = child.value.trim();
341
+ break;
342
+ }
343
+ if (t.isJSXExpressionContainer(child) && t.isStringLiteral(child.expression)) {
344
+ textValue = child.expression.value;
345
+ break;
346
+ }
347
+ }
348
+ if (!textValue)
349
+ return;
350
+ // Extract id, context, and comment from opening element attributes
351
+ let explicitId;
352
+ let msgctxt;
353
+ let translatorComment;
354
+ for (const attr of p.node.openingElement.attributes) {
355
+ if (!t.isJSXAttribute(attr) || !t.isJSXIdentifier(attr.name))
356
+ continue;
357
+ const val = t.isStringLiteral(attr.value)
358
+ ? attr.value.value
359
+ : t.isJSXExpressionContainer(attr.value) && t.isStringLiteral(attr.value.expression)
360
+ ? attr.value.expression.value
361
+ : undefined;
362
+ if (val === undefined)
363
+ continue;
364
+ if (attr.name.name === 'id')
365
+ explicitId = val;
366
+ if (attr.name.name === 'context')
367
+ msgctxt = val;
368
+ if (attr.name.name === 'comment')
369
+ translatorComment = val;
370
+ }
371
+ // Compute the hash written to `#. id:` and used as the compiled JSON key:
372
+ // id present → literal id (e.g. "action.archive"), no msgctxt
373
+ // context only → hash(context + \x04 + message), msgctxt = context
374
+ // neither → hash(message), no msgctxt
375
+ let precomputedHash;
376
+ if (explicitId) {
377
+ precomputedHash = explicitId;
378
+ msgctxt = undefined; // id supersedes context; no msgctxt in PO
379
+ }
380
+ else if (msgctxt) {
381
+ precomputedHash = (0, i18n_1.generateMessageId)(`${msgctxt}\x04${(0, i18n_1.normalizeMessage)(textValue)}`);
382
+ }
383
+ const line = p.node.loc?.start.line || 0;
384
+ addExtractedMessage(textValue, msgctxt, `${relativeFile}:${line}`, translatorComment, precomputedHash);
385
+ },
386
+ });
387
+ }
388
+ catch (e) {
389
+ console.error(`Failed to parse ${file}:`, e.message);
390
+ }
391
+ });
392
+ console.log(`Found ${extractedMessages.size} translatable strings.`);
393
+ const targets = [];
394
+ if (explicitOutFile) {
395
+ const ext = path.extname(explicitOutFile).slice(1);
396
+ const format = formatArg || (ext === 'po' ? 'po' : 'json');
397
+ targets.push({ outFile: explicitOutFile, locale: explicitLocale || 'en', format });
398
+ }
399
+ else {
400
+ localeArgs.forEach((arg) => {
401
+ if (arg.includes('*')) {
402
+ const matchedFiles = (0, glob_1.globSync)(arg, { absolute: true });
403
+ matchedFiles.forEach((file) => {
404
+ const ext = path.extname(file).slice(1);
405
+ const format = formatArg || (ext === 'po' ? 'po' : 'json');
406
+ // Guess locale from path: locales/en.po -> en, locales/en/messages.po -> en
407
+ let locale = path.basename(file, '.' + ext);
408
+ if (locale === 'messages' || locale === 'translations' || locale === 'LC_MESSAGES') {
409
+ const parts = file.split(path.sep);
410
+ locale = parts[parts.length - 2];
411
+ }
412
+ targets.push({ outFile: file, locale, format });
413
+ });
414
+ }
415
+ else {
416
+ let outFile;
417
+ let locale;
418
+ let format;
419
+ if (arg.includes('.') || arg.includes('/') || arg.includes('\\')) {
420
+ outFile = arg;
421
+ const ext = path.extname(arg).slice(1);
422
+ format = formatArg || (ext === 'po' ? 'po' : 'json');
423
+ locale = path.basename(arg, '.' + ext);
424
+ if (locale === 'messages' || locale === 'translations' || locale === 'LC_MESSAGES') {
425
+ const parts = path.resolve(arg).split(path.sep);
426
+ locale = parts[parts.length - 2];
427
+ }
428
+ }
429
+ else {
430
+ format = formatArg || 'json';
431
+ const extension = format === 'po' ? 'po' : 'json';
432
+ outFile = path.join(outDir, `${arg}.${extension}`);
433
+ locale = arg;
434
+ }
435
+ targets.push({ outFile, locale, format });
436
+ }
437
+ });
438
+ }
439
+ if (targets.length === 0) {
440
+ console.error('No target files found or specified.');
441
+ process.exit(1);
442
+ }
443
+ targets.forEach(({ outFile, locale, format }) => {
444
+ let existingTranslations = {};
445
+ if (fs.existsSync(outFile)) {
446
+ try {
447
+ if (format === 'po') {
448
+ const fileContent = fs.readFileSync(outFile);
449
+ const parsedPo = gettextParser.po.parse(fileContent);
450
+ Object.entries(parsedPo.translations).forEach(([context, entries]) => {
451
+ Object.entries(entries).forEach(([msgid, data]) => {
452
+ if (msgid === '')
453
+ return;
454
+ // Find the hash: check context (v2), then comment (v3), then msgid (v1)
455
+ const commentHash = data.comments?.extracted?.match(/id: (\w+)/)?.[1];
456
+ const hash = context || commentHash || msgid;
457
+ existingTranslations[hash] = {
458
+ defaultMessage: data.msgid || data.comments?.extracted || '',
459
+ translation: data.msgstr[0] || '',
460
+ };
461
+ });
462
+ });
463
+ }
464
+ else {
465
+ existingTranslations = JSON.parse(fs.readFileSync(outFile, 'utf8'));
466
+ }
467
+ }
468
+ catch (e) {
469
+ console.error(`Failed to parse existing translation file at ${outFile}:`, e.message);
470
+ return; // Skip this file
471
+ }
472
+ }
473
+ else {
474
+ const parentDir = path.dirname(outFile);
475
+ if (!fs.existsSync(parentDir)) {
476
+ fs.mkdirSync(parentDir, { recursive: true });
477
+ }
478
+ }
479
+ const updatedTranslations = {};
480
+ let addedCount = 0;
481
+ extractedMessages.forEach((data) => {
482
+ const hash = data.precomputedHash ?? (data.msgctxt ? data.msgctxt : (0, i18n_1.generateMessageId)(data.msgid));
483
+ if (existingTranslations[hash]) {
484
+ const existing = existingTranslations[hash];
485
+ updatedTranslations[hash] = {
486
+ defaultMessage: data.msgid,
487
+ translation: existing.translation || (existing.description ? '' : ''),
488
+ locations: Array.from(data.locations),
489
+ msgctxt: data.msgctxt,
490
+ translatorComment: data.translatorComment,
491
+ };
492
+ }
493
+ else {
494
+ updatedTranslations[hash] = {
495
+ defaultMessage: data.msgid,
496
+ translation: '',
497
+ locations: Array.from(data.locations),
498
+ msgctxt: data.msgctxt,
499
+ translatorComment: data.translatorComment,
500
+ };
501
+ addedCount++;
502
+ }
503
+ });
504
+ // Keep hardcoded keys (like ra.*)
505
+ Object.keys(existingTranslations).forEach((key) => {
506
+ if (!updatedTranslations[key]) {
507
+ const existing = existingTranslations[key];
508
+ updatedTranslations[key] = {
509
+ defaultMessage: existing.defaultMessage || '',
510
+ translation: existing.translation || '',
511
+ };
512
+ }
513
+ });
514
+ if (format === 'po') {
515
+ const poData = {
516
+ charset: 'utf-8',
517
+ headers: {
518
+ 'content-type': 'text/plain; charset=utf-8',
519
+ language: locale,
520
+ },
521
+ translations: {
522
+ '': {
523
+ '': { msgid: '', msgstr: [''] }, // Header
524
+ },
525
+ },
526
+ };
527
+ Object.entries(updatedTranslations).forEach(([hash, data]) => {
528
+ const context = data.msgctxt || '';
529
+ if (!poData.translations[context]) {
530
+ poData.translations[context] = {};
531
+ }
532
+ // To achieve multi-line PO visual without \n, we must ensure the strings
533
+ // themselves don't have newlines before gettext-parser sees them.
534
+ // However, we WANT the structured look.
535
+ // If we want gettext-parser to wrap, we usually can't control it.
536
+ // Instead, we will use our previously successful "compiledPo.replace" approach
537
+ // but with a better regex that actually works on the serialized output.
538
+ poData.translations[context][data.defaultMessage] = {
539
+ msgid: data.defaultMessage,
540
+ msgctxt: data.msgctxt ? data.msgctxt : undefined,
541
+ msgstr: [data.translation],
542
+ comments: {
543
+ ...(data.translatorComment ? { translator: data.translatorComment } : {}),
544
+ extracted: `id: ${hash}`,
545
+ reference: data.locations?.join('\n'),
546
+ },
547
+ };
548
+ });
549
+ const compiledPo = gettextParser.po.compile(poData);
550
+ fs.writeFileSync(outFile, compiledPo);
551
+ }
552
+ else {
553
+ fs.writeFileSync(outFile, JSON.stringify(updatedTranslations, null, 2));
554
+ }
555
+ console.log(`Updated ${outFile} (${locale}): Added ${addedCount} new messages.`);
556
+ });
557
+ }
558
+ main();
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Pretty-prints an ICU message for better readability in translation files.
3
+ * Uses newlines and indentation for nested structures.
4
+ */
5
+ export declare function prettyPrintICU(msg: string): string;
6
+ /**
7
+ * Canonicalizes an ICU message using the official printer and normalization.
8
+ * This ensures consistent spacing and a single-line format for hashing and compilation.
9
+ */
10
+ export declare function canonicalizeMessage(msg: string): string;
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.prettyPrintICU = prettyPrintICU;
4
+ exports.canonicalizeMessage = canonicalizeMessage;
5
+ const icu_messageformat_parser_1 = require("@formatjs/icu-messageformat-parser");
6
+ const printer_js_1 = require("@formatjs/icu-messageformat-parser/printer.js");
7
+ const hash_1 = require("./hash");
8
+ /**
9
+ * Pretty-prints an ICU message for better readability in translation files.
10
+ * Uses newlines and indentation for nested structures.
11
+ */
12
+ function prettyPrintICU(msg) {
13
+ try {
14
+ const ast = (0, icu_messageformat_parser_1.parse)(msg);
15
+ return printElement(ast, 0).trim();
16
+ }
17
+ catch (e) {
18
+ return msg.trim();
19
+ }
20
+ }
21
+ function printElement(elements, indent) {
22
+ const padding = ' '.repeat(indent);
23
+ let result = '';
24
+ elements.forEach((el) => {
25
+ if ((0, icu_messageformat_parser_1.isLiteralElement)(el)) {
26
+ result += el.value;
27
+ }
28
+ else if ((0, icu_messageformat_parser_1.isPluralElement)(el) || (0, icu_messageformat_parser_1.isSelectElement)(el)) {
29
+ const type = (0, icu_messageformat_parser_1.isPluralElement)(el) ? 'plural' : 'select';
30
+ result += `{${el.value}, ${type},\n`;
31
+ const options = Object.entries(el.options);
32
+ options.forEach(([key, opt], _) => {
33
+ result += `${padding} ${key} {${printElement(opt.value, indent + 2)}}\n`;
34
+ });
35
+ result += `${padding}}`;
36
+ }
37
+ else if ((0, icu_messageformat_parser_1.isTagElement)(el)) {
38
+ result += `<${el.value}>${printElement(el.children, indent)}</${el.value}>`;
39
+ }
40
+ else if ((0, icu_messageformat_parser_1.isPoundElement)(el)) {
41
+ result += '#';
42
+ }
43
+ else {
44
+ // For arguments like {name}
45
+ result += `{${el.value || ''}}`;
46
+ }
47
+ });
48
+ return result;
49
+ }
50
+ /**
51
+ * Canonicalizes an ICU message using the official printer and normalization.
52
+ * This ensures consistent spacing and a single-line format for hashing and compilation.
53
+ */
54
+ function canonicalizeMessage(msg) {
55
+ try {
56
+ const ast = (0, icu_messageformat_parser_1.parse)(msg);
57
+ return (0, hash_1.normalizeMessage)((0, printer_js_1.printAST)(ast));
58
+ }
59
+ catch (e) {
60
+ return (0, hash_1.normalizeMessage)(msg);
61
+ }
62
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Simple FNV-1a hash for generating stable message IDs.
3
+ * This is lightweight and works identically in Node and the Browser.
4
+ */
5
+ export declare function generateMessageId(msg: string): string;
6
+ /**
7
+ * Normalizes a message string by collapsing multiple whitespaces and newlines.
8
+ * This ensures that formatting changes in the source code do not change the message ID.
9
+ */
10
+ export declare function normalizeMessage(msg: string): string;
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateMessageId = generateMessageId;
4
+ exports.normalizeMessage = normalizeMessage;
5
+ /**
6
+ * Simple FNV-1a hash for generating stable message IDs.
7
+ * This is lightweight and works identically in Node and the Browser.
8
+ */
9
+ function generateMessageId(msg) {
10
+ const normalized = normalizeMessage(msg);
11
+ let hash = 0x811c9dc5;
12
+ for (let i = 0; i < normalized.length; i++) {
13
+ hash ^= normalized.charCodeAt(i);
14
+ // FNV-1a prime multiplication (hash * 16777619)
15
+ hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
16
+ }
17
+ // Convert to a base36 string for a compact, stable ID
18
+ return (hash >>> 0).toString(36);
19
+ }
20
+ /**
21
+ * Normalizes a message string by collapsing multiple whitespaces and newlines.
22
+ * This ensures that formatting changes in the source code do not change the message ID.
23
+ */
24
+ function normalizeMessage(msg) {
25
+ return msg.replace(/\s+/g, ' ').trim();
26
+ }
@@ -0,0 +1,2 @@
1
+ import type { I18nProvider } from '@strato-admin/ra-core';
2
+ export declare const icuI18nProvider: (getMessages: (locale: string) => any | Promise<any>, initialLocale?: string, availableLocales?: any[]) => I18nProvider;