@red-hat-developer-hub/translations-cli 0.0.1
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/CHANGELOG.md +7 -0
- package/bin/translations-cli +34 -0
- package/dist/commands/clean.cjs.js +130 -0
- package/dist/commands/deploy.cjs.js +53 -0
- package/dist/commands/download.cjs.js +304 -0
- package/dist/commands/generate.cjs.js +1294 -0
- package/dist/commands/index.cjs.js +146 -0
- package/dist/commands/init.cjs.js +115 -0
- package/dist/commands/list.cjs.js +178 -0
- package/dist/commands/setupMemsource.cjs.js +338 -0
- package/dist/commands/status.cjs.js +40 -0
- package/dist/commands/sync.cjs.js +226 -0
- package/dist/commands/upload.cjs.js +506 -0
- package/dist/index.cjs.js +30 -0
- package/dist/lib/errors.cjs.js +36 -0
- package/dist/lib/i18n/analyzeStatus.cjs.js +79 -0
- package/dist/lib/i18n/config.cjs.js +256 -0
- package/dist/lib/i18n/deployTranslations.cjs.js +1213 -0
- package/dist/lib/i18n/extractKeys.cjs.js +418 -0
- package/dist/lib/i18n/formatReport.cjs.js +138 -0
- package/dist/lib/i18n/generateFiles.cjs.js +94 -0
- package/dist/lib/i18n/loadFile.cjs.js +93 -0
- package/dist/lib/i18n/mergeFiles.cjs.js +126 -0
- package/dist/lib/i18n/uploadCache.cjs.js +83 -0
- package/dist/lib/i18n/validateFile.cjs.js +189 -0
- package/dist/lib/paths.cjs.js +51 -0
- package/dist/lib/utils/exec.cjs.js +41 -0
- package/dist/lib/utils/translationUtils.cjs.js +33 -0
- package/dist/lib/version.cjs.js +38 -0
- package/package.json +65 -0
|
@@ -0,0 +1,1213 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var fs = require('fs-extra');
|
|
4
|
+
var path = require('node:path');
|
|
5
|
+
var os = require('node:os');
|
|
6
|
+
var chalk = require('chalk');
|
|
7
|
+
|
|
8
|
+
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
9
|
+
|
|
10
|
+
var fs__default = /*#__PURE__*/_interopDefaultCompat(fs);
|
|
11
|
+
var path__default = /*#__PURE__*/_interopDefaultCompat(path);
|
|
12
|
+
var os__default = /*#__PURE__*/_interopDefaultCompat(os);
|
|
13
|
+
var chalk__default = /*#__PURE__*/_interopDefaultCompat(chalk);
|
|
14
|
+
|
|
15
|
+
function findRefImportPath(translationDir) {
|
|
16
|
+
if (fs__default.default.existsSync(path__default.default.join(translationDir, "ref.ts"))) {
|
|
17
|
+
return "./ref";
|
|
18
|
+
}
|
|
19
|
+
if (fs__default.default.existsSync(path__default.default.join(translationDir, "translations.ts"))) {
|
|
20
|
+
return "./translations";
|
|
21
|
+
}
|
|
22
|
+
return "./ref";
|
|
23
|
+
}
|
|
24
|
+
function extractRefInfo(filePath) {
|
|
25
|
+
try {
|
|
26
|
+
const content = fs__default.default.readFileSync(filePath, "utf-8");
|
|
27
|
+
const refImportMatch = content.match(
|
|
28
|
+
/import\s*{\s*(\w+TranslationRef)\s*}\s*from\s*['"]([^'"]+)['"]/
|
|
29
|
+
);
|
|
30
|
+
if (!refImportMatch) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const refImportName = refImportMatch[1];
|
|
34
|
+
let refImportPath = refImportMatch[2];
|
|
35
|
+
if (refImportPath.startsWith("./")) {
|
|
36
|
+
const translationDir = path__default.default.dirname(filePath);
|
|
37
|
+
const expectedFile = path__default.default.join(
|
|
38
|
+
translationDir,
|
|
39
|
+
`${refImportPath.replace("./", "")}.ts`
|
|
40
|
+
);
|
|
41
|
+
if (!fs__default.default.existsSync(expectedFile)) {
|
|
42
|
+
refImportPath = findRefImportPath(translationDir);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
let variableMatch = content.match(
|
|
46
|
+
/const\s+(\w+Translation(?:It|Ja|De|Fr|Es))\s*=/
|
|
47
|
+
);
|
|
48
|
+
if (!variableMatch) {
|
|
49
|
+
variableMatch = content.match(
|
|
50
|
+
/const\s+([a-z]+)\s*=\s*createTranslationMessages/
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
if (!variableMatch) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
const variableName = variableMatch[1];
|
|
57
|
+
return { refImportName, refImportPath, variableName };
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function getPluginPackageImport(pluginName) {
|
|
63
|
+
const pluginPackageMap = {
|
|
64
|
+
search: "@backstage/plugin-search/alpha",
|
|
65
|
+
"user-settings": "@backstage/plugin-user-settings/alpha",
|
|
66
|
+
scaffolder: "@backstage/plugin-scaffolder/alpha",
|
|
67
|
+
"core-components": "@backstage/core-components/alpha",
|
|
68
|
+
"catalog-import": "@backstage/plugin-catalog-import/alpha",
|
|
69
|
+
catalog: "@backstage/plugin-catalog-react/alpha"
|
|
70
|
+
};
|
|
71
|
+
return pluginPackageMap[pluginName] || null;
|
|
72
|
+
}
|
|
73
|
+
function sanitizePluginName(pluginName) {
|
|
74
|
+
if (pluginName.includes(".")) {
|
|
75
|
+
const parts = pluginName.split(".");
|
|
76
|
+
return parts[parts.length - 1];
|
|
77
|
+
}
|
|
78
|
+
return pluginName.split("-").map(
|
|
79
|
+
(word, i) => i === 0 ? word : word.charAt(0).toUpperCase() + word.slice(1)
|
|
80
|
+
).join("");
|
|
81
|
+
}
|
|
82
|
+
function inferRefInfo(pluginName, lang, repoType, translationDir) {
|
|
83
|
+
const sanitized = sanitizePluginName(pluginName);
|
|
84
|
+
const refImportName = `${sanitized}TranslationRef`;
|
|
85
|
+
const langCapitalized = lang.charAt(0).toUpperCase() + lang.slice(1);
|
|
86
|
+
const variableName = `${sanitized}Translation${langCapitalized}`;
|
|
87
|
+
let refImportPath = "./ref";
|
|
88
|
+
if (repoType === "rhdh") {
|
|
89
|
+
const packageImport = getPluginPackageImport(pluginName);
|
|
90
|
+
if (packageImport) {
|
|
91
|
+
refImportPath = packageImport;
|
|
92
|
+
}
|
|
93
|
+
} else if (translationDir) {
|
|
94
|
+
refImportPath = findRefImportPath(translationDir);
|
|
95
|
+
}
|
|
96
|
+
return { refImportName, refImportPath, variableName };
|
|
97
|
+
}
|
|
98
|
+
function detectRepoType(repoRoot) {
|
|
99
|
+
const workspacesDir = path__default.default.join(repoRoot, "workspaces");
|
|
100
|
+
const packagesDir = path__default.default.join(repoRoot, "packages");
|
|
101
|
+
const pluginsDir = path__default.default.join(repoRoot, "plugins");
|
|
102
|
+
if (fs__default.default.existsSync(workspacesDir)) {
|
|
103
|
+
const repoName = path__default.default.basename(repoRoot);
|
|
104
|
+
if (repoName === "community-plugins") {
|
|
105
|
+
return "community-plugins";
|
|
106
|
+
}
|
|
107
|
+
return "rhdh-plugins";
|
|
108
|
+
}
|
|
109
|
+
if (fs__default.default.existsSync(packagesDir)) {
|
|
110
|
+
const appDir = path__default.default.join(packagesDir, "app");
|
|
111
|
+
if (fs__default.default.existsSync(appDir)) {
|
|
112
|
+
return "rhdh";
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (fs__default.default.existsSync(pluginsDir)) {
|
|
116
|
+
const repoName = path__default.default.basename(repoRoot);
|
|
117
|
+
if (repoName === "backstage") {
|
|
118
|
+
return "backstage";
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return "unknown";
|
|
122
|
+
}
|
|
123
|
+
function findPluginInWorkspaces(pluginName, repoRoot) {
|
|
124
|
+
const workspacesDir = path__default.default.join(repoRoot, "workspaces");
|
|
125
|
+
if (!fs__default.default.existsSync(workspacesDir)) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
const cleanPluginName = pluginName.replace(/^plugin\./, "");
|
|
129
|
+
const workspaceDirs = fs__default.default.readdirSync(workspacesDir);
|
|
130
|
+
for (const workspace of workspaceDirs) {
|
|
131
|
+
const pluginsDir = path__default.default.join(
|
|
132
|
+
workspacesDir,
|
|
133
|
+
workspace,
|
|
134
|
+
"plugins",
|
|
135
|
+
cleanPluginName,
|
|
136
|
+
"src",
|
|
137
|
+
"translations"
|
|
138
|
+
);
|
|
139
|
+
if (fs__default.default.existsSync(pluginsDir)) {
|
|
140
|
+
return pluginsDir;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
function findPluginInRhdh(pluginName, repoRoot) {
|
|
146
|
+
const searchPaths = [
|
|
147
|
+
// Standard location: packages/app/src/translations/{plugin}/
|
|
148
|
+
path__default.default.join(repoRoot, "packages", "app", "src", "translations", pluginName),
|
|
149
|
+
// Alternative location: packages/app/src/components/{plugin}/translations/
|
|
150
|
+
path__default.default.join(
|
|
151
|
+
repoRoot,
|
|
152
|
+
"packages",
|
|
153
|
+
"app",
|
|
154
|
+
"src",
|
|
155
|
+
"components",
|
|
156
|
+
pluginName,
|
|
157
|
+
"translations"
|
|
158
|
+
)
|
|
159
|
+
];
|
|
160
|
+
for (const searchPath of searchPaths) {
|
|
161
|
+
if (fs__default.default.existsSync(searchPath)) {
|
|
162
|
+
const refFile = path__default.default.join(searchPath, "ref.ts");
|
|
163
|
+
const translationsFile = path__default.default.join(searchPath, "translations.ts");
|
|
164
|
+
if (fs__default.default.existsSync(refFile) || fs__default.default.existsSync(translationsFile)) {
|
|
165
|
+
return searchPath;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const appSrcDir = path__default.default.join(repoRoot, "packages", "app", "src");
|
|
170
|
+
if (fs__default.default.existsSync(appSrcDir)) {
|
|
171
|
+
const findTranslationDir = (dir, depth = 0) => {
|
|
172
|
+
if (depth > 3) return null;
|
|
173
|
+
try {
|
|
174
|
+
const entries = fs__default.default.readdirSync(dir, { withFileTypes: true });
|
|
175
|
+
for (const entry of entries) {
|
|
176
|
+
if (entry.isDirectory()) {
|
|
177
|
+
const subDir = path__default.default.join(dir, entry.name);
|
|
178
|
+
const langFiles = fs__default.default.readdirSync(subDir).filter(
|
|
179
|
+
(f) => /^(fr|it|ja|de|es|ko|zh|pt|ru|ar|hi|nl|pl|sv)\.ts$/.test(f)
|
|
180
|
+
);
|
|
181
|
+
if (langFiles.length > 0) {
|
|
182
|
+
const refFile = path__default.default.join(subDir, "ref.ts");
|
|
183
|
+
const translationsFile = path__default.default.join(subDir, "translations.ts");
|
|
184
|
+
if (fs__default.default.existsSync(refFile) || fs__default.default.existsSync(translationsFile)) {
|
|
185
|
+
const sampleLangFile = path__default.default.join(subDir, langFiles[0]);
|
|
186
|
+
try {
|
|
187
|
+
const content = fs__default.default.readFileSync(sampleLangFile, "utf-8");
|
|
188
|
+
const pluginRefPattern = new RegExp(
|
|
189
|
+
`(?:${pluginName}|${pluginName.replace(
|
|
190
|
+
/-/g,
|
|
191
|
+
""
|
|
192
|
+
)})TranslationRef`,
|
|
193
|
+
"i"
|
|
194
|
+
);
|
|
195
|
+
if (pluginRefPattern.test(content)) {
|
|
196
|
+
return subDir;
|
|
197
|
+
}
|
|
198
|
+
} catch {
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const found = findTranslationDir(subDir, depth + 1);
|
|
203
|
+
if (found) return found;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
} catch {
|
|
207
|
+
}
|
|
208
|
+
return null;
|
|
209
|
+
};
|
|
210
|
+
const foundDir = findTranslationDir(appSrcDir);
|
|
211
|
+
if (foundDir) {
|
|
212
|
+
return foundDir;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const standardPluginDir = path__default.default.join(
|
|
216
|
+
repoRoot,
|
|
217
|
+
"packages",
|
|
218
|
+
"app",
|
|
219
|
+
"src",
|
|
220
|
+
"translations",
|
|
221
|
+
pluginName
|
|
222
|
+
);
|
|
223
|
+
const translationsDir = path__default.default.join(
|
|
224
|
+
repoRoot,
|
|
225
|
+
"packages",
|
|
226
|
+
"app",
|
|
227
|
+
"src",
|
|
228
|
+
"translations"
|
|
229
|
+
);
|
|
230
|
+
if (fs__default.default.existsSync(translationsDir)) {
|
|
231
|
+
fs__default.default.ensureDirSync(standardPluginDir);
|
|
232
|
+
return standardPluginDir;
|
|
233
|
+
}
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
function getTargetRepoRoot(repoRoot, repoType) {
|
|
237
|
+
if (repoType === "backstage" || repoType === "community-plugins") {
|
|
238
|
+
const possibleRhdhPaths = [
|
|
239
|
+
path__default.default.join(path__default.default.dirname(repoRoot), "rhdh"),
|
|
240
|
+
// Fallback: Try environment variable or common development location
|
|
241
|
+
process.env.RHDH_REPO_PATH || path__default.default.join(os__default.default.homedir(), "redhat", "rhdh")
|
|
242
|
+
];
|
|
243
|
+
for (const rhdhPath of possibleRhdhPaths) {
|
|
244
|
+
if (fs__default.default.existsSync(rhdhPath)) {
|
|
245
|
+
return rhdhPath;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
console.warn(
|
|
249
|
+
chalk__default.default.yellow(
|
|
250
|
+
`\u26A0\uFE0F RHDH repo not found. Deploying to translations directory in current location.`
|
|
251
|
+
)
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
return repoRoot;
|
|
255
|
+
}
|
|
256
|
+
function getCommunityPluginsRepoRoot(repoRoot) {
|
|
257
|
+
const possiblePaths = [
|
|
258
|
+
path__default.default.join(path__default.default.dirname(repoRoot), "community-plugins"),
|
|
259
|
+
// Fallback: Try environment variable or common development location
|
|
260
|
+
process.env.COMMUNITY_PLUGINS_REPO_PATH || path__default.default.join(os__default.default.homedir(), "redhat", "community-plugins")
|
|
261
|
+
];
|
|
262
|
+
for (const communityPluginsPath of possiblePaths) {
|
|
263
|
+
if (fs__default.default.existsSync(communityPluginsPath)) {
|
|
264
|
+
const workspacesDir = path__default.default.join(communityPluginsPath, "workspaces");
|
|
265
|
+
if (fs__default.default.existsSync(workspacesDir)) {
|
|
266
|
+
return communityPluginsPath;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
function isRedHatOwnedPlugin(pluginName, communityPluginsRoot) {
|
|
273
|
+
if (!communityPluginsRoot) {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
const cleanPluginName = pluginName.replace(/^plugin\./, "");
|
|
277
|
+
const workspacesDir = path__default.default.join(communityPluginsRoot, "workspaces");
|
|
278
|
+
if (!fs__default.default.existsSync(workspacesDir)) {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
const workspaceDirs = fs__default.default.readdirSync(workspacesDir);
|
|
282
|
+
for (const workspace of workspaceDirs) {
|
|
283
|
+
const pluginDir = path__default.default.join(
|
|
284
|
+
workspacesDir,
|
|
285
|
+
workspace,
|
|
286
|
+
"plugins",
|
|
287
|
+
cleanPluginName
|
|
288
|
+
);
|
|
289
|
+
if (fs__default.default.existsSync(pluginDir)) {
|
|
290
|
+
const packageJsonPath = path__default.default.join(pluginDir, "package.json");
|
|
291
|
+
if (fs__default.default.existsSync(packageJsonPath)) {
|
|
292
|
+
try {
|
|
293
|
+
const packageJson = fs__default.default.readJsonSync(packageJsonPath);
|
|
294
|
+
if (packageJson.author === "Red Hat") {
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
} catch {
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
function findPluginTranslationDir(pluginName, repoRoot, repoType) {
|
|
305
|
+
if (repoType === "backstage" || repoType === "community-plugins") {
|
|
306
|
+
const workspacesDir = path__default.default.join(repoRoot, "workspaces");
|
|
307
|
+
if (fs__default.default.existsSync(workspacesDir)) {
|
|
308
|
+
return findPluginInWorkspaces(pluginName, repoRoot);
|
|
309
|
+
}
|
|
310
|
+
const targetRoot = getTargetRepoRoot(repoRoot, repoType);
|
|
311
|
+
const translationsDir = path__default.default.join(targetRoot, "translations");
|
|
312
|
+
const pluginDir = path__default.default.join(translationsDir, pluginName);
|
|
313
|
+
if (!fs__default.default.existsSync(pluginDir)) {
|
|
314
|
+
fs__default.default.ensureDirSync(pluginDir);
|
|
315
|
+
}
|
|
316
|
+
return pluginDir;
|
|
317
|
+
}
|
|
318
|
+
if (repoType === "rhdh-plugins") {
|
|
319
|
+
return findPluginInWorkspaces(pluginName, repoRoot);
|
|
320
|
+
}
|
|
321
|
+
if (repoType === "rhdh") {
|
|
322
|
+
if (pluginName === "rhdh") {
|
|
323
|
+
const rhdhDir = path__default.default.join(
|
|
324
|
+
repoRoot,
|
|
325
|
+
"packages",
|
|
326
|
+
"app",
|
|
327
|
+
"src",
|
|
328
|
+
"translations",
|
|
329
|
+
"rhdh"
|
|
330
|
+
);
|
|
331
|
+
if (!fs__default.default.existsSync(rhdhDir)) {
|
|
332
|
+
fs__default.default.ensureDirSync(rhdhDir);
|
|
333
|
+
}
|
|
334
|
+
return rhdhDir;
|
|
335
|
+
}
|
|
336
|
+
return findPluginInRhdh(pluginName, repoRoot);
|
|
337
|
+
}
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
function extractCopyrightHeader(filePath) {
|
|
341
|
+
try {
|
|
342
|
+
if (!fs__default.default.existsSync(filePath)) {
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
const content = fs__default.default.readFileSync(filePath, "utf-8");
|
|
346
|
+
const headerMatch = content.match(/\/\*[\s\S]*?\*\//);
|
|
347
|
+
if (headerMatch) {
|
|
348
|
+
return headerMatch[0];
|
|
349
|
+
}
|
|
350
|
+
} catch {
|
|
351
|
+
}
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
function getCopyrightHeader(translationDir) {
|
|
355
|
+
const refFile = path__default.default.join(translationDir, "ref.ts");
|
|
356
|
+
let header = extractCopyrightHeader(refFile);
|
|
357
|
+
if (header) {
|
|
358
|
+
return header;
|
|
359
|
+
}
|
|
360
|
+
if (fs__default.default.existsSync(translationDir)) {
|
|
361
|
+
const langFiles = fs__default.default.readdirSync(translationDir).filter(
|
|
362
|
+
(f) => f.endsWith(".ts") && !f.includes("ref") && !f.includes("translations") && !f.includes("index")
|
|
363
|
+
);
|
|
364
|
+
for (const langFile of langFiles) {
|
|
365
|
+
const langFilePath = path__default.default.join(translationDir, langFile);
|
|
366
|
+
header = extractCopyrightHeader(langFilePath);
|
|
367
|
+
if (header) {
|
|
368
|
+
return header;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return `/*
|
|
373
|
+
* Copyright 2024 The Backstage Authors
|
|
374
|
+
*
|
|
375
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
376
|
+
* you may not use this file except in compliance with the License.
|
|
377
|
+
* You may obtain a copy of the License at
|
|
378
|
+
*
|
|
379
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
380
|
+
*
|
|
381
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
382
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
383
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
384
|
+
* See the License for the specific language governing permissions and
|
|
385
|
+
* limitations under the License.
|
|
386
|
+
*/`;
|
|
387
|
+
}
|
|
388
|
+
function generateTranslationFile(pluginName, lang, messages, refImportName, refImportPath, variableName, translationDir) {
|
|
389
|
+
let langName;
|
|
390
|
+
if (lang === "it") {
|
|
391
|
+
langName = "Italian";
|
|
392
|
+
} else if (lang === "ja") {
|
|
393
|
+
langName = "Japanese";
|
|
394
|
+
} else {
|
|
395
|
+
langName = lang;
|
|
396
|
+
}
|
|
397
|
+
const messagesContent = Object.entries(messages).map(([key, value]) => {
|
|
398
|
+
const escapedValue = value.replaceAll(/\\/g, "\\\\").replaceAll(/'/g, "\\'").replaceAll(/\n/g, "\\n");
|
|
399
|
+
return ` '${key}': '${escapedValue}',`;
|
|
400
|
+
}).join("\n");
|
|
401
|
+
const copyrightHeader = translationDir ? getCopyrightHeader(translationDir) : `/*
|
|
402
|
+
* Copyright 2024 The Backstage Authors
|
|
403
|
+
*
|
|
404
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
405
|
+
* you may not use this file except in compliance with the License.
|
|
406
|
+
* You may obtain a copy of the License at
|
|
407
|
+
*
|
|
408
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
409
|
+
*
|
|
410
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
411
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
412
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
413
|
+
* See the License for the specific language governing permissions and
|
|
414
|
+
* limitations under the License.
|
|
415
|
+
*/`;
|
|
416
|
+
return `${copyrightHeader}
|
|
417
|
+
|
|
418
|
+
import { createTranslationMessages } from '@backstage/core-plugin-api/alpha';
|
|
419
|
+
import { ${refImportName} } from '${refImportPath}';
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* ${langName} translation for ${pluginName}.
|
|
423
|
+
* @public
|
|
424
|
+
*/
|
|
425
|
+
const ${variableName} = createTranslationMessages({
|
|
426
|
+
ref: ${refImportName},
|
|
427
|
+
messages: {
|
|
428
|
+
${messagesContent}
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
export default ${variableName};
|
|
433
|
+
`;
|
|
434
|
+
}
|
|
435
|
+
function detectDownloadedFiles(downloadDir, repoType) {
|
|
436
|
+
const files = {};
|
|
437
|
+
if (!fs__default.default.existsSync(downloadDir)) {
|
|
438
|
+
return files;
|
|
439
|
+
}
|
|
440
|
+
const allFiles = fs__default.default.readdirSync(downloadDir).filter((f) => f.endsWith(".json"));
|
|
441
|
+
for (const file of allFiles) {
|
|
442
|
+
const langCodes = "(it|ja|fr|de|es|ko|zh|pt|ru|ar|hi|nl|pl|sv)";
|
|
443
|
+
let match = file.match(
|
|
444
|
+
new RegExp(`^([a-z-]+)-(s\\d+)-${langCodes}(?:-C)?\\.json$`, "i")
|
|
445
|
+
);
|
|
446
|
+
if (!match) {
|
|
447
|
+
match = file.match(
|
|
448
|
+
new RegExp(
|
|
449
|
+
`^([a-z-]+)-(\\d{4}-\\d{2}-\\d{2})-${langCodes}(?:-C)?\\.json$`,
|
|
450
|
+
"i"
|
|
451
|
+
)
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
if (!match) {
|
|
455
|
+
match = file.match(
|
|
456
|
+
new RegExp(
|
|
457
|
+
`^([a-z-]+)-reference-(\\d{4}-\\d{2}-\\d{2})-${langCodes}(?:-C)?\\.json$`,
|
|
458
|
+
"i"
|
|
459
|
+
)
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
if (match) {
|
|
463
|
+
const fileRepo = match[1];
|
|
464
|
+
const lang = match[3];
|
|
465
|
+
if (repoType === "rhdh-plugins" && fileRepo === "rhdh-plugins" || repoType === "community-plugins" && fileRepo === "community-plugins" || repoType === "rhdh" && fileRepo === "rhdh" || repoType === "backstage" && fileRepo === "backstage" || // Allow backstage and community-plugins files when running from rhdh repo
|
|
466
|
+
// (since they deploy to rhdh/translations)
|
|
467
|
+
repoType === "rhdh" && (fileRepo === "backstage" || fileRepo === "community-plugins")) {
|
|
468
|
+
files[lang] = file;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return files;
|
|
473
|
+
}
|
|
474
|
+
function determineTargetFile(pluginName, lang, repoType, translationDir) {
|
|
475
|
+
if (repoType === "backstage" || repoType === "community-plugins") {
|
|
476
|
+
return path__default.default.join(translationDir, `${lang}.ts`);
|
|
477
|
+
}
|
|
478
|
+
if (repoType === "rhdh") {
|
|
479
|
+
if (pluginName === "rhdh") {
|
|
480
|
+
return path__default.default.join(translationDir, `${lang}.ts`);
|
|
481
|
+
}
|
|
482
|
+
if (fs__default.default.existsSync(translationDir)) {
|
|
483
|
+
const existingFiles = fs__default.default.readdirSync(translationDir).filter(
|
|
484
|
+
(f) => f.endsWith(".ts") && !f.includes("ref") && !f.includes("translations")
|
|
485
|
+
);
|
|
486
|
+
const pluginLangPattern = new RegExp(`^${pluginName}-[a-z]{2}\\.ts$`);
|
|
487
|
+
const pluginLangFile2 = existingFiles.find((f) => pluginLangPattern.test(f));
|
|
488
|
+
if (pluginLangFile2) {
|
|
489
|
+
return path__default.default.join(translationDir, `${pluginName}-${lang}.ts`);
|
|
490
|
+
}
|
|
491
|
+
const langPattern = /^[a-z]{2}\.ts$/;
|
|
492
|
+
const langFile2 = existingFiles.find((f) => langPattern.test(f));
|
|
493
|
+
if (langFile2) {
|
|
494
|
+
return path__default.default.join(translationDir, `${lang}.ts`);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
const pluginLangFile = path__default.default.join(
|
|
498
|
+
translationDir,
|
|
499
|
+
`${pluginName}-${lang}.ts`
|
|
500
|
+
);
|
|
501
|
+
const langFile = path__default.default.join(translationDir, `${lang}.ts`);
|
|
502
|
+
if (fs__default.default.existsSync(pluginLangFile)) {
|
|
503
|
+
return pluginLangFile;
|
|
504
|
+
}
|
|
505
|
+
if (fs__default.default.existsSync(langFile)) {
|
|
506
|
+
return langFile;
|
|
507
|
+
}
|
|
508
|
+
return pluginLangFile;
|
|
509
|
+
}
|
|
510
|
+
return path__default.default.join(translationDir, `${lang}.ts`);
|
|
511
|
+
}
|
|
512
|
+
function getOtherLanguageFiles(lang, repoType, pluginName, translationDir) {
|
|
513
|
+
const otherLangs = ["it", "ja", "de", "fr", "es", "en"].filter(
|
|
514
|
+
(l) => l !== lang
|
|
515
|
+
);
|
|
516
|
+
if (repoType === "rhdh") {
|
|
517
|
+
return otherLangs.flatMap((l) => {
|
|
518
|
+
const pluginLangFile = path__default.default.join(translationDir, `${pluginName}-${l}.ts`);
|
|
519
|
+
const langFile = path__default.default.join(translationDir, `${l}.ts`);
|
|
520
|
+
const files = [];
|
|
521
|
+
if (fs__default.default.existsSync(pluginLangFile)) files.push(pluginLangFile);
|
|
522
|
+
if (fs__default.default.existsSync(langFile)) files.push(langFile);
|
|
523
|
+
return files;
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
return otherLangs.map((l) => path__default.default.join(translationDir, `${l}.ts`)).filter((f) => fs__default.default.existsSync(f));
|
|
527
|
+
}
|
|
528
|
+
function transformVariableName(variableName, lang) {
|
|
529
|
+
const langCapitalized = lang.charAt(0).toUpperCase() + lang.slice(1);
|
|
530
|
+
if (variableName.match(/Translation(It|Ja|De|Fr|Es)$/)) {
|
|
531
|
+
return variableName.replaceAll(
|
|
532
|
+
/Translation(It|Ja|De|Fr|Es)$/g,
|
|
533
|
+
`Translation${langCapitalized}`
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
return lang;
|
|
537
|
+
}
|
|
538
|
+
function verifyImportPath(refInfo, translationDir) {
|
|
539
|
+
if (!refInfo.refImportPath.startsWith("./")) {
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
const expectedFile = path__default.default.join(
|
|
543
|
+
translationDir,
|
|
544
|
+
`${refInfo.refImportPath.replace("./", "")}.ts`
|
|
545
|
+
);
|
|
546
|
+
if (!fs__default.default.existsSync(expectedFile)) {
|
|
547
|
+
refInfo.refImportPath = findRefImportPath(translationDir);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
function extractRefInfoFromOtherFiles(otherLangFiles, repoType, lang, translationDir) {
|
|
551
|
+
for (const otherFile of otherLangFiles) {
|
|
552
|
+
const otherRefInfo = extractRefInfo(otherFile);
|
|
553
|
+
if (!otherRefInfo) {
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
verifyImportPath(otherRefInfo, translationDir);
|
|
557
|
+
const isExternalImport = !otherRefInfo.refImportPath.startsWith("./");
|
|
558
|
+
const shouldUseForRhdh = repoType === "rhdh" && isExternalImport;
|
|
559
|
+
const shouldUseForOthers = repoType !== "rhdh" || !isExternalImport;
|
|
560
|
+
if (shouldUseForRhdh || shouldUseForOthers) {
|
|
561
|
+
return {
|
|
562
|
+
refImportName: otherRefInfo.refImportName,
|
|
563
|
+
refImportPath: otherRefInfo.refImportPath,
|
|
564
|
+
variableName: transformVariableName(otherRefInfo.variableName, lang)
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
function extractRefInfoFromRefFile(translationDir) {
|
|
571
|
+
const refFile = path__default.default.join(translationDir, "ref.ts");
|
|
572
|
+
if (!fs__default.default.existsSync(refFile)) {
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
try {
|
|
576
|
+
const content = fs__default.default.readFileSync(refFile, "utf-8");
|
|
577
|
+
const refExportMatch = content.match(
|
|
578
|
+
/export\s+const\s+(\w+TranslationRef)\s*=/
|
|
579
|
+
);
|
|
580
|
+
if (refExportMatch) {
|
|
581
|
+
return {
|
|
582
|
+
refImportName: refExportMatch[1],
|
|
583
|
+
refImportPath: "./ref"
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
} catch {
|
|
587
|
+
}
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
590
|
+
function getRefInfoForPlugin(pluginName, lang, repoType, translationDir, targetFile, exists) {
|
|
591
|
+
const otherLangFiles = getOtherLanguageFiles(
|
|
592
|
+
lang,
|
|
593
|
+
repoType,
|
|
594
|
+
pluginName,
|
|
595
|
+
translationDir
|
|
596
|
+
);
|
|
597
|
+
const refInfoFromOthers = extractRefInfoFromOtherFiles(
|
|
598
|
+
otherLangFiles,
|
|
599
|
+
repoType,
|
|
600
|
+
lang,
|
|
601
|
+
translationDir
|
|
602
|
+
);
|
|
603
|
+
if (refInfoFromOthers) {
|
|
604
|
+
return refInfoFromOthers;
|
|
605
|
+
}
|
|
606
|
+
if (exists) {
|
|
607
|
+
const existingRefInfo = extractRefInfo(targetFile);
|
|
608
|
+
if (existingRefInfo) {
|
|
609
|
+
return existingRefInfo;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
const anyOtherFile = otherLangFiles.find((f) => fs__default.default.existsSync(f));
|
|
613
|
+
if (anyOtherFile) {
|
|
614
|
+
const otherRefInfo = extractRefInfo(anyOtherFile);
|
|
615
|
+
if (otherRefInfo) {
|
|
616
|
+
verifyImportPath(otherRefInfo, translationDir);
|
|
617
|
+
return {
|
|
618
|
+
refImportName: otherRefInfo.refImportName,
|
|
619
|
+
refImportPath: otherRefInfo.refImportPath,
|
|
620
|
+
variableName: transformVariableName(otherRefInfo.variableName, lang)
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
const refInfoFromRef = extractRefInfoFromRefFile(translationDir);
|
|
625
|
+
if (refInfoFromRef) {
|
|
626
|
+
const langCapitalized = lang.charAt(0).toUpperCase() + lang.slice(1);
|
|
627
|
+
const baseName = refInfoFromRef.refImportName.replace("TranslationRef", "");
|
|
628
|
+
const variableName = `${baseName}Translation${langCapitalized}`;
|
|
629
|
+
return {
|
|
630
|
+
refImportName: refInfoFromRef.refImportName,
|
|
631
|
+
refImportPath: refInfoFromRef.refImportPath,
|
|
632
|
+
variableName
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
return inferRefInfo(pluginName, lang, repoType, translationDir);
|
|
636
|
+
}
|
|
637
|
+
function processPluginTranslation(pluginName, translations, lang, repoType, repoRoot) {
|
|
638
|
+
const translationDir = findPluginTranslationDir(
|
|
639
|
+
pluginName,
|
|
640
|
+
repoRoot,
|
|
641
|
+
repoType
|
|
642
|
+
);
|
|
643
|
+
if (!translationDir) {
|
|
644
|
+
console.warn(
|
|
645
|
+
chalk__default.default.yellow(` \u26A0\uFE0F Plugin "${pluginName}" not found, skipping...`)
|
|
646
|
+
);
|
|
647
|
+
return null;
|
|
648
|
+
}
|
|
649
|
+
console.log(chalk__default.default.gray(` \u{1F4E6} Plugin: ${pluginName} \u2192 ${translationDir}`));
|
|
650
|
+
const targetFile = determineTargetFile(
|
|
651
|
+
pluginName,
|
|
652
|
+
lang,
|
|
653
|
+
repoType,
|
|
654
|
+
translationDir
|
|
655
|
+
);
|
|
656
|
+
const exists = fs__default.default.existsSync(targetFile);
|
|
657
|
+
const refInfo = getRefInfoForPlugin(
|
|
658
|
+
pluginName,
|
|
659
|
+
lang,
|
|
660
|
+
repoType,
|
|
661
|
+
translationDir,
|
|
662
|
+
targetFile,
|
|
663
|
+
exists
|
|
664
|
+
);
|
|
665
|
+
const refFile = path__default.default.join(translationDir, "ref.ts");
|
|
666
|
+
let filteredTranslations = translations;
|
|
667
|
+
let filteredCount = 0;
|
|
668
|
+
if (fs__default.default.existsSync(refFile)) {
|
|
669
|
+
const refKeys = extractKeysFromRefFile(refFile);
|
|
670
|
+
if (refKeys.size > 0) {
|
|
671
|
+
const validKeys = new Set(refKeys);
|
|
672
|
+
const originalCount = Object.keys(translations).length;
|
|
673
|
+
filteredTranslations = Object.fromEntries(
|
|
674
|
+
Object.entries(translations).filter(([key]) => validKeys.has(key))
|
|
675
|
+
);
|
|
676
|
+
filteredCount = originalCount - Object.keys(filteredTranslations).length;
|
|
677
|
+
if (filteredCount > 0) {
|
|
678
|
+
const invalidKeys = Object.keys(translations).filter(
|
|
679
|
+
(key) => !validKeys.has(key)
|
|
680
|
+
);
|
|
681
|
+
console.warn(
|
|
682
|
+
chalk__default.default.yellow(
|
|
683
|
+
` \u26A0\uFE0F Filtered out ${filteredCount} invalid key(s) not in ref.ts: ${invalidKeys.slice(0, 3).join(", ")}${invalidKeys.length > 3 ? "..." : ""}`
|
|
684
|
+
)
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
const content = generateTranslationFile(
|
|
690
|
+
pluginName,
|
|
691
|
+
lang,
|
|
692
|
+
filteredTranslations,
|
|
693
|
+
refInfo.refImportName,
|
|
694
|
+
refInfo.refImportPath,
|
|
695
|
+
refInfo.variableName,
|
|
696
|
+
translationDir
|
|
697
|
+
);
|
|
698
|
+
fs__default.default.writeFileSync(targetFile, content, "utf-8");
|
|
699
|
+
const relativePath = path__default.default.relative(repoRoot, targetFile);
|
|
700
|
+
const keyCount = Object.keys(filteredTranslations).length;
|
|
701
|
+
if (exists) {
|
|
702
|
+
console.log(
|
|
703
|
+
chalk__default.default.green(` \u2705 Updated: ${relativePath} (${keyCount} keys)`)
|
|
704
|
+
);
|
|
705
|
+
return { updated: true, created: false };
|
|
706
|
+
}
|
|
707
|
+
console.log(
|
|
708
|
+
chalk__default.default.green(` \u2728 Created: ${relativePath} (${keyCount} keys)`)
|
|
709
|
+
);
|
|
710
|
+
return { updated: false, created: true };
|
|
711
|
+
}
|
|
712
|
+
function processLanguageTranslations(data, lang, repoType, repoRoot, communityPluginsRoot) {
|
|
713
|
+
let updated = 0;
|
|
714
|
+
let created = 0;
|
|
715
|
+
for (const [pluginName, pluginData] of Object.entries(data)) {
|
|
716
|
+
const pluginDataObj = pluginData;
|
|
717
|
+
let translations = pluginDataObj[lang] || {};
|
|
718
|
+
if (Object.keys(translations).length === 0 && pluginDataObj.en) {
|
|
719
|
+
pluginDataObj[lang] = pluginDataObj.en;
|
|
720
|
+
translations = pluginDataObj[lang];
|
|
721
|
+
}
|
|
722
|
+
if (Object.keys(translations).length === 0) {
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
if (repoType === "backstage") {
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
if (repoType === "community-plugins") {
|
|
729
|
+
if (communityPluginsRoot && isRedHatOwnedPlugin(pluginName, communityPluginsRoot)) {
|
|
730
|
+
console.log(
|
|
731
|
+
chalk__default.default.cyan(
|
|
732
|
+
` \u{1F534} Red Hat owned plugin detected: ${pluginName} \u2192 deploying to community-plugins`
|
|
733
|
+
)
|
|
734
|
+
);
|
|
735
|
+
const communityPluginsResult = processPluginTranslation(
|
|
736
|
+
pluginName,
|
|
737
|
+
translations,
|
|
738
|
+
lang,
|
|
739
|
+
"community-plugins",
|
|
740
|
+
communityPluginsRoot
|
|
741
|
+
);
|
|
742
|
+
if (communityPluginsResult) {
|
|
743
|
+
if (communityPluginsResult.updated) updated++;
|
|
744
|
+
if (communityPluginsResult.created) created++;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
const result = processPluginTranslation(
|
|
750
|
+
pluginName,
|
|
751
|
+
translations,
|
|
752
|
+
lang,
|
|
753
|
+
repoType,
|
|
754
|
+
repoRoot
|
|
755
|
+
);
|
|
756
|
+
if (result) {
|
|
757
|
+
if (result.updated) updated++;
|
|
758
|
+
if (result.created) created++;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
return { updated, created };
|
|
762
|
+
}
|
|
763
|
+
function extractKeysFromTranslationFile(filePath) {
|
|
764
|
+
const keys = /* @__PURE__ */ new Set();
|
|
765
|
+
try {
|
|
766
|
+
const content = fs__default.default.readFileSync(filePath, "utf-8");
|
|
767
|
+
const messagesMatch = content.match(/messages:\s*\{([\s\S]*?)\}\s*\}\)/);
|
|
768
|
+
if (messagesMatch) {
|
|
769
|
+
const messagesContent = messagesMatch[1];
|
|
770
|
+
const keyPattern = /['"]([^'"]+)['"]\s*:\s*['"]/g;
|
|
771
|
+
let match = keyPattern.exec(messagesContent);
|
|
772
|
+
while (match !== null) {
|
|
773
|
+
keys.add(match[1]);
|
|
774
|
+
match = keyPattern.exec(messagesContent);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
} catch (error) {
|
|
778
|
+
}
|
|
779
|
+
return keys;
|
|
780
|
+
}
|
|
781
|
+
function extractKeysFromRefFile(refFilePath) {
|
|
782
|
+
const keys = /* @__PURE__ */ new Set();
|
|
783
|
+
try {
|
|
784
|
+
const content = fs__default.default.readFileSync(refFilePath, "utf-8");
|
|
785
|
+
const messagesMatch = content.match(
|
|
786
|
+
/export\s+const\s+\w+Messages\s*=\s*\{([\s\S]*?)\};/
|
|
787
|
+
);
|
|
788
|
+
if (!messagesMatch) {
|
|
789
|
+
return keys;
|
|
790
|
+
}
|
|
791
|
+
const messagesContent = messagesMatch[1];
|
|
792
|
+
const extractNestedKeys = (nestedContent, prefix = "", depth = 0, maxDepth = 10) => {
|
|
793
|
+
if (depth > maxDepth) {
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
const isWhitespace = (char) => {
|
|
797
|
+
return char === " " || char === " " || char === "\n" || char === "\r";
|
|
798
|
+
};
|
|
799
|
+
const isIdentifierStart = (char) => {
|
|
800
|
+
return char >= "a" && char <= "z" || char >= "A" && char <= "Z" || char === "_" || char === "$";
|
|
801
|
+
};
|
|
802
|
+
const isIdentifierPart = (char) => {
|
|
803
|
+
return isIdentifierStart(char) || char >= "0" && char <= "9";
|
|
804
|
+
};
|
|
805
|
+
const skipWhitespace = (text, startIndex) => {
|
|
806
|
+
let idx = startIndex;
|
|
807
|
+
while (idx < text.length && isWhitespace(text[idx])) {
|
|
808
|
+
idx++;
|
|
809
|
+
}
|
|
810
|
+
return idx;
|
|
811
|
+
};
|
|
812
|
+
const findMatchingBrace = (textContent, startIndex) => {
|
|
813
|
+
const valueStart = textContent.indexOf("{", startIndex);
|
|
814
|
+
if (valueStart === -1) {
|
|
815
|
+
return null;
|
|
816
|
+
}
|
|
817
|
+
let braceCount = 0;
|
|
818
|
+
for (let i = valueStart; i < textContent.length; i++) {
|
|
819
|
+
if (textContent[i] === "{") braceCount++;
|
|
820
|
+
if (textContent[i] === "}") {
|
|
821
|
+
braceCount--;
|
|
822
|
+
if (braceCount === 0) {
|
|
823
|
+
return textContent.substring(valueStart, i + 1);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
return null;
|
|
828
|
+
};
|
|
829
|
+
const MAX_CONTENT_LENGTH = 1024 * 1024;
|
|
830
|
+
if (nestedContent.length > MAX_CONTENT_LENGTH) {
|
|
831
|
+
console.warn(
|
|
832
|
+
`Content too large (${nestedContent.length} chars), skipping extraction`
|
|
833
|
+
);
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
const allMatches = [];
|
|
837
|
+
const parseKeyValuePairs = (textToParse) => {
|
|
838
|
+
let i = 0;
|
|
839
|
+
const MAX_VALUE_LENGTH = 5e3;
|
|
840
|
+
const MAX_KEY_LENGTH = 500;
|
|
841
|
+
while (i < textToParse.length) {
|
|
842
|
+
i = skipWhitespace(textToParse, i);
|
|
843
|
+
if (i >= textToParse.length) break;
|
|
844
|
+
const keyStart = i;
|
|
845
|
+
let key = "";
|
|
846
|
+
let keyEnd = i;
|
|
847
|
+
if (textToParse[i] === "'" || textToParse[i] === '"') {
|
|
848
|
+
const quote = textToParse[i];
|
|
849
|
+
i++;
|
|
850
|
+
const keyStartInner = i;
|
|
851
|
+
while (i < textToParse.length && textToParse[i] !== quote) {
|
|
852
|
+
if (i - keyStartInner > MAX_KEY_LENGTH) break;
|
|
853
|
+
i++;
|
|
854
|
+
}
|
|
855
|
+
if (i < textToParse.length && textToParse[i] === quote) {
|
|
856
|
+
key = textToParse.substring(keyStartInner, i);
|
|
857
|
+
i++;
|
|
858
|
+
keyEnd = i;
|
|
859
|
+
} else {
|
|
860
|
+
i++;
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
} else if (isIdentifierStart(textToParse[i])) {
|
|
864
|
+
const keyStartInner = i;
|
|
865
|
+
while (i < textToParse.length && isIdentifierPart(textToParse[i])) {
|
|
866
|
+
if (i - keyStartInner > MAX_KEY_LENGTH) break;
|
|
867
|
+
i++;
|
|
868
|
+
}
|
|
869
|
+
key = textToParse.substring(keyStartInner, i);
|
|
870
|
+
keyEnd = i;
|
|
871
|
+
} else {
|
|
872
|
+
i++;
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
i = skipWhitespace(textToParse, i);
|
|
876
|
+
if (i >= textToParse.length || textToParse[i] !== ":") {
|
|
877
|
+
i = keyEnd + 1;
|
|
878
|
+
continue;
|
|
879
|
+
}
|
|
880
|
+
i++;
|
|
881
|
+
i = skipWhitespace(textToParse, i);
|
|
882
|
+
const valueStart = i;
|
|
883
|
+
let valueEnd = i;
|
|
884
|
+
let foundDelimiter = false;
|
|
885
|
+
while (i < textToParse.length && !foundDelimiter) {
|
|
886
|
+
if (i - valueStart > MAX_VALUE_LENGTH) {
|
|
887
|
+
break;
|
|
888
|
+
}
|
|
889
|
+
const char = textToParse[i];
|
|
890
|
+
if (char === "," || char === "}" || char === "\n") {
|
|
891
|
+
valueEnd = i;
|
|
892
|
+
foundDelimiter = true;
|
|
893
|
+
} else {
|
|
894
|
+
i++;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
if (foundDelimiter && valueEnd > valueStart) {
|
|
898
|
+
const value = textToParse.substring(valueStart, valueEnd).trim();
|
|
899
|
+
if (value.length > 0) {
|
|
900
|
+
allMatches.push({
|
|
901
|
+
key,
|
|
902
|
+
value,
|
|
903
|
+
index: keyStart,
|
|
904
|
+
endIndex: valueEnd + 1
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
i = valueEnd + 1;
|
|
908
|
+
} else {
|
|
909
|
+
i = keyEnd + 1;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
};
|
|
913
|
+
parseKeyValuePairs(nestedContent);
|
|
914
|
+
allMatches.sort((a, b) => a.index - b.index);
|
|
915
|
+
for (const matchData of allMatches) {
|
|
916
|
+
const fullKey = prefix ? `${prefix}.${matchData.key}` : matchData.key;
|
|
917
|
+
if (matchData.value.startsWith("{")) {
|
|
918
|
+
const nestedSubContent = findMatchingBrace(
|
|
919
|
+
nestedContent,
|
|
920
|
+
matchData.index
|
|
921
|
+
);
|
|
922
|
+
if (nestedSubContent) {
|
|
923
|
+
extractNestedKeys(nestedSubContent, fullKey, depth + 1, maxDepth);
|
|
924
|
+
}
|
|
925
|
+
} else if (matchData.value.length > 0 && (matchData.value[0] === "'" || matchData.value[0] === '"')) {
|
|
926
|
+
keys.add(fullKey);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
};
|
|
930
|
+
extractNestedKeys(messagesContent);
|
|
931
|
+
} catch (error) {
|
|
932
|
+
}
|
|
933
|
+
return keys;
|
|
934
|
+
}
|
|
935
|
+
function validateTranslationKeys(_repoType, repoRoot) {
|
|
936
|
+
const errors = [];
|
|
937
|
+
const supportedLangs = ["fr", "it", "ja", "de", "es"];
|
|
938
|
+
const workspacesDir = path__default.default.join(repoRoot, "workspaces");
|
|
939
|
+
if (!fs__default.default.existsSync(workspacesDir)) {
|
|
940
|
+
return { hasErrors: false, errors: [] };
|
|
941
|
+
}
|
|
942
|
+
const workspaceDirs = fs__default.default.readdirSync(workspacesDir);
|
|
943
|
+
for (const workspace of workspaceDirs) {
|
|
944
|
+
const pluginsDir = path__default.default.join(workspacesDir, workspace, "plugins");
|
|
945
|
+
if (!fs__default.default.existsSync(pluginsDir)) {
|
|
946
|
+
continue;
|
|
947
|
+
}
|
|
948
|
+
const pluginDirs = fs__default.default.readdirSync(pluginsDir);
|
|
949
|
+
for (const plugin of pluginDirs) {
|
|
950
|
+
const translationDir = path__default.default.join(
|
|
951
|
+
pluginsDir,
|
|
952
|
+
plugin,
|
|
953
|
+
"src",
|
|
954
|
+
"translations"
|
|
955
|
+
);
|
|
956
|
+
if (!fs__default.default.existsSync(translationDir)) {
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
const refFile = path__default.default.join(translationDir, "ref.ts");
|
|
960
|
+
if (!fs__default.default.existsSync(refFile)) {
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
const refKeys = extractKeysFromRefFile(refFile);
|
|
964
|
+
if (refKeys.size === 0) {
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
967
|
+
for (const lang of supportedLangs) {
|
|
968
|
+
const langFile = path__default.default.join(translationDir, `${lang}.ts`);
|
|
969
|
+
if (!fs__default.default.existsSync(langFile)) {
|
|
970
|
+
continue;
|
|
971
|
+
}
|
|
972
|
+
const langKeys = extractKeysFromTranslationFile(langFile);
|
|
973
|
+
const missingKeys = Array.from(refKeys).filter(
|
|
974
|
+
(key) => !langKeys.has(key)
|
|
975
|
+
);
|
|
976
|
+
if (missingKeys.length > 0) {
|
|
977
|
+
errors.push(
|
|
978
|
+
`\u274C ${workspace}/plugins/${plugin}/src/translations/${lang}.ts is missing ${missingKeys.length} key(s): ${missingKeys.slice(0, 5).join(", ")}${missingKeys.length > 5 ? "..." : ""}`
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
const extraKeys = Array.from(langKeys).filter((key) => !refKeys.has(key));
|
|
982
|
+
if (extraKeys.length > 0) {
|
|
983
|
+
errors.push(
|
|
984
|
+
`\u26A0\uFE0F ${workspace}/plugins/${plugin}/src/translations/${lang}.ts has ${extraKeys.length} extra key(s) not in ref: ${extraKeys.slice(0, 5).join(", ")}${extraKeys.length > 5 ? "..." : ""}`
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
return { hasErrors: errors.length > 0, errors };
|
|
991
|
+
}
|
|
992
|
+
async function deployTranslations(downloadDir, repoRoot) {
|
|
993
|
+
console.log(chalk__default.default.blue("\u{1F680} Deploying translations...\n"));
|
|
994
|
+
const repoType = detectRepoType(repoRoot);
|
|
995
|
+
console.log(chalk__default.default.cyan(`\u{1F4E6} Detected repository: ${repoType}
|
|
996
|
+
`));
|
|
997
|
+
if (repoType === "unknown") {
|
|
998
|
+
throw new Error(
|
|
999
|
+
"Could not detect repository type. Expected: rhdh-plugins, community-plugins, rhdh, or backstage"
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
1002
|
+
const repoFiles = detectDownloadedFiles(downloadDir, repoType);
|
|
1003
|
+
if (Object.keys(repoFiles).length === 0) {
|
|
1004
|
+
console.warn(
|
|
1005
|
+
chalk__default.default.yellow(
|
|
1006
|
+
`\u26A0\uFE0F No translation files found for ${repoType} in ${downloadDir}`
|
|
1007
|
+
)
|
|
1008
|
+
);
|
|
1009
|
+
console.warn(
|
|
1010
|
+
chalk__default.default.gray(
|
|
1011
|
+
` Expected files like: ${repoType}-*-{lang}.json or ${repoType}-*-{lang}-C.json`
|
|
1012
|
+
)
|
|
1013
|
+
);
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
console.log(
|
|
1017
|
+
chalk__default.default.cyan(
|
|
1018
|
+
`\u{1F4C1} Found ${Object.keys(repoFiles).length} translation file(s) for ${repoType}`
|
|
1019
|
+
)
|
|
1020
|
+
);
|
|
1021
|
+
let totalUpdated = 0;
|
|
1022
|
+
let totalCreated = 0;
|
|
1023
|
+
for (const [lang, originalFilename] of Object.entries(repoFiles)) {
|
|
1024
|
+
const filepath = path__default.default.join(downloadDir, originalFilename);
|
|
1025
|
+
if (!fs__default.default.existsSync(filepath)) {
|
|
1026
|
+
console.warn(chalk__default.default.yellow(` \u26A0\uFE0F File not found: ${originalFilename}`));
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
1029
|
+
let displayFilename = originalFilename;
|
|
1030
|
+
displayFilename = displayFilename.replace(/-C\.json$/, ".json");
|
|
1031
|
+
displayFilename = displayFilename.replace(/-reference-/, "-");
|
|
1032
|
+
let effectiveRepoType = repoType;
|
|
1033
|
+
const fileRepoMatch = originalFilename.match(
|
|
1034
|
+
/^(backstage|community-plugins|rhdh-plugins|rhdh)-/
|
|
1035
|
+
);
|
|
1036
|
+
if (fileRepoMatch && repoType === "rhdh") {
|
|
1037
|
+
const fileRepo = fileRepoMatch[1];
|
|
1038
|
+
if (fileRepo === "backstage" || fileRepo === "community-plugins") {
|
|
1039
|
+
effectiveRepoType = fileRepo;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
let data;
|
|
1043
|
+
try {
|
|
1044
|
+
const fileContent = fs__default.default.readFileSync(filepath, "utf-8");
|
|
1045
|
+
data = JSON.parse(fileContent);
|
|
1046
|
+
} catch (error) {
|
|
1047
|
+
console.error(
|
|
1048
|
+
chalk__default.default.red(`
|
|
1049
|
+
\u274C Error parsing JSON file: ${displayFilename}`)
|
|
1050
|
+
);
|
|
1051
|
+
console.error(
|
|
1052
|
+
chalk__default.default.red(
|
|
1053
|
+
` ${error instanceof Error ? error.message : "Invalid JSON format"}`
|
|
1054
|
+
)
|
|
1055
|
+
);
|
|
1056
|
+
console.error(
|
|
1057
|
+
chalk__default.default.yellow(
|
|
1058
|
+
` Skipping this file. Please check the file and try again.`
|
|
1059
|
+
)
|
|
1060
|
+
);
|
|
1061
|
+
continue;
|
|
1062
|
+
}
|
|
1063
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
1064
|
+
console.error(
|
|
1065
|
+
chalk__default.default.red(`
|
|
1066
|
+
\u274C Invalid JSON structure in file: ${displayFilename}`)
|
|
1067
|
+
);
|
|
1068
|
+
console.error(
|
|
1069
|
+
chalk__default.default.red(` Expected a JSON object, got: ${typeof data}`)
|
|
1070
|
+
);
|
|
1071
|
+
continue;
|
|
1072
|
+
}
|
|
1073
|
+
console.log(chalk__default.default.cyan(`
|
|
1074
|
+
\u{1F30D} Language: ${lang.toUpperCase()}`));
|
|
1075
|
+
console.log(chalk__default.default.gray(` \u{1F4C4} Processing: ${displayFilename}`));
|
|
1076
|
+
let communityPluginsRoot = null;
|
|
1077
|
+
if (effectiveRepoType === "backstage") {
|
|
1078
|
+
const targetRoot = getTargetRepoRoot(repoRoot, effectiveRepoType);
|
|
1079
|
+
const translationsDir = path__default.default.join(targetRoot, "translations");
|
|
1080
|
+
if (fs__default.default.existsSync(translationsDir)) {
|
|
1081
|
+
let timestamp = "";
|
|
1082
|
+
const dateMatch = displayFilename.match(/(\d{4}-\d{2}-\d{2})/);
|
|
1083
|
+
const sprintMatch = displayFilename.match(/(s\d+)/);
|
|
1084
|
+
if (dateMatch) {
|
|
1085
|
+
timestamp = dateMatch[1];
|
|
1086
|
+
} else if (sprintMatch) {
|
|
1087
|
+
timestamp = sprintMatch[1];
|
|
1088
|
+
} else {
|
|
1089
|
+
timestamp = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1090
|
+
}
|
|
1091
|
+
const cleanFilename = `${effectiveRepoType}-${timestamp}-${lang}.json`;
|
|
1092
|
+
const targetJsonPath = path__default.default.join(translationsDir, cleanFilename);
|
|
1093
|
+
if (filepath !== targetJsonPath) {
|
|
1094
|
+
const jsonData = JSON.parse(fs__default.default.readFileSync(filepath, "utf-8"));
|
|
1095
|
+
const updatedData = {};
|
|
1096
|
+
for (const [pluginName, pluginData] of Object.entries(jsonData)) {
|
|
1097
|
+
const pluginDataObj = pluginData;
|
|
1098
|
+
updatedData[pluginName] = {};
|
|
1099
|
+
if (pluginDataObj.en) {
|
|
1100
|
+
updatedData[pluginName][lang] = pluginDataObj.en;
|
|
1101
|
+
} else if (pluginDataObj[lang]) {
|
|
1102
|
+
updatedData[pluginName][lang] = pluginDataObj[lang];
|
|
1103
|
+
} else {
|
|
1104
|
+
updatedData[pluginName] = pluginDataObj;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
fs__default.default.writeFileSync(
|
|
1108
|
+
targetJsonPath,
|
|
1109
|
+
JSON.stringify(updatedData, null, 2),
|
|
1110
|
+
"utf-8"
|
|
1111
|
+
);
|
|
1112
|
+
console.log(
|
|
1113
|
+
chalk__default.default.green(
|
|
1114
|
+
` \u{1F4CB} Copied and updated JSON to: ${path__default.default.relative(
|
|
1115
|
+
repoRoot,
|
|
1116
|
+
targetJsonPath
|
|
1117
|
+
)} (replaced "en" with "${lang}")`
|
|
1118
|
+
)
|
|
1119
|
+
);
|
|
1120
|
+
} else {
|
|
1121
|
+
const jsonData = JSON.parse(fs__default.default.readFileSync(filepath, "utf-8"));
|
|
1122
|
+
const updatedData = {};
|
|
1123
|
+
let needsUpdate = false;
|
|
1124
|
+
for (const [pluginName, pluginData] of Object.entries(jsonData)) {
|
|
1125
|
+
const pluginDataObj = pluginData;
|
|
1126
|
+
updatedData[pluginName] = {};
|
|
1127
|
+
if (pluginDataObj.en) {
|
|
1128
|
+
updatedData[pluginName][lang] = pluginDataObj.en;
|
|
1129
|
+
needsUpdate = true;
|
|
1130
|
+
} else if (pluginDataObj[lang]) {
|
|
1131
|
+
updatedData[pluginName][lang] = pluginDataObj[lang];
|
|
1132
|
+
} else {
|
|
1133
|
+
updatedData[pluginName] = pluginDataObj;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
if (needsUpdate) {
|
|
1137
|
+
fs__default.default.writeFileSync(
|
|
1138
|
+
targetJsonPath,
|
|
1139
|
+
JSON.stringify(updatedData, null, 2),
|
|
1140
|
+
"utf-8"
|
|
1141
|
+
);
|
|
1142
|
+
console.log(
|
|
1143
|
+
chalk__default.default.green(
|
|
1144
|
+
` \u{1F4CB} Updated JSON file: ${path__default.default.relative(
|
|
1145
|
+
repoRoot,
|
|
1146
|
+
targetJsonPath
|
|
1147
|
+
)} (replaced "en" with "${lang}")`
|
|
1148
|
+
)
|
|
1149
|
+
);
|
|
1150
|
+
} else {
|
|
1151
|
+
console.log(
|
|
1152
|
+
chalk__default.default.gray(
|
|
1153
|
+
` \u{1F4CB} JSON already in target location: ${path__default.default.relative(
|
|
1154
|
+
repoRoot,
|
|
1155
|
+
targetJsonPath
|
|
1156
|
+
)}`
|
|
1157
|
+
)
|
|
1158
|
+
);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
if (repoType === "rhdh" && effectiveRepoType === "community-plugins") {
|
|
1164
|
+
communityPluginsRoot = getCommunityPluginsRepoRoot(repoRoot);
|
|
1165
|
+
if (communityPluginsRoot) {
|
|
1166
|
+
console.log(
|
|
1167
|
+
chalk__default.default.gray(
|
|
1168
|
+
` \u{1F4E6} Community-plugins repo found: ${communityPluginsRoot}`
|
|
1169
|
+
)
|
|
1170
|
+
);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
const { updated, created } = processLanguageTranslations(
|
|
1174
|
+
data,
|
|
1175
|
+
lang,
|
|
1176
|
+
effectiveRepoType,
|
|
1177
|
+
repoRoot,
|
|
1178
|
+
communityPluginsRoot
|
|
1179
|
+
);
|
|
1180
|
+
totalUpdated += updated;
|
|
1181
|
+
totalCreated += created;
|
|
1182
|
+
}
|
|
1183
|
+
console.log(chalk__default.default.blue(`
|
|
1184
|
+
|
|
1185
|
+
\u{1F4CA} Summary:`));
|
|
1186
|
+
console.log(chalk__default.default.green(` \u2705 Updated: ${totalUpdated} files`));
|
|
1187
|
+
console.log(chalk__default.default.green(` \u2728 Created: ${totalCreated} files`));
|
|
1188
|
+
console.log(chalk__default.default.blue(`
|
|
1189
|
+
\u{1F50D} Validating translation keys...`));
|
|
1190
|
+
const validationResults = validateTranslationKeys(repoType, repoRoot);
|
|
1191
|
+
if (validationResults.hasErrors) {
|
|
1192
|
+
console.log(chalk__default.default.red(`
|
|
1193
|
+
\u274C Validation found issues:`));
|
|
1194
|
+
validationResults.errors.forEach((error) => {
|
|
1195
|
+
console.log(chalk__default.default.red(` ${error}`));
|
|
1196
|
+
});
|
|
1197
|
+
console.log(
|
|
1198
|
+
chalk__default.default.yellow(
|
|
1199
|
+
`
|
|
1200
|
+
\u26A0\uFE0F Please review and fix the missing keys before committing.`
|
|
1201
|
+
)
|
|
1202
|
+
);
|
|
1203
|
+
} else {
|
|
1204
|
+
console.log(chalk__default.default.green(`
|
|
1205
|
+
\u2705 All translation files have matching keys!`));
|
|
1206
|
+
}
|
|
1207
|
+
console.log(chalk__default.default.blue(`
|
|
1208
|
+
\u{1F389} Deployment complete!`));
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
exports.deployTranslations = deployTranslations;
|
|
1212
|
+
exports.isRedHatOwnedPlugin = isRedHatOwnedPlugin;
|
|
1213
|
+
//# sourceMappingURL=deployTranslations.cjs.js.map
|