@streamplace/components 0.8.16 → 0.8.17
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,21 @@
|
|
|
1
|
+
import { defineConfig } from "i18next-cli";
|
|
2
|
+
// i18next-cli configuration
|
|
3
|
+
// We use i18next-fluent, so variant handling (platform, count, etc.)
|
|
4
|
+
// is done using Fluent's select expressions within messages.
|
|
5
|
+
// Example: shortcut-key-search = { $platform -> [mac] Cmd+K [windows] Ctrl+K *[other] Ctrl+K }
|
|
6
|
+
|
|
7
|
+
export default defineConfig({
|
|
8
|
+
locales: ["en-US", "es-ES", "fr-FR", "pt-BR", "zh-Hant"],
|
|
9
|
+
extract: {
|
|
10
|
+
input: [
|
|
11
|
+
"src/**/*.{js,jsx,ts,tsx}",
|
|
12
|
+
"../app/src/**/*.{js,jsx,ts,tsx}",
|
|
13
|
+
"../app/components/**/*.{js,jsx,ts,tsx}",
|
|
14
|
+
],
|
|
15
|
+
contextSeparator: "|",
|
|
16
|
+
pluralSeparator: "/",
|
|
17
|
+
output: "public/locales/{{language}}/{{namespace}}.json",
|
|
18
|
+
primaryLanguage: "en-US",
|
|
19
|
+
defaultNS: "common",
|
|
20
|
+
},
|
|
21
|
+
});
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@streamplace/components",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.17",
|
|
4
4
|
"description": "Streamplace React (Native) Components",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "src/index.tsx",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@fluent/syntax": "^0.19.0",
|
|
21
21
|
"@types/sdp-transform": "^2.15.0",
|
|
22
|
+
"i18next-cli": "^1.32.0",
|
|
22
23
|
"nodemon": "^3.1.10",
|
|
23
24
|
"tsup": "^8.5.0"
|
|
24
25
|
},
|
|
@@ -74,8 +75,8 @@
|
|
|
74
75
|
"start": "tsc --watch --preserveWatchOutput",
|
|
75
76
|
"prepare": "node scripts/compile-translations.js && tsc",
|
|
76
77
|
"i18n:compile": "node scripts/compile-translations.js",
|
|
77
|
-
"i18n:watch": "nodemon scripts/compile-translations.js
|
|
78
|
-
"i18n:extract": "node scripts/
|
|
78
|
+
"i18n:watch": "nodemon --watch 'locales/**/*.ftl' --exec 'node scripts/compile-translations.js'",
|
|
79
|
+
"i18n:extract": "i18next-cli extract && node scripts/migrate-i18n.js"
|
|
79
80
|
},
|
|
80
|
-
"gitHead": "
|
|
81
|
+
"gitHead": "80f92d685c83c1065486e447b05a365d0cfc4d01"
|
|
81
82
|
}
|
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* i18n
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* i18n migration script
|
|
5
|
+
* Migrates extracted JSON keys to .ftl files for translation
|
|
6
|
+
*
|
|
7
|
+
* This script expects that i18next-cli has already extracted keys to JSON files.
|
|
8
|
+
* It reads those JSON files, compares them to existing .ftl files, and adds any
|
|
9
|
+
* new keys to the .ftl files.
|
|
10
|
+
*
|
|
11
|
+
* For keys with i18next context/plural suffixes (e.g., key_male, key_female, key_one, key_other),
|
|
12
|
+
* it will convert them into Fluent select expressions.
|
|
8
13
|
*
|
|
9
14
|
* Usage:
|
|
10
|
-
* node
|
|
11
|
-
* node
|
|
12
|
-
* node
|
|
15
|
+
* node migrate-i18n.js # Report new keys
|
|
16
|
+
* node migrate-i18n.js --add-to=common # Add new keys to common.ftl
|
|
17
|
+
* node migrate-i18n.js --add-to=settings # Add new keys to settings.ftl
|
|
13
18
|
*/
|
|
14
19
|
|
|
15
|
-
const { execSync } = require("child_process");
|
|
16
20
|
const fs = require("fs");
|
|
17
21
|
const path = require("path");
|
|
18
22
|
|
|
@@ -24,7 +28,6 @@ const addToNamespace = args
|
|
|
24
28
|
|
|
25
29
|
// Paths
|
|
26
30
|
const COMPONENTS_ROOT = path.join(__dirname, "..");
|
|
27
|
-
const APP_ROOT = path.join(__dirname, "..", "..", "app");
|
|
28
31
|
const MANIFEST_PATH = path.join(COMPONENTS_ROOT, "locales/manifest.json");
|
|
29
32
|
const LOCALES_FTL_DIR = path.join(COMPONENTS_ROOT, "locales");
|
|
30
33
|
const LOCALES_JSON_DIR = path.join(COMPONENTS_ROOT, "public/locales");
|
|
@@ -32,72 +35,141 @@ const LOCALES_JSON_DIR = path.join(COMPONENTS_ROOT, "public/locales");
|
|
|
32
35
|
// Load manifest
|
|
33
36
|
const manifest = JSON.parse(fs.readFileSync(MANIFEST_PATH, "utf8"));
|
|
34
37
|
|
|
35
|
-
//
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
indentation: 2,
|
|
42
|
-
keepRemoved: true,
|
|
43
|
-
keySeparator: false,
|
|
44
|
-
namespaceSeparator: false,
|
|
45
|
-
|
|
46
|
-
lexers: {
|
|
47
|
-
js: ["JavascriptLexer"],
|
|
48
|
-
ts: ["JavascriptLexer"],
|
|
49
|
-
jsx: ["JsxLexer"],
|
|
50
|
-
tsx: ["JsxLexer"],
|
|
51
|
-
html: false,
|
|
52
|
-
htm: false,
|
|
53
|
-
handlebars: false,
|
|
54
|
-
hbs: false,
|
|
55
|
-
},
|
|
56
|
-
|
|
57
|
-
locales: manifest.supportedLocales,
|
|
58
|
-
output: path.join(LOCALES_JSON_DIR, "$LOCALE/$NAMESPACE.json"),
|
|
59
|
-
input: [
|
|
60
|
-
path.join(COMPONENTS_ROOT, "src/**/*.{js,jsx,ts,tsx}"),
|
|
61
|
-
path.join(APP_ROOT, "src/**/*.{js,jsx,ts,tsx}"),
|
|
62
|
-
path.join(APP_ROOT, "components/**/*.{js,jsx,ts,tsx}"),
|
|
63
|
-
"!**/node_modules/**",
|
|
64
|
-
"!**/dist/**",
|
|
65
|
-
"!**/*.test.{js,jsx,ts,tsx}",
|
|
66
|
-
"!**/*.spec.{js,jsx,ts,tsx}",
|
|
67
|
-
],
|
|
68
|
-
|
|
69
|
-
verbose: true,
|
|
70
|
-
sort: true,
|
|
71
|
-
failOnWarnings: false,
|
|
72
|
-
failOnUpdate: false,
|
|
73
|
-
};
|
|
38
|
+
// Plural forms that i18next uses
|
|
39
|
+
const PLURAL_FORMS = ["zero", "one", "two", "few", "many", "other"];
|
|
40
|
+
|
|
41
|
+
// Separators used by i18next-cli (configured in i18next.config.js)
|
|
42
|
+
const CONTEXT_SEPARATOR = "|";
|
|
43
|
+
const PLURAL_SEPARATOR = "/";
|
|
74
44
|
|
|
75
45
|
/**
|
|
76
|
-
*
|
|
46
|
+
* Group keys by base name, detecting context and plural variants
|
|
47
|
+
* Returns { baseKey: { base: true, variants: { context: Set, plurals: Set } } }
|
|
77
48
|
*/
|
|
78
|
-
function
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
49
|
+
function groupKeysByBase(keys) {
|
|
50
|
+
const groups = {};
|
|
51
|
+
|
|
52
|
+
for (const key of keys) {
|
|
53
|
+
if (!key.includes(CONTEXT_SEPARATOR) && !key.includes(PLURAL_SEPARATOR)) {
|
|
54
|
+
// Simple key with no variants
|
|
55
|
+
if (!groups[key]) {
|
|
56
|
+
groups[key] = {
|
|
57
|
+
base: true,
|
|
58
|
+
variants: { contexts: new Set(), plurals: new Set() },
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
groups[key].base = true;
|
|
62
|
+
} else {
|
|
63
|
+
// Key with variants
|
|
64
|
+
// Format: base|context/plural or base/plural or base|context
|
|
65
|
+
let baseKey = key;
|
|
66
|
+
const detectedContexts = new Set();
|
|
67
|
+
const detectedPlurals = new Set();
|
|
68
|
+
|
|
69
|
+
// Split by context separator first
|
|
70
|
+
if (key.includes(CONTEXT_SEPARATOR)) {
|
|
71
|
+
const contextParts = key.split(CONTEXT_SEPARATOR);
|
|
72
|
+
baseKey = contextParts[0];
|
|
73
|
+
|
|
74
|
+
// The remaining part might have plurals
|
|
75
|
+
const contextAndPlural = contextParts[1];
|
|
76
|
+
|
|
77
|
+
if (contextAndPlural.includes(PLURAL_SEPARATOR)) {
|
|
78
|
+
const pluralParts = contextAndPlural.split(PLURAL_SEPARATOR);
|
|
79
|
+
detectedContexts.add(pluralParts[0]);
|
|
80
|
+
pluralParts.slice(1).forEach((p) => {
|
|
81
|
+
if (PLURAL_FORMS.includes(p)) {
|
|
82
|
+
detectedPlurals.add(p);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
} else {
|
|
86
|
+
detectedContexts.add(contextAndPlural);
|
|
87
|
+
}
|
|
88
|
+
} else if (key.includes(PLURAL_SEPARATOR)) {
|
|
89
|
+
// No context, just plural
|
|
90
|
+
const pluralParts = key.split(PLURAL_SEPARATOR);
|
|
91
|
+
baseKey = pluralParts[0];
|
|
92
|
+
pluralParts.slice(1).forEach((p) => {
|
|
93
|
+
if (PLURAL_FORMS.includes(p)) {
|
|
94
|
+
detectedPlurals.add(p);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
85
98
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
99
|
+
if (!groups[baseKey]) {
|
|
100
|
+
groups[baseKey] = {
|
|
101
|
+
base: false,
|
|
102
|
+
variants: { contexts: new Set(), plurals: new Set() },
|
|
103
|
+
};
|
|
104
|
+
}
|
|
90
105
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
} catch (error) {
|
|
94
|
-
console.error("❌ Error extracting i18n keys:", error.message);
|
|
95
|
-
return false;
|
|
96
|
-
} finally {
|
|
97
|
-
if (fs.existsSync(configPath)) {
|
|
98
|
-
fs.unlinkSync(configPath);
|
|
106
|
+
detectedContexts.forEach((c) => groups[baseKey].variants.contexts.add(c));
|
|
107
|
+
detectedPlurals.forEach((p) => groups[baseKey].variants.plurals.add(p));
|
|
99
108
|
}
|
|
100
109
|
}
|
|
110
|
+
|
|
111
|
+
return groups;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Convert a group of keys into Fluent format
|
|
116
|
+
*/
|
|
117
|
+
function convertToFluentFormat(baseKey, group) {
|
|
118
|
+
const hasContexts = group.variants.contexts.size > 0;
|
|
119
|
+
const hasPlurals = group.variants.plurals.size > 0;
|
|
120
|
+
|
|
121
|
+
if (!hasContexts && !hasPlurals) {
|
|
122
|
+
// Simple key
|
|
123
|
+
return `${baseKey} = ${baseKey}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Build Fluent select expression
|
|
127
|
+
let selector = "";
|
|
128
|
+
let variants = [];
|
|
129
|
+
|
|
130
|
+
if (hasContexts && hasPlurals) {
|
|
131
|
+
// Both context and plural - outer selector is context, inner is plural
|
|
132
|
+
selector = "$context";
|
|
133
|
+
const contextsList = Array.from(group.variants.contexts).sort();
|
|
134
|
+
const pluralsList = Array.from(group.variants.plurals).sort();
|
|
135
|
+
|
|
136
|
+
contextsList.forEach((context, idx) => {
|
|
137
|
+
const isDefault = idx === contextsList.length - 1;
|
|
138
|
+
const prefix = isDefault ? "*" : " ";
|
|
139
|
+
|
|
140
|
+
// Build inner plural select
|
|
141
|
+
const pluralVariants = pluralsList
|
|
142
|
+
.map((p) => {
|
|
143
|
+
const pluralPrefix = p === "other" ? "*" : "";
|
|
144
|
+
return `${pluralPrefix}[${p}] ${baseKey}`;
|
|
145
|
+
})
|
|
146
|
+
.join(" ");
|
|
147
|
+
|
|
148
|
+
variants.push(
|
|
149
|
+
`\n ${prefix}[${context}] { $count -> ${pluralVariants} }`,
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
} else if (hasContexts) {
|
|
153
|
+
// Only context
|
|
154
|
+
selector = "$context";
|
|
155
|
+
const contextsList = Array.from(group.variants.contexts).sort();
|
|
156
|
+
contextsList.forEach((context, idx) => {
|
|
157
|
+
const isDefault = idx === contextsList.length - 1;
|
|
158
|
+
const prefix = isDefault ? "*" : " ";
|
|
159
|
+
variants.push(`\n ${prefix}[${context}] ${baseKey}`);
|
|
160
|
+
});
|
|
161
|
+
} else if (hasPlurals) {
|
|
162
|
+
// Only plural
|
|
163
|
+
selector = "$count";
|
|
164
|
+
const pluralsList = Array.from(group.variants.plurals).sort();
|
|
165
|
+
pluralsList.forEach((plural) => {
|
|
166
|
+
const isDefault = plural === "other";
|
|
167
|
+
const prefix = isDefault ? "*" : " ";
|
|
168
|
+
variants.push(`\n ${prefix}[${plural}] ${baseKey}`);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return `# TODO: Convert to proper Fluent select expression\n${baseKey} = { ${selector} ->${variants.join("")}\n}`;
|
|
101
173
|
}
|
|
102
174
|
|
|
103
175
|
/**
|
|
@@ -151,7 +223,7 @@ function getNamespaces(localeJsonDir) {
|
|
|
151
223
|
}
|
|
152
224
|
|
|
153
225
|
/**
|
|
154
|
-
* Add new keys to a .ftl file
|
|
226
|
+
* Add new keys to a .ftl file, converting context/plural keys to Fluent format
|
|
155
227
|
*/
|
|
156
228
|
function addKeysToFtlFile(localeDir, namespace, newKeys, locale) {
|
|
157
229
|
const targetFile = path.join(localeDir, `${namespace}.ftl`);
|
|
@@ -169,6 +241,15 @@ function addKeysToFtlFile(localeDir, namespace, newKeys, locale) {
|
|
|
169
241
|
fs.writeFileSync(targetFile, header);
|
|
170
242
|
}
|
|
171
243
|
|
|
244
|
+
// Group keys by base to detect context/plural variants
|
|
245
|
+
const keyGroups = groupKeysByBase(newKeys);
|
|
246
|
+
|
|
247
|
+
// Build content
|
|
248
|
+
const fluentEntries = [];
|
|
249
|
+
for (const [baseKey, group] of Object.entries(keyGroups)) {
|
|
250
|
+
fluentEntries.push(convertToFluentFormat(baseKey, group));
|
|
251
|
+
}
|
|
252
|
+
|
|
172
253
|
// Append new keys
|
|
173
254
|
let content = fs.readFileSync(targetFile, "utf8");
|
|
174
255
|
|
|
@@ -177,7 +258,7 @@ function addKeysToFtlFile(localeDir, namespace, newKeys, locale) {
|
|
|
177
258
|
}
|
|
178
259
|
|
|
179
260
|
content += "\n# Newly extracted keys\n";
|
|
180
|
-
content +=
|
|
261
|
+
content += fluentEntries.join("\n\n") + "\n";
|
|
181
262
|
|
|
182
263
|
fs.writeFileSync(targetFile, content);
|
|
183
264
|
|
|
@@ -188,7 +269,7 @@ function addKeysToFtlFile(localeDir, namespace, newKeys, locale) {
|
|
|
188
269
|
* Migrate extracted JSON keys to .ftl files
|
|
189
270
|
*/
|
|
190
271
|
function migrateKeysToFtl() {
|
|
191
|
-
console.log("
|
|
272
|
+
console.log("🔄 Analyzing extracted keys...");
|
|
192
273
|
|
|
193
274
|
const newKeysByLocaleAndNamespace = {}; // locale -> namespace -> [keys]
|
|
194
275
|
|
|
@@ -297,7 +378,9 @@ function migrateKeysToFtl() {
|
|
|
297
378
|
|
|
298
379
|
console.log("\n💡 Next steps:");
|
|
299
380
|
console.log(" 1. Review the new keys in your .ftl files");
|
|
300
|
-
console.log(
|
|
381
|
+
console.log(
|
|
382
|
+
" 2. Convert TODO placeholders to proper Fluent translations",
|
|
383
|
+
);
|
|
301
384
|
console.log(" 3. Run `pnpm i18n:compile` to update compiled JSON files");
|
|
302
385
|
} else {
|
|
303
386
|
// Just report
|
|
@@ -318,19 +401,13 @@ function migrateKeysToFtl() {
|
|
|
318
401
|
);
|
|
319
402
|
console.log("\nTo add these keys to a specific namespace file, run:");
|
|
320
403
|
Array.from(namespaceSet).forEach((ns) => {
|
|
321
|
-
console.log(` node
|
|
404
|
+
console.log(` node migrate-i18n.js --add-to=${ns}`);
|
|
322
405
|
});
|
|
323
406
|
}
|
|
324
407
|
}
|
|
325
408
|
|
|
326
409
|
function main() {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
if (success) {
|
|
330
|
-
migrateKeysToFtl();
|
|
331
|
-
} else {
|
|
332
|
-
process.exit(1);
|
|
333
|
-
}
|
|
410
|
+
migrateKeysToFtl();
|
|
334
411
|
}
|
|
335
412
|
|
|
336
413
|
main();
|