@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.
- 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 +5 -2
- 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/dist/cli/compile.js
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import * as fs from 'fs';
|
|
3
|
-
import * as path from 'path';
|
|
4
|
-
import { globSync } from 'glob';
|
|
5
|
-
import * as gettextParser from 'gettext-parser';
|
|
6
|
-
function main() {
|
|
7
|
-
const args = process.argv.slice(2);
|
|
8
|
-
const pattern = args[0] || 'locales';
|
|
9
|
-
let files = [];
|
|
10
|
-
if (fs.existsSync(pattern) && fs.statSync(pattern).isDirectory()) {
|
|
11
|
-
// Original behavior: if a directory is passed, find .json and .po files inside it
|
|
12
|
-
files = fs.readdirSync(pattern)
|
|
13
|
-
.filter(file => (file.endsWith('.json') || file.endsWith('.po')) && !file.endsWith('.compiled.json'))
|
|
14
|
-
.map(file => path.join(pattern, file));
|
|
15
|
-
}
|
|
16
|
-
else {
|
|
17
|
-
// New behavior: support glob patterns
|
|
18
|
-
files = globSync(pattern, { absolute: true })
|
|
19
|
-
.filter(file => (file.endsWith('.json') || file.endsWith('.po')) && !file.endsWith('.compiled.json'));
|
|
20
|
-
}
|
|
21
|
-
if (files.length === 0) {
|
|
22
|
-
console.log(`No .json or .po files found matching: ${pattern}`);
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
let processedCount = 0;
|
|
26
|
-
files.forEach(filePath => {
|
|
27
|
-
const compiledFilePath = filePath.replace(/\.(json|po)$/, '.compiled.json');
|
|
28
|
-
const fileName = path.basename(filePath);
|
|
29
|
-
let translations = {};
|
|
30
|
-
try {
|
|
31
|
-
if (filePath.endsWith('.po')) {
|
|
32
|
-
const parsed = gettextParser.po.parse(fs.readFileSync(filePath));
|
|
33
|
-
Object.entries(parsed.translations).forEach(([context, entries]) => {
|
|
34
|
-
Object.entries(entries).forEach(([msgid, data]) => {
|
|
35
|
-
if (msgid === '')
|
|
36
|
-
return; // skip header
|
|
37
|
-
// Find the hash: check context (v2), then comment (v3), then msgid (v1)
|
|
38
|
-
const commentHash = data.comments?.extracted?.match(/id: (\w+)/)?.[1];
|
|
39
|
-
const hash = context || commentHash || msgid;
|
|
40
|
-
translations[hash] = {
|
|
41
|
-
defaultMessage: data.msgid || data.comments?.extracted || '',
|
|
42
|
-
translation: data.msgstr[0] || ''
|
|
43
|
-
};
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
else {
|
|
48
|
-
translations = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
catch (e) {
|
|
52
|
-
console.error(`Failed to parse translation file at ${filePath}:`, e.message);
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
const compiledMapping = {};
|
|
56
|
-
Object.entries(translations).forEach(([msgid, data]) => {
|
|
57
|
-
// If translation is empty or whitespace, fallback to defaultMessage
|
|
58
|
-
if (data.translation && data.translation.trim() !== '') {
|
|
59
|
-
compiledMapping[msgid] = data.translation;
|
|
60
|
-
}
|
|
61
|
-
else {
|
|
62
|
-
compiledMapping[msgid] = data.defaultMessage;
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
fs.writeFileSync(compiledFilePath, JSON.stringify(compiledMapping, null, 2));
|
|
66
|
-
console.log(`Compiled ${fileName} -> ${path.basename(compiledFilePath)} (${Object.keys(compiledMapping).length} messages)`);
|
|
67
|
-
processedCount++;
|
|
68
|
-
});
|
|
69
|
-
console.log(`Successfully compiled ${processedCount} files.`);
|
|
70
|
-
}
|
|
71
|
-
if (require.main === module) {
|
|
72
|
-
main();
|
|
73
|
-
}
|
package/dist/cli/extract.js
DELETED
|
@@ -1,317 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import * as fs from 'fs';
|
|
3
|
-
import * as path from 'path';
|
|
4
|
-
import { globSync } from 'glob';
|
|
5
|
-
import { parse } from '@babel/parser';
|
|
6
|
-
import traverse from '@babel/traverse';
|
|
7
|
-
import * as t from '@babel/types';
|
|
8
|
-
import * as gettextParser from 'gettext-parser';
|
|
9
|
-
import { generateMessageId } from '@strato-admin/i18n';
|
|
10
|
-
// List of Strato Admin components to extract from
|
|
11
|
-
const DEFAULT_STRATO_COMPONENTS = new Set([
|
|
12
|
-
'ArrayField',
|
|
13
|
-
'AttributeEditor',
|
|
14
|
-
'AutocompleteInput',
|
|
15
|
-
'BooleanField',
|
|
16
|
-
'BulkDeleteButton',
|
|
17
|
-
'Button',
|
|
18
|
-
'Create',
|
|
19
|
-
'CreateButton',
|
|
20
|
-
'DateField',
|
|
21
|
-
'Edit',
|
|
22
|
-
'EditButton',
|
|
23
|
-
'FormField',
|
|
24
|
-
'List',
|
|
25
|
-
'NumberField',
|
|
26
|
-
'NumberInput',
|
|
27
|
-
'ReferenceField',
|
|
28
|
-
'ReferenceInput',
|
|
29
|
-
'ReferenceManyField',
|
|
30
|
-
'Resource',
|
|
31
|
-
'ResourceSchema',
|
|
32
|
-
'SaveButton',
|
|
33
|
-
'SelectInput',
|
|
34
|
-
'Show',
|
|
35
|
-
'StatusIndicatorField.Label',
|
|
36
|
-
'Table',
|
|
37
|
-
'Table.Col',
|
|
38
|
-
'TextAreaInput',
|
|
39
|
-
'TextField',
|
|
40
|
-
'TextInput',
|
|
41
|
-
]);
|
|
42
|
-
// List of translatable props
|
|
43
|
-
const DEFAULT_TRANSLATABLE_PROPS = new Set([
|
|
44
|
-
'label',
|
|
45
|
-
'title',
|
|
46
|
-
'placeholder',
|
|
47
|
-
'emptyText',
|
|
48
|
-
'helperText',
|
|
49
|
-
'description',
|
|
50
|
-
'successMessage',
|
|
51
|
-
'errorMessage',
|
|
52
|
-
]);
|
|
53
|
-
function loadConfig(configPath) {
|
|
54
|
-
const components = new Set(DEFAULT_STRATO_COMPONENTS);
|
|
55
|
-
const translatableProps = new Set(DEFAULT_TRANSLATABLE_PROPS);
|
|
56
|
-
const resolvedConfigPath = configPath || path.join(process.cwd(), 'strato-i18n.config.json');
|
|
57
|
-
if (fs.existsSync(resolvedConfigPath)) {
|
|
58
|
-
try {
|
|
59
|
-
const config = JSON.parse(fs.readFileSync(resolvedConfigPath, 'utf8'));
|
|
60
|
-
if (config.components) {
|
|
61
|
-
config.components.forEach((c) => components.add(c));
|
|
62
|
-
}
|
|
63
|
-
if (config.translatableProps) {
|
|
64
|
-
config.translatableProps.forEach((p) => translatableProps.add(p));
|
|
65
|
-
}
|
|
66
|
-
console.log(`Loaded configuration from ${resolvedConfigPath}`);
|
|
67
|
-
}
|
|
68
|
-
catch (e) {
|
|
69
|
-
console.error(`Failed to parse configuration file at ${resolvedConfigPath}:`, e.message);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return { components, translatableProps };
|
|
73
|
-
}
|
|
74
|
-
function getJSXElementName(node) {
|
|
75
|
-
if (t.isJSXIdentifier(node)) {
|
|
76
|
-
return node.name;
|
|
77
|
-
}
|
|
78
|
-
if (t.isJSXMemberExpression(node)) {
|
|
79
|
-
return `${getJSXElementName(node.object)}.${node.property.name}`;
|
|
80
|
-
}
|
|
81
|
-
if (t.isJSXNamespacedName(node)) {
|
|
82
|
-
return `${node.namespace.name}:${node.name.name}`;
|
|
83
|
-
}
|
|
84
|
-
return '';
|
|
85
|
-
}
|
|
86
|
-
function parseArgs() {
|
|
87
|
-
const args = process.argv.slice(2);
|
|
88
|
-
let format = undefined;
|
|
89
|
-
let config = undefined;
|
|
90
|
-
const positionalArgs = [];
|
|
91
|
-
for (let i = 0; i < args.length; i++) {
|
|
92
|
-
if (args[i] === '--format' && i + 1 < args.length) {
|
|
93
|
-
format = args[i + 1];
|
|
94
|
-
i++;
|
|
95
|
-
}
|
|
96
|
-
else if (args[i].startsWith('--format=')) {
|
|
97
|
-
format = args[i].split('=')[1];
|
|
98
|
-
}
|
|
99
|
-
else if (args[i] === '--config' && i + 1 < args.length) {
|
|
100
|
-
config = args[i + 1];
|
|
101
|
-
i++;
|
|
102
|
-
}
|
|
103
|
-
else if (args[i].startsWith('--config=')) {
|
|
104
|
-
config = args[i].split('=')[1];
|
|
105
|
-
}
|
|
106
|
-
else {
|
|
107
|
-
positionalArgs.push(args[i]);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
const srcPattern = positionalArgs[0] || 'src/**/*.{ts,tsx}';
|
|
111
|
-
let outDir = 'locales';
|
|
112
|
-
let localeArgs = [];
|
|
113
|
-
if (positionalArgs.length > 1) {
|
|
114
|
-
if (positionalArgs[1].includes('*')) {
|
|
115
|
-
// First target is a glob, so we treat ALL subsequent args as targets
|
|
116
|
-
outDir = '.';
|
|
117
|
-
localeArgs = positionalArgs.slice(1);
|
|
118
|
-
}
|
|
119
|
-
else {
|
|
120
|
-
// First target is a directory
|
|
121
|
-
outDir = positionalArgs[1];
|
|
122
|
-
localeArgs = positionalArgs.slice(2);
|
|
123
|
-
if (localeArgs.length === 0) {
|
|
124
|
-
localeArgs = ['en'];
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
else {
|
|
129
|
-
localeArgs = ['en'];
|
|
130
|
-
}
|
|
131
|
-
return { srcPattern, outDir, localeArgs, format, config };
|
|
132
|
-
}
|
|
133
|
-
function main() {
|
|
134
|
-
const { srcPattern, outDir, localeArgs, format: formatArg, config: configPath } = parseArgs();
|
|
135
|
-
const { components, translatableProps } = loadConfig(configPath);
|
|
136
|
-
console.log(`Extracting messages from ${srcPattern} (using Babel)...`);
|
|
137
|
-
const files = globSync(srcPattern, { absolute: true });
|
|
138
|
-
const extractedMessages = new Set();
|
|
139
|
-
files.forEach((file) => {
|
|
140
|
-
try {
|
|
141
|
-
const content = fs.readFileSync(file, 'utf8');
|
|
142
|
-
const ast = parse(content, {
|
|
143
|
-
sourceType: 'module',
|
|
144
|
-
plugins: ['typescript', 'jsx', 'decorators-legacy'],
|
|
145
|
-
});
|
|
146
|
-
traverse(ast, {
|
|
147
|
-
JSXOpeningElement(path) {
|
|
148
|
-
const tagName = getJSXElementName(path.node.name);
|
|
149
|
-
const baseNameMatch = components.has(tagName) || Array.from(components).some((c) => tagName.startsWith(c + '.') || tagName === c);
|
|
150
|
-
if (baseNameMatch) {
|
|
151
|
-
path.node.attributes.forEach((attr) => {
|
|
152
|
-
if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name) && translatableProps.has(attr.name.name)) {
|
|
153
|
-
let textValue = null;
|
|
154
|
-
if (attr.value) {
|
|
155
|
-
if (t.isStringLiteral(attr.value)) {
|
|
156
|
-
textValue = attr.value.value;
|
|
157
|
-
}
|
|
158
|
-
else if (t.isJSXExpressionContainer(attr.value)) {
|
|
159
|
-
const expr = attr.value.expression;
|
|
160
|
-
if (t.isStringLiteral(expr) || t.isTemplateLiteral(expr)) {
|
|
161
|
-
if (t.isStringLiteral(expr)) {
|
|
162
|
-
textValue = expr.value;
|
|
163
|
-
}
|
|
164
|
-
else if (expr.quasis.length === 1) {
|
|
165
|
-
textValue = expr.quasis[0].value.raw;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
if (textValue && textValue.trim() !== '') {
|
|
171
|
-
extractedMessages.add(textValue);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
},
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
catch (e) {
|
|
180
|
-
console.error(`Failed to parse ${file}:`, e.message);
|
|
181
|
-
}
|
|
182
|
-
});
|
|
183
|
-
console.log(`Found ${extractedMessages.size} translatable strings.`);
|
|
184
|
-
const targets = [];
|
|
185
|
-
localeArgs.forEach((arg) => {
|
|
186
|
-
if (arg.includes('*')) {
|
|
187
|
-
const matchedFiles = globSync(arg, { absolute: true });
|
|
188
|
-
matchedFiles.forEach((file) => {
|
|
189
|
-
const ext = path.extname(file).slice(1);
|
|
190
|
-
const format = formatArg || (ext === 'po' ? 'po' : 'json');
|
|
191
|
-
// Guess locale from path: locales/en.po -> en, locales/en/messages.po -> en
|
|
192
|
-
let locale = path.basename(file, '.' + ext);
|
|
193
|
-
if (locale === 'messages' || locale === 'translations' || locale === 'LC_MESSAGES') {
|
|
194
|
-
const parts = file.split(path.sep);
|
|
195
|
-
locale = parts[parts.length - 2];
|
|
196
|
-
}
|
|
197
|
-
targets.push({ outFile: file, locale, format });
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
else {
|
|
201
|
-
let outFile;
|
|
202
|
-
let locale;
|
|
203
|
-
let format;
|
|
204
|
-
if (arg.includes('.') || arg.includes('/') || arg.includes('\\')) {
|
|
205
|
-
outFile = arg;
|
|
206
|
-
const ext = path.extname(arg).slice(1);
|
|
207
|
-
format = formatArg || (ext === 'po' ? 'po' : 'json');
|
|
208
|
-
locale = path.basename(arg, '.' + ext);
|
|
209
|
-
if (locale === 'messages' || locale === 'translations' || locale === 'LC_MESSAGES') {
|
|
210
|
-
const parts = path.resolve(arg).split(path.sep);
|
|
211
|
-
locale = parts[parts.length - 2];
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
else {
|
|
215
|
-
format = formatArg || 'json';
|
|
216
|
-
const extension = format === 'po' ? 'po' : 'json';
|
|
217
|
-
outFile = path.join(outDir, `${arg}.${extension}`);
|
|
218
|
-
locale = arg;
|
|
219
|
-
}
|
|
220
|
-
targets.push({ outFile, locale, format });
|
|
221
|
-
}
|
|
222
|
-
});
|
|
223
|
-
if (targets.length === 0) {
|
|
224
|
-
console.error('No target files found or specified.');
|
|
225
|
-
process.exit(1);
|
|
226
|
-
}
|
|
227
|
-
targets.forEach(({ outFile, locale, format }) => {
|
|
228
|
-
let existingTranslations = {};
|
|
229
|
-
if (fs.existsSync(outFile)) {
|
|
230
|
-
try {
|
|
231
|
-
if (format === 'po') {
|
|
232
|
-
const fileContent = fs.readFileSync(outFile);
|
|
233
|
-
const parsedPo = gettextParser.po.parse(fileContent);
|
|
234
|
-
Object.entries(parsedPo.translations).forEach(([context, entries]) => {
|
|
235
|
-
Object.entries(entries).forEach(([msgid, data]) => {
|
|
236
|
-
if (msgid === '')
|
|
237
|
-
return;
|
|
238
|
-
// Find the hash: check context (v2), then comment (v3), then msgid (v1)
|
|
239
|
-
const commentHash = data.comments?.extracted?.match(/id: (\w+)/)?.[1];
|
|
240
|
-
const hash = context || commentHash || msgid;
|
|
241
|
-
existingTranslations[hash] = {
|
|
242
|
-
defaultMessage: data.msgid || data.comments?.extracted || '',
|
|
243
|
-
translation: data.msgstr[0] || '',
|
|
244
|
-
};
|
|
245
|
-
});
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
else {
|
|
249
|
-
existingTranslations = JSON.parse(fs.readFileSync(outFile, 'utf8'));
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
catch (e) {
|
|
253
|
-
console.error(`Failed to parse existing translation file at ${outFile}:`, e.message);
|
|
254
|
-
return; // Skip this file
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
else {
|
|
258
|
-
const parentDir = path.dirname(outFile);
|
|
259
|
-
if (!fs.existsSync(parentDir)) {
|
|
260
|
-
fs.mkdirSync(parentDir, { recursive: true });
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
const updatedTranslations = {};
|
|
264
|
-
let addedCount = 0;
|
|
265
|
-
extractedMessages.forEach((msg) => {
|
|
266
|
-
const msgid = generateMessageId(msg);
|
|
267
|
-
if (existingTranslations[msgid]) {
|
|
268
|
-
updatedTranslations[msgid] = { ...existingTranslations[msgid] };
|
|
269
|
-
updatedTranslations[msgid].defaultMessage = msg;
|
|
270
|
-
}
|
|
271
|
-
else {
|
|
272
|
-
updatedTranslations[msgid] = {
|
|
273
|
-
defaultMessage: msg,
|
|
274
|
-
translation: '',
|
|
275
|
-
};
|
|
276
|
-
addedCount++;
|
|
277
|
-
}
|
|
278
|
-
});
|
|
279
|
-
Object.keys(existingTranslations).forEach((key) => {
|
|
280
|
-
if (!updatedTranslations[key]) {
|
|
281
|
-
updatedTranslations[key] = existingTranslations[key];
|
|
282
|
-
}
|
|
283
|
-
});
|
|
284
|
-
if (format === 'po') {
|
|
285
|
-
const poData = {
|
|
286
|
-
charset: 'utf-8',
|
|
287
|
-
headers: {
|
|
288
|
-
'content-type': 'text/plain; charset=utf-8',
|
|
289
|
-
language: locale,
|
|
290
|
-
},
|
|
291
|
-
translations: {
|
|
292
|
-
'': {
|
|
293
|
-
'': { msgid: '', msgstr: [''] }, // Header
|
|
294
|
-
},
|
|
295
|
-
},
|
|
296
|
-
};
|
|
297
|
-
Object.entries(updatedTranslations).forEach(([hash, data]) => {
|
|
298
|
-
poData.translations[''][data.defaultMessage] = {
|
|
299
|
-
msgid: data.defaultMessage,
|
|
300
|
-
msgstr: [data.translation],
|
|
301
|
-
comments: {
|
|
302
|
-
extracted: `id: ${hash}`,
|
|
303
|
-
},
|
|
304
|
-
};
|
|
305
|
-
});
|
|
306
|
-
const compiledPo = gettextParser.po.compile(poData);
|
|
307
|
-
fs.writeFileSync(outFile, compiledPo);
|
|
308
|
-
}
|
|
309
|
-
else {
|
|
310
|
-
fs.writeFileSync(outFile, JSON.stringify(updatedTranslations, null, 2));
|
|
311
|
-
}
|
|
312
|
-
console.log(`Updated ${outFile} (${locale}): Added ${addedCount} new messages.`);
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
if (require.main === module) {
|
|
316
|
-
main();
|
|
317
|
-
}
|
|
File without changes
|
|
File without changes
|