@backstage/cli-module-translations 0.0.0-nightly-20260317031259

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 ADDED
@@ -0,0 +1,13 @@
1
+ # @backstage/cli-module-translations
2
+
3
+ ## 0.0.0-nightly-20260317031259
4
+
5
+ ### Minor Changes
6
+
7
+ - 329f394: Initial release of the CLI module packages. Each module provides a set of commands that can be discovered automatically by `@backstage/cli` or executed standalone.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies
12
+ - @backstage/cli-node@0.0.0-nightly-20260317031259
13
+ - @backstage/cli-common@0.0.0-nightly-20260317031259
package/README.md ADDED
@@ -0,0 +1,15 @@
1
+ # @backstage/cli-module-translations
2
+
3
+ CLI module that provides translation management commands for the Backstage CLI.
4
+
5
+ ## Commands
6
+
7
+ | Command | Description |
8
+ | :-------------------- | :------------------------------------------------------------------------------------ |
9
+ | `translations export` | Export translation messages from an app and all of its frontend plugins to JSON files |
10
+ | `translations import` | Generate translation resource wiring from translated JSON files |
11
+
12
+ ## Documentation
13
+
14
+ - [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md)
15
+ - [Backstage Documentation](https://backstage.io/docs)
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * Copyright 2024 The Backstage Authors
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+
18
+ const path = require('node:path');
19
+
20
+ /* eslint-disable-next-line no-restricted-syntax */
21
+ const isLocal = require('node:fs').existsSync(
22
+ path.resolve(__dirname, '../src'),
23
+ );
24
+
25
+ if (isLocal) {
26
+ require('@backstage/cli-node/config/nodeTransform.cjs');
27
+ }
28
+
29
+ const { runCliModule } = require('@backstage/cli-node');
30
+ const cliModule = require(isLocal ? '../src/index' : '..').default;
31
+ const pkg = require('../package.json');
32
+ runCliModule({ module: cliModule, name: pkg.name, version: pkg.version });
@@ -0,0 +1,128 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var cleye = require('cleye');
6
+ var cliCommon = require('@backstage/cli-common');
7
+ var fs = require('fs-extra');
8
+ var node_path = require('node:path');
9
+ var discoverPackages = require('../lib/discoverPackages.cjs.js');
10
+ var extractTranslations = require('../lib/extractTranslations.cjs.js');
11
+ var messageFilePath = require('../lib/messageFilePath.cjs.js');
12
+
13
+ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
14
+
15
+ var fs__default = /*#__PURE__*/_interopDefaultCompat(fs);
16
+
17
+ var _export = async ({ args, info }) => {
18
+ const {
19
+ flags: { output, pattern }
20
+ } = cleye.cli(
21
+ {
22
+ help: info,
23
+ booleanFlagNegation: true,
24
+ flags: {
25
+ output: {
26
+ type: String,
27
+ default: "translations",
28
+ description: "Output directory for exported messages and manifest"
29
+ },
30
+ pattern: {
31
+ type: String,
32
+ default: messageFilePath.DEFAULT_MESSAGE_PATTERN,
33
+ description: "File path pattern for message files, with {id} and {lang} placeholders"
34
+ }
35
+ }
36
+ },
37
+ void 0,
38
+ args
39
+ );
40
+ const options = { output, pattern };
41
+ messageFilePath.validatePattern(options.pattern);
42
+ const targetPackageJson = await discoverPackages.readTargetPackage(
43
+ cliCommon.targetPaths.dir,
44
+ cliCommon.targetPaths.rootDir
45
+ );
46
+ const outputDir = node_path.resolve(cliCommon.targetPaths.dir, options.output);
47
+ const manifestPath = node_path.resolve(outputDir, "manifest.json");
48
+ const tsconfigPath = cliCommon.targetPaths.resolveRoot("tsconfig.json");
49
+ if (!await fs__default.default.pathExists(tsconfigPath)) {
50
+ throw new Error(
51
+ `No tsconfig.json found at ${tsconfigPath}. The translations export command requires a tsconfig.json in the repo root.`
52
+ );
53
+ }
54
+ console.log(
55
+ `Discovering frontend dependencies of ${targetPackageJson.name}...`
56
+ );
57
+ const packages = await discoverPackages.discoverFrontendPackages(
58
+ targetPackageJson,
59
+ cliCommon.targetPaths.dir
60
+ );
61
+ console.log(`Found ${packages.length} frontend packages to scan`);
62
+ console.log("Creating TypeScript project...");
63
+ const project = extractTranslations.createTranslationProject(tsconfigPath);
64
+ const allRefs = [];
65
+ for (const pkg of packages) {
66
+ for (const [exportPath, filePath] of pkg.entryPoints) {
67
+ try {
68
+ const sourceFile = project.addSourceFileAtPath(filePath);
69
+ const refs = extractTranslations.extractTranslationRefsFromSourceFile(
70
+ sourceFile,
71
+ pkg.name,
72
+ exportPath
73
+ );
74
+ allRefs.push(...refs);
75
+ } catch (error) {
76
+ console.warn(
77
+ ` Warning: failed to process ${pkg.name} (${exportPath}): ${error}`
78
+ );
79
+ }
80
+ }
81
+ }
82
+ if (allRefs.length === 0) {
83
+ console.log("No translation refs found.");
84
+ return;
85
+ }
86
+ console.log(`Found ${allRefs.length} translation ref(s):`);
87
+ for (const ref of allRefs) {
88
+ const messageCount = Object.keys(ref.messages).length;
89
+ console.log(` ${ref.id} (${ref.packageName}, ${messageCount} messages)`);
90
+ }
91
+ for (const ref of allRefs) {
92
+ const relPath = messageFilePath.formatMessagePath(
93
+ options.pattern,
94
+ ref.id,
95
+ messageFilePath.DEFAULT_LANGUAGE
96
+ );
97
+ const filePath = node_path.resolve(outputDir, relPath);
98
+ await fs__default.default.ensureDir(node_path.dirname(filePath));
99
+ await fs__default.default.writeJson(filePath, ref.messages, { spaces: 2 });
100
+ }
101
+ const manifest = {};
102
+ for (const ref of allRefs) {
103
+ manifest[ref.id] = {
104
+ package: ref.packageName,
105
+ exportPath: ref.exportPath,
106
+ exportName: ref.exportName
107
+ };
108
+ }
109
+ await fs__default.default.writeJson(
110
+ manifestPath,
111
+ { pattern: options.pattern, refs: manifest },
112
+ { spaces: 2 }
113
+ );
114
+ const examplePath = messageFilePath.formatMessagePath(
115
+ options.pattern,
116
+ "<ref-id>",
117
+ messageFilePath.DEFAULT_LANGUAGE
118
+ );
119
+ console.log(
120
+ `
121
+ Exported ${allRefs.length} translation ref(s) to ${options.output}/`
122
+ );
123
+ console.log(` Messages: ${options.output}/${examplePath}`);
124
+ console.log(` Manifest: ${options.output}/manifest.json`);
125
+ };
126
+
127
+ exports.default = _export;
128
+ //# sourceMappingURL=export.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"export.cjs.js","sources":["../../src/commands/export.ts"],"sourcesContent":["/*\n * Copyright 2026 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { cli } from 'cleye';\nimport { targetPaths } from '@backstage/cli-common';\nimport fs from 'fs-extra';\nimport { dirname, resolve as resolvePath } from 'node:path';\nimport {\n discoverFrontendPackages,\n readTargetPackage,\n} from '../lib/discoverPackages';\nimport {\n createTranslationProject,\n extractTranslationRefsFromSourceFile,\n TranslationRefInfo,\n} from '../lib/extractTranslations';\nimport {\n DEFAULT_LANGUAGE,\n DEFAULT_MESSAGE_PATTERN,\n formatMessagePath,\n validatePattern,\n} from '../lib/messageFilePath';\nimport type { CliCommandContext } from '@backstage/cli-node';\n\nexport default async ({ args, info }: CliCommandContext) => {\n const {\n flags: { output, pattern },\n } = cli(\n {\n help: info,\n booleanFlagNegation: true,\n flags: {\n output: {\n type: String,\n default: 'translations',\n description: 'Output directory for exported messages and manifest',\n },\n pattern: {\n type: String,\n default: DEFAULT_MESSAGE_PATTERN,\n description:\n 'File path pattern for message files, with {id} and {lang} placeholders',\n },\n },\n },\n undefined,\n args,\n );\n\n const options = { output, pattern };\n\n validatePattern(options.pattern);\n\n const targetPackageJson = await readTargetPackage(\n targetPaths.dir,\n targetPaths.rootDir,\n );\n\n const outputDir = resolvePath(targetPaths.dir, options.output);\n const manifestPath = resolvePath(outputDir, 'manifest.json');\n\n const tsconfigPath = targetPaths.resolveRoot('tsconfig.json');\n if (!(await fs.pathExists(tsconfigPath))) {\n throw new Error(\n `No tsconfig.json found at ${tsconfigPath}. ` +\n 'The translations export command requires a tsconfig.json in the repo root.',\n );\n }\n\n console.log(\n `Discovering frontend dependencies of ${targetPackageJson.name}...`,\n );\n const packages = await discoverFrontendPackages(\n targetPackageJson,\n targetPaths.dir,\n );\n console.log(`Found ${packages.length} frontend packages to scan`);\n\n console.log('Creating TypeScript project...');\n const project = createTranslationProject(tsconfigPath);\n\n const allRefs: TranslationRefInfo[] = [];\n\n for (const pkg of packages) {\n for (const [exportPath, filePath] of pkg.entryPoints) {\n try {\n const sourceFile = project.addSourceFileAtPath(filePath);\n const refs = extractTranslationRefsFromSourceFile(\n sourceFile,\n pkg.name,\n exportPath,\n );\n allRefs.push(...refs);\n } catch (error) {\n console.warn(\n ` Warning: failed to process ${pkg.name} (${exportPath}): ${error}`,\n );\n }\n }\n }\n\n if (allRefs.length === 0) {\n console.log('No translation refs found.');\n return;\n }\n\n console.log(`Found ${allRefs.length} translation ref(s):`);\n for (const ref of allRefs) {\n const messageCount = Object.keys(ref.messages).length;\n console.log(` ${ref.id} (${ref.packageName}, ${messageCount} messages)`);\n }\n\n // Write message files using the configured pattern\n for (const ref of allRefs) {\n const relPath = formatMessagePath(\n options.pattern,\n ref.id,\n DEFAULT_LANGUAGE,\n );\n const filePath = resolvePath(outputDir, relPath);\n await fs.ensureDir(dirname(filePath));\n await fs.writeJson(filePath, ref.messages, { spaces: 2 });\n }\n\n // Write manifest\n const manifest: Record<string, object> = {};\n for (const ref of allRefs) {\n manifest[ref.id] = {\n package: ref.packageName,\n exportPath: ref.exportPath,\n exportName: ref.exportName,\n };\n }\n await fs.writeJson(\n manifestPath,\n { pattern: options.pattern, refs: manifest },\n { spaces: 2 },\n );\n\n const examplePath = formatMessagePath(\n options.pattern,\n '<ref-id>',\n DEFAULT_LANGUAGE,\n );\n console.log(\n `\\nExported ${allRefs.length} translation ref(s) to ${options.output}/`,\n );\n console.log(` Messages: ${options.output}/${examplePath}`);\n console.log(` Manifest: ${options.output}/manifest.json`);\n};\n"],"names":["cli","DEFAULT_MESSAGE_PATTERN","validatePattern","readTargetPackage","targetPaths","resolvePath","fs","discoverFrontendPackages","createTranslationProject","extractTranslationRefsFromSourceFile","formatMessagePath","DEFAULT_LANGUAGE","dirname"],"mappings":"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"}
@@ -0,0 +1,165 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var cleye = require('cleye');
6
+ var cliCommon = require('@backstage/cli-common');
7
+ var fs = require('fs-extra');
8
+ var node_path = require('node:path');
9
+ var discoverPackages = require('../lib/discoverPackages.cjs.js');
10
+ var messageFilePath = require('../lib/messageFilePath.cjs.js');
11
+
12
+ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
13
+
14
+ var fs__default = /*#__PURE__*/_interopDefaultCompat(fs);
15
+
16
+ var _import = async ({ args, info }) => {
17
+ const {
18
+ flags: { input, output }
19
+ } = cleye.cli(
20
+ {
21
+ help: info,
22
+ booleanFlagNegation: true,
23
+ flags: {
24
+ input: {
25
+ type: String,
26
+ default: "translations",
27
+ description: "Input directory containing the manifest and translated message files"
28
+ },
29
+ output: {
30
+ type: String,
31
+ default: "src/translations/resources.ts",
32
+ description: "Output path for the generated wiring module"
33
+ }
34
+ }
35
+ },
36
+ void 0,
37
+ args
38
+ );
39
+ const options = { input, output };
40
+ await discoverPackages.readTargetPackage(cliCommon.targetPaths.dir, cliCommon.targetPaths.rootDir);
41
+ const inputDir = node_path.resolve(cliCommon.targetPaths.dir, options.input);
42
+ const manifestPath = node_path.resolve(inputDir, "manifest.json");
43
+ const outputPath = node_path.resolve(cliCommon.targetPaths.dir, options.output);
44
+ if (!await fs__default.default.pathExists(manifestPath)) {
45
+ throw new Error(
46
+ `No manifest.json found at ${manifestPath}. Run "backstage-cli translations export" first.`
47
+ );
48
+ }
49
+ const manifest = await fs__default.default.readJson(manifestPath);
50
+ if (!manifest.pattern) {
51
+ throw new Error(
52
+ 'No pattern found in manifest.json. Re-run "backstage-cli translations export" to regenerate it.'
53
+ );
54
+ }
55
+ const pattern = manifest.pattern;
56
+ const parsePath = messageFilePath.createMessagePathParser(pattern);
57
+ const allFiles = (await collectJsonFiles(inputDir)).filter(
58
+ (f) => f !== "manifest.json"
59
+ );
60
+ const translationsByRef = /* @__PURE__ */ new Map();
61
+ let skipped = 0;
62
+ for (const relPath of allFiles) {
63
+ const parsed = parsePath(relPath);
64
+ if (!parsed) {
65
+ skipped++;
66
+ continue;
67
+ }
68
+ if (parsed.lang === messageFilePath.DEFAULT_LANGUAGE) {
69
+ continue;
70
+ }
71
+ if (!manifest.refs[parsed.id]) {
72
+ console.warn(
73
+ ` Warning: skipping ${relPath} - ref '${parsed.id}' not found in manifest`
74
+ );
75
+ continue;
76
+ }
77
+ const existing = translationsByRef.get(parsed.id) ?? [];
78
+ existing.push({ lang: parsed.lang, relPath });
79
+ translationsByRef.set(parsed.id, existing);
80
+ }
81
+ if (skipped > 0) {
82
+ console.warn(
83
+ ` Warning: ${skipped} file(s) did not match the pattern '${pattern}'`
84
+ );
85
+ }
86
+ if (translationsByRef.size === 0) {
87
+ console.log("No translated message files found.");
88
+ const example = messageFilePath.formatMessagePath(pattern, "<ref-id>", "sv");
89
+ console.log(
90
+ `Add translated files as ${example} in the translations directory.`
91
+ );
92
+ return;
93
+ }
94
+ const importLines = [];
95
+ const resourceLines = [];
96
+ importLines.push(
97
+ "import { createTranslationResource } from '@backstage/frontend-plugin-api';"
98
+ );
99
+ for (const [refId, entries] of [...translationsByRef.entries()].sort(
100
+ ([a], [b]) => a.localeCompare(b)
101
+ )) {
102
+ const refEntry = manifest.refs[refId];
103
+ const importPath = refEntry.exportPath === "." ? refEntry.package : `${refEntry.package}/${refEntry.exportPath.replace(/^\.\//, "")}`;
104
+ importLines.push(`import { ${refEntry.exportName} } from '${importPath}';`);
105
+ const translationEntries = entries.sort((a, b) => a.lang.localeCompare(b.lang)).map(({ lang, relPath }) => {
106
+ const jsonRelPath = node_path.relative(
107
+ node_path.resolve(outputPath, ".."),
108
+ node_path.resolve(inputDir, relPath)
109
+ ).split(node_path.sep).join("/");
110
+ return ` ${JSON.stringify(lang)}: () => import('./${jsonRelPath}'),`;
111
+ }).join("\n");
112
+ resourceLines.push(
113
+ [
114
+ ` createTranslationResource({`,
115
+ ` ref: ${refEntry.exportName},`,
116
+ ` translations: {`,
117
+ translationEntries,
118
+ ` },`,
119
+ ` }),`
120
+ ].join("\n")
121
+ );
122
+ }
123
+ const fileContent = [
124
+ "// This file is auto-generated by backstage-cli translations import",
125
+ "// Do not edit manually.",
126
+ "",
127
+ ...importLines,
128
+ "",
129
+ "export default [",
130
+ ...resourceLines,
131
+ "];",
132
+ ""
133
+ ].join("\n");
134
+ await fs__default.default.ensureDir(node_path.resolve(outputPath, ".."));
135
+ await fs__default.default.writeFile(outputPath, fileContent, "utf8");
136
+ const totalFiles = [...translationsByRef.values()].reduce(
137
+ (sum, e) => sum + e.length,
138
+ 0
139
+ );
140
+ console.log(`Generated translation resources at ${options.output}`);
141
+ console.log(
142
+ ` ${translationsByRef.size} ref(s), ${totalFiles} translation file(s)`
143
+ );
144
+ console.log(
145
+ "\nImport this file in your app and pass the resources to your translation API setup."
146
+ );
147
+ };
148
+ async function collectJsonFiles(dir, prefix = "") {
149
+ const entries = await fs__default.default.readdir(dir, { withFileTypes: true });
150
+ const results = [];
151
+ for (const entry of entries) {
152
+ const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
153
+ if (entry.isDirectory()) {
154
+ results.push(
155
+ ...await collectJsonFiles(node_path.resolve(dir, entry.name), relPath)
156
+ );
157
+ } else if (entry.isFile() && entry.name.endsWith(".json")) {
158
+ results.push(relPath);
159
+ }
160
+ }
161
+ return results;
162
+ }
163
+
164
+ exports.default = _import;
165
+ //# sourceMappingURL=import.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"import.cjs.js","sources":["../../src/commands/import.ts"],"sourcesContent":["/*\n * Copyright 2026 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { cli } from 'cleye';\nimport { targetPaths } from '@backstage/cli-common';\nimport fs from 'fs-extra';\nimport {\n resolve as resolvePath,\n relative as relativePath,\n sep,\n} from 'node:path';\nimport { readTargetPackage } from '../lib/discoverPackages';\nimport {\n DEFAULT_LANGUAGE,\n createMessagePathParser,\n formatMessagePath,\n} from '../lib/messageFilePath';\nimport type { CliCommandContext } from '@backstage/cli-node';\n\ninterface ManifestRefEntry {\n package: string;\n exportPath: string;\n exportName: string;\n}\n\ninterface Manifest {\n pattern?: string;\n refs: Record<string, ManifestRefEntry>;\n}\n\nexport default async ({ args, info }: CliCommandContext) => {\n const {\n flags: { input, output },\n } = cli(\n {\n help: info,\n booleanFlagNegation: true,\n flags: {\n input: {\n type: String,\n default: 'translations',\n description:\n 'Input directory containing the manifest and translated message files',\n },\n output: {\n type: String,\n default: 'src/translations/resources.ts',\n description: 'Output path for the generated wiring module',\n },\n },\n },\n undefined,\n args,\n );\n\n const options = { input, output };\n await readTargetPackage(targetPaths.dir, targetPaths.rootDir);\n\n const inputDir = resolvePath(targetPaths.dir, options.input);\n const manifestPath = resolvePath(inputDir, 'manifest.json');\n const outputPath = resolvePath(targetPaths.dir, options.output);\n\n if (!(await fs.pathExists(manifestPath))) {\n throw new Error(\n `No manifest.json found at ${manifestPath}. ` +\n 'Run \"backstage-cli translations export\" first.',\n );\n }\n\n const manifest: Manifest = await fs.readJson(manifestPath);\n\n if (!manifest.pattern) {\n throw new Error(\n 'No pattern found in manifest.json. Re-run \"backstage-cli translations export\" to regenerate it.',\n );\n }\n const pattern = manifest.pattern;\n\n const parsePath = createMessagePathParser(pattern);\n\n // Discover all JSON files under the translations directory\n const allFiles = (await collectJsonFiles(inputDir)).filter(\n f => f !== 'manifest.json',\n );\n\n // Parse each file to extract id + lang, filtering out default language files\n const translationsByRef = new Map<\n string,\n Array<{ lang: string; relPath: string }>\n >();\n let skipped = 0;\n\n for (const relPath of allFiles) {\n const parsed = parsePath(relPath);\n if (!parsed) {\n skipped++;\n continue;\n }\n\n if (parsed.lang === DEFAULT_LANGUAGE) {\n continue;\n }\n\n if (!manifest.refs[parsed.id]) {\n console.warn(\n ` Warning: skipping ${relPath} - ref '${parsed.id}' not found in manifest`,\n );\n continue;\n }\n\n const existing = translationsByRef.get(parsed.id) ?? [];\n existing.push({ lang: parsed.lang, relPath });\n translationsByRef.set(parsed.id, existing);\n }\n\n if (skipped > 0) {\n console.warn(\n ` Warning: ${skipped} file(s) did not match the pattern '${pattern}'`,\n );\n }\n\n if (translationsByRef.size === 0) {\n console.log('No translated message files found.');\n const example = formatMessagePath(pattern, '<ref-id>', 'sv');\n console.log(\n `Add translated files as ${example} in the translations directory.`,\n );\n return;\n }\n\n // Generate the wiring module\n const importLines: string[] = [];\n const resourceLines: string[] = [];\n\n importLines.push(\n \"import { createTranslationResource } from '@backstage/frontend-plugin-api';\",\n );\n\n for (const [refId, entries] of [...translationsByRef.entries()].sort(\n ([a], [b]) => a.localeCompare(b),\n )) {\n const refEntry = manifest.refs[refId];\n const importPath =\n refEntry.exportPath === '.'\n ? refEntry.package\n : `${refEntry.package}/${refEntry.exportPath.replace(/^\\.\\//, '')}`;\n\n importLines.push(`import { ${refEntry.exportName} } from '${importPath}';`);\n\n const translationEntries = entries\n .sort((a, b) => a.lang.localeCompare(b.lang))\n .map(({ lang, relPath }) => {\n const jsonRelPath = relativePath(\n resolvePath(outputPath, '..'),\n resolvePath(inputDir, relPath),\n )\n .split(sep)\n .join('/');\n return ` ${JSON.stringify(lang)}: () => import('./${jsonRelPath}'),`;\n })\n .join('\\n');\n\n resourceLines.push(\n [\n ` createTranslationResource({`,\n ` ref: ${refEntry.exportName},`,\n ` translations: {`,\n translationEntries,\n ` },`,\n ` }),`,\n ].join('\\n'),\n );\n }\n\n const fileContent = [\n '// This file is auto-generated by backstage-cli translations import',\n '// Do not edit manually.',\n '',\n ...importLines,\n '',\n 'export default [',\n ...resourceLines,\n '];',\n '',\n ].join('\\n');\n\n await fs.ensureDir(resolvePath(outputPath, '..'));\n await fs.writeFile(outputPath, fileContent, 'utf8');\n\n const totalFiles = [...translationsByRef.values()].reduce(\n (sum, e) => sum + e.length,\n 0,\n );\n console.log(`Generated translation resources at ${options.output}`);\n console.log(\n ` ${translationsByRef.size} ref(s), ${totalFiles} translation file(s)`,\n );\n console.log(\n '\\nImport this file in your app and pass the resources to your translation API setup.',\n );\n};\n\n/**\n * Recursively collects all .json files under a directory, returning paths\n * relative to that directory using forward slashes.\n */\nasync function collectJsonFiles(dir: string, prefix = ''): Promise<string[]> {\n const entries = await fs.readdir(dir, { withFileTypes: true });\n const results: string[] = [];\n\n for (const entry of entries) {\n const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;\n if (entry.isDirectory()) {\n results.push(\n ...(await collectJsonFiles(resolvePath(dir, entry.name), relPath)),\n );\n } else if (entry.isFile() && entry.name.endsWith('.json')) {\n results.push(relPath);\n }\n }\n\n return results;\n}\n"],"names":["cli","readTargetPackage","targetPaths","resolvePath","fs","createMessagePathParser","DEFAULT_LANGUAGE","formatMessagePath","relativePath","sep"],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var cliNode = require('@backstage/cli-node');
6
+ var _package = require('./package.json.cjs.js');
7
+
8
+ var index = cliNode.createCliModule({
9
+ packageJson: _package.default,
10
+ init: async (reg) => {
11
+ reg.addCommand({
12
+ path: ["translations", "export"],
13
+ description: "Export translation messages from an app and all of its frontend plugins to JSON files",
14
+ execute: { loader: () => import('./commands/export.cjs.js') }
15
+ });
16
+ reg.addCommand({
17
+ path: ["translations", "import"],
18
+ description: "Generate translation resource wiring from translated JSON files",
19
+ execute: { loader: () => import('./commands/import.cjs.js') }
20
+ });
21
+ }
22
+ });
23
+
24
+ exports.default = index;
25
+ //# sourceMappingURL=index.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs.js","sources":["../src/index.ts"],"sourcesContent":["/*\n * Copyright 2026 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { createCliModule } from '@backstage/cli-node';\nimport packageJson from '../package.json';\n\nexport default createCliModule({\n packageJson,\n init: async reg => {\n reg.addCommand({\n path: ['translations', 'export'],\n description:\n 'Export translation messages from an app and all of its frontend plugins to JSON files',\n execute: { loader: () => import('./commands/export') },\n });\n\n reg.addCommand({\n path: ['translations', 'import'],\n description:\n 'Generate translation resource wiring from translated JSON files',\n execute: { loader: () => import('./commands/import') },\n });\n },\n});\n"],"names":["createCliModule","packageJson"],"mappings":";;;;;;;AAkBA,YAAeA,uBAAA,CAAgB;AAAA,eAC7BC,gBAAA;AAAA,EACA,IAAA,EAAM,OAAM,GAAA,KAAO;AACjB,IAAA,GAAA,CAAI,UAAA,CAAW;AAAA,MACb,IAAA,EAAM,CAAC,cAAA,EAAgB,QAAQ,CAAA;AAAA,MAC/B,WAAA,EACE,uFAAA;AAAA,MACF,SAAS,EAAE,MAAA,EAAQ,MAAM,OAAO,0BAAmB,CAAA;AAAE,KACtD,CAAA;AAED,IAAA,GAAA,CAAI,UAAA,CAAW;AAAA,MACb,IAAA,EAAM,CAAC,cAAA,EAAgB,QAAQ,CAAA;AAAA,MAC/B,WAAA,EACE,iEAAA;AAAA,MACF,SAAS,EAAE,MAAA,EAAQ,MAAM,OAAO,0BAAmB,CAAA;AAAE,KACtD,CAAA;AAAA,EACH;AACF,CAAC,CAAA;;;;"}
@@ -0,0 +1,5 @@
1
+ import * as _backstage_cli_node from '@backstage/cli-node';
2
+
3
+ declare const _default: _backstage_cli_node.CliModule;
4
+
5
+ export { _default as default };
@@ -0,0 +1,121 @@
1
+ 'use strict';
2
+
3
+ var cliNode = require('@backstage/cli-node');
4
+ var node_path = require('node:path');
5
+ var fs = require('fs-extra');
6
+
7
+ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
8
+
9
+ var fs__default = /*#__PURE__*/_interopDefaultCompat(fs);
10
+
11
+ async function readTargetPackage(packageDir, repoRoot) {
12
+ const packageJsonPath = node_path.resolve(packageDir, "package.json");
13
+ if (!await fs__default.default.pathExists(packageJsonPath)) {
14
+ throw new Error(
15
+ "No package.json found in the current directory. The translations commands must be run from within a package directory."
16
+ );
17
+ }
18
+ if (node_path.resolve(packageDir) === node_path.resolve(repoRoot)) {
19
+ throw new Error(
20
+ "The translations commands must be run from within a package directory, not from the repository root. For example: cd packages/app && backstage-cli translations export"
21
+ );
22
+ }
23
+ return fs__default.default.readJson(packageJsonPath);
24
+ }
25
+ async function discoverFrontendPackages(targetPackageJson, targetDir) {
26
+ let workspaceByName;
27
+ try {
28
+ const workspacePackages = await cliNode.PackageGraph.listTargetPackages();
29
+ workspaceByName = new Map(
30
+ workspacePackages.map((p) => [p.packageJson.name, p])
31
+ );
32
+ } catch {
33
+ workspaceByName = /* @__PURE__ */ new Map();
34
+ }
35
+ const visited = /* @__PURE__ */ new Set();
36
+ const result = [];
37
+ async function visit(packageJson, pkgDir, includeDevDeps) {
38
+ const deps = {
39
+ ...packageJson.dependencies,
40
+ ...includeDevDeps ? packageJson.devDependencies ?? {} : {}
41
+ };
42
+ for (const depName of Object.keys(deps)) {
43
+ if (visited.has(depName)) {
44
+ continue;
45
+ }
46
+ visited.add(depName);
47
+ let depPkgJson;
48
+ let depDir;
49
+ let isWorkspace;
50
+ const workspacePkg = workspaceByName.get(depName);
51
+ if (workspacePkg) {
52
+ depPkgJson = workspacePkg.packageJson;
53
+ depDir = workspacePkg.dir;
54
+ isWorkspace = true;
55
+ } else {
56
+ try {
57
+ const pkgJsonPath = require.resolve(`${depName}/package.json`, {
58
+ paths: [pkgDir]
59
+ });
60
+ depPkgJson = await fs__default.default.readJson(pkgJsonPath);
61
+ depDir = node_path.dirname(pkgJsonPath);
62
+ isWorkspace = false;
63
+ } catch {
64
+ continue;
65
+ }
66
+ }
67
+ if (!depPkgJson.backstage) {
68
+ continue;
69
+ }
70
+ const role = depPkgJson.backstage?.role;
71
+ if (role && isFrontendRole(role)) {
72
+ const entryPoints = resolveEntryPoints(depPkgJson, depDir, isWorkspace);
73
+ if (entryPoints.size > 0) {
74
+ result.push({ name: depName, dir: depDir, entryPoints });
75
+ }
76
+ }
77
+ await visit(depPkgJson, depDir, false);
78
+ }
79
+ }
80
+ await visit(targetPackageJson, targetDir, true);
81
+ return result;
82
+ }
83
+ function resolveEntryPoints(packageJson, packageDir, isWorkspace) {
84
+ const entryPoints = /* @__PURE__ */ new Map();
85
+ const exports$1 = packageJson.exports;
86
+ if (exports$1) {
87
+ for (const [subpath, target] of Object.entries(exports$1)) {
88
+ if (subpath === "./package.json") {
89
+ continue;
90
+ }
91
+ let filePath;
92
+ if (typeof target === "string") {
93
+ filePath = target;
94
+ } else if (isWorkspace) {
95
+ filePath = target?.import ?? target?.types ?? target?.default;
96
+ } else {
97
+ filePath = target?.types ?? target?.import ?? target?.default;
98
+ }
99
+ if (typeof filePath === "string") {
100
+ entryPoints.set(subpath, node_path.resolve(packageDir, filePath));
101
+ }
102
+ }
103
+ } else {
104
+ const main = isWorkspace ? packageJson.main ?? packageJson.types : packageJson.types ?? packageJson.main;
105
+ if (main) {
106
+ entryPoints.set(".", node_path.resolve(packageDir, main));
107
+ }
108
+ }
109
+ return entryPoints;
110
+ }
111
+ function isFrontendRole(role) {
112
+ try {
113
+ return cliNode.PackageRoles.getRoleInfo(role).platform === "web";
114
+ } catch {
115
+ return false;
116
+ }
117
+ }
118
+
119
+ exports.discoverFrontendPackages = discoverFrontendPackages;
120
+ exports.readTargetPackage = readTargetPackage;
121
+ //# sourceMappingURL=discoverPackages.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discoverPackages.cjs.js","sources":["../../src/lib/discoverPackages.ts"],"sourcesContent":["/*\n * Copyright 2026 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n BackstagePackageJson,\n PackageGraph,\n PackageRoles,\n} from '@backstage/cli-node';\nimport { dirname, resolve as resolvePath } from 'node:path';\nimport fs from 'fs-extra';\n\n/** A discovered package with its entry points resolved to file paths. */\nexport interface DiscoveredPackage {\n /** The package name, e.g. '@backstage/plugin-org' */\n name: string;\n /** The directory of the package */\n dir: string;\n /** Map of export subpath (e.g. '.', './alpha') to the resolved file path */\n entryPoints: Map<string, string>;\n}\n\n/**\n * Reads the package.json from the given directory and validates that it\n * is a workspace package (not the repo root).\n */\nexport async function readTargetPackage(\n packageDir: string,\n repoRoot: string,\n): Promise<BackstagePackageJson> {\n const packageJsonPath = resolvePath(packageDir, 'package.json');\n\n if (!(await fs.pathExists(packageJsonPath))) {\n throw new Error(\n 'No package.json found in the current directory. ' +\n 'The translations commands must be run from within a package directory.',\n );\n }\n\n if (resolvePath(packageDir) === resolvePath(repoRoot)) {\n throw new Error(\n 'The translations commands must be run from within a package directory, ' +\n 'not from the repository root. For example: cd packages/app && backstage-cli translations export',\n );\n }\n\n return fs.readJson(packageJsonPath);\n}\n\n/**\n * Discovers frontend packages that are transitive dependencies of the given\n * target package and resolves their entry point file paths. Walks both\n * workspace packages (source) and npm-installed packages (declaration files).\n */\nexport async function discoverFrontendPackages(\n targetPackageJson: BackstagePackageJson,\n targetDir: string,\n): Promise<DiscoveredPackage[]> {\n // Build a lookup of workspace packages for preferring source over dist\n let workspaceByName: Map<\n string,\n { packageJson: BackstagePackageJson; dir: string }\n >;\n try {\n const workspacePackages = await PackageGraph.listTargetPackages();\n workspaceByName = new Map(\n workspacePackages.map(p => [p.packageJson.name, p]),\n );\n } catch {\n workspaceByName = new Map();\n }\n\n const visited = new Set<string>();\n const result: DiscoveredPackage[] = [];\n\n async function visit(\n packageJson: BackstagePackageJson,\n pkgDir: string,\n includeDevDeps: boolean,\n ) {\n const deps: Record<string, string> = {\n ...packageJson.dependencies,\n ...(includeDevDeps ? packageJson.devDependencies ?? {} : {}),\n };\n\n for (const depName of Object.keys(deps)) {\n if (visited.has(depName)) {\n continue;\n }\n visited.add(depName);\n\n let depPkgJson: BackstagePackageJson;\n let depDir: string;\n let isWorkspace: boolean;\n\n // Prefer workspace package (has source files) over npm-installed\n const workspacePkg = workspaceByName.get(depName);\n if (workspacePkg) {\n depPkgJson = workspacePkg.packageJson;\n depDir = workspacePkg.dir;\n isWorkspace = true;\n } else {\n try {\n const pkgJsonPath = require.resolve(`${depName}/package.json`, {\n paths: [pkgDir],\n });\n depPkgJson = await fs.readJson(pkgJsonPath);\n depDir = dirname(pkgJsonPath);\n isWorkspace = false;\n } catch {\n continue;\n }\n }\n\n // Only recurse into Backstage ecosystem packages\n if (!depPkgJson.backstage) {\n continue;\n }\n\n const role = depPkgJson.backstage?.role;\n if (role && isFrontendRole(role)) {\n const entryPoints = resolveEntryPoints(depPkgJson, depDir, isWorkspace);\n if (entryPoints.size > 0) {\n result.push({ name: depName, dir: depDir, entryPoints });\n }\n }\n\n // Walk this package's production dependencies for transitive refs\n await visit(depPkgJson, depDir, false);\n }\n }\n\n // Start from the target, including its devDependencies\n await visit(targetPackageJson, targetDir, true);\n\n return result;\n}\n\n/**\n * Resolves the entry points of a package to absolute file paths.\n * For workspace packages, prefers source entry points (import/default).\n * For npm packages, prefers type declaration entry points (.d.ts).\n */\nfunction resolveEntryPoints(\n packageJson: BackstagePackageJson,\n packageDir: string,\n isWorkspace: boolean,\n): Map<string, string> {\n const entryPoints = new Map<string, string>();\n\n const exports = (packageJson as any).exports as\n | Record<string, string | Record<string, string>>\n | undefined;\n\n if (exports) {\n for (const [subpath, target] of Object.entries(exports)) {\n if (subpath === './package.json') {\n continue;\n }\n\n let filePath: string | undefined;\n if (typeof target === 'string') {\n filePath = target;\n } else if (isWorkspace) {\n // Workspace: exports point to source .ts files\n filePath = target?.import ?? target?.types ?? target?.default;\n } else {\n // npm: prefer .d.ts for type-based extraction\n filePath = target?.types ?? target?.import ?? target?.default;\n }\n\n if (typeof filePath === 'string') {\n entryPoints.set(subpath, resolvePath(packageDir, filePath));\n }\n }\n } else {\n // Fallback: prefer types for npm, source for workspace\n const main = isWorkspace\n ? packageJson.main ?? packageJson.types\n : packageJson.types ?? packageJson.main;\n if (main) {\n entryPoints.set('.', resolvePath(packageDir, main));\n }\n }\n\n return entryPoints;\n}\n\nfunction isFrontendRole(role: string): boolean {\n try {\n return PackageRoles.getRoleInfo(role).platform === 'web';\n } catch {\n return false;\n }\n}\n"],"names":["resolvePath","fs","PackageGraph","dirname","exports","PackageRoles"],"mappings":"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"}
@@ -0,0 +1,71 @@
1
+ 'use strict';
2
+
3
+ var tsMorph = require('ts-morph');
4
+
5
+ function extractTranslationRefsFromSourceFile(sourceFile, packageName, exportPath) {
6
+ const results = [];
7
+ for (const exportSymbol of sourceFile.getExportSymbols()) {
8
+ const declarations = exportSymbol.getDeclarations();
9
+ if (declarations.length === 0) {
10
+ continue;
11
+ }
12
+ const declaration = declarations[0];
13
+ const exportType = declaration.getType();
14
+ const refInfo = extractTranslationRefFromType(exportType, declaration);
15
+ if (!refInfo) {
16
+ continue;
17
+ }
18
+ results.push({
19
+ ...refInfo,
20
+ packageName,
21
+ exportPath,
22
+ exportName: exportSymbol.getName()
23
+ });
24
+ }
25
+ return results;
26
+ }
27
+ function extractTranslationRefFromType(type, declaration) {
28
+ const resolvedType = type.getTargetType() ?? type;
29
+ const $$typeProperty = resolvedType.getProperties().find((p) => p.getName() === "$$type");
30
+ if (!$$typeProperty) {
31
+ return void 0;
32
+ }
33
+ const $$typeDecl = $$typeProperty.getValueDeclaration();
34
+ if (!$$typeDecl) {
35
+ return void 0;
36
+ }
37
+ if (!$$typeDecl.getText().includes("'@backstage/TranslationRef'")) {
38
+ return void 0;
39
+ }
40
+ const typeArgs = type.getTypeArguments();
41
+ if (typeArgs.length < 2) {
42
+ return void 0;
43
+ }
44
+ const [idType, messagesType] = typeArgs;
45
+ if (!idType.isStringLiteral()) {
46
+ return void 0;
47
+ }
48
+ const id = idType.getLiteralValueOrThrow();
49
+ const messages = {};
50
+ for (const messageProp of messagesType.getProperties()) {
51
+ const key = messageProp.getName();
52
+ const propType = messageProp.getTypeAtLocation(declaration);
53
+ if (propType.isStringLiteral()) {
54
+ messages[key] = propType.getLiteralValueOrThrow();
55
+ }
56
+ }
57
+ if (Object.keys(messages).length === 0) {
58
+ return void 0;
59
+ }
60
+ return { id, messages };
61
+ }
62
+ function createTranslationProject(tsconfigPath) {
63
+ return new tsMorph.Project({
64
+ tsConfigFilePath: tsconfigPath,
65
+ skipAddingFilesFromTsConfig: true
66
+ });
67
+ }
68
+
69
+ exports.createTranslationProject = createTranslationProject;
70
+ exports.extractTranslationRefsFromSourceFile = extractTranslationRefsFromSourceFile;
71
+ //# sourceMappingURL=extractTranslations.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extractTranslations.cjs.js","sources":["../../src/lib/extractTranslations.ts"],"sourcesContent":["/*\n * Copyright 2026 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Node, Project, SourceFile, Type, ts } from 'ts-morph';\n\n/** Information about a discovered translation ref. */\nexport interface TranslationRefInfo {\n /** The ref ID, e.g. 'org' */\n id: string;\n /** The package name, e.g. '@backstage/plugin-org' */\n packageName: string;\n /** The subpath export where this ref is accessible, e.g. './alpha' or '.' */\n exportPath: string;\n /** The exported symbol name, e.g. 'orgTranslationRef' */\n exportName: string;\n /** Flattened message map: key -> default message string */\n messages: Record<string, string>;\n}\n\n/**\n * Given a ts-morph SourceFile, finds all exported TranslationRef symbols\n * and extracts their id and messages from the type system.\n */\nexport function extractTranslationRefsFromSourceFile(\n sourceFile: SourceFile,\n packageName: string,\n exportPath: string,\n): TranslationRefInfo[] {\n const results: TranslationRefInfo[] = [];\n\n for (const exportSymbol of sourceFile.getExportSymbols()) {\n const declarations = exportSymbol.getDeclarations();\n if (declarations.length === 0) {\n continue;\n }\n\n const declaration = declarations[0];\n const exportType = declaration.getType();\n\n const refInfo = extractTranslationRefFromType(exportType, declaration);\n if (!refInfo) {\n continue;\n }\n\n results.push({\n ...refInfo,\n packageName,\n exportPath,\n exportName: exportSymbol.getName(),\n });\n }\n\n return results;\n}\n\n/**\n * Checks whether a type is a TranslationRef by inspecting the $$type\n * property on the target type, then extracts the id and messages from\n * the type arguments of the generic instantiation.\n */\nfunction extractTranslationRefFromType(\n type: Type<ts.Type>,\n declaration: Node,\n): Pick<TranslationRefInfo, 'id' | 'messages'> | undefined {\n // Check the $$type property on the uninstantiated (target) type\n const resolvedType = type.getTargetType() ?? type;\n const $$typeProperty = resolvedType\n .getProperties()\n .find(p => p.getName() === '$$type');\n if (!$$typeProperty) {\n return undefined;\n }\n const $$typeDecl = $$typeProperty.getValueDeclaration();\n if (!$$typeDecl) {\n return undefined;\n }\n if (!$$typeDecl.getText().includes(\"'@backstage/TranslationRef'\")) {\n return undefined;\n }\n\n // The type is TranslationRef<TId, TMessages> - extract the type arguments\n const typeArgs = type.getTypeArguments();\n if (typeArgs.length < 2) {\n return undefined;\n }\n\n const [idType, messagesType] = typeArgs;\n\n if (!idType.isStringLiteral()) {\n return undefined;\n }\n const id = idType.getLiteralValueOrThrow() as string;\n\n // Extract messages from the TMessages type argument\n const messages: Record<string, string> = {};\n for (const messageProp of messagesType.getProperties()) {\n const key = messageProp.getName();\n // Resolve the property type in the context of the declaration\n const propType = messageProp.getTypeAtLocation(declaration);\n if (propType.isStringLiteral()) {\n messages[key] = propType.getLiteralValueOrThrow() as string;\n }\n }\n\n if (Object.keys(messages).length === 0) {\n return undefined;\n }\n\n return { id, messages };\n}\n\n/**\n * Creates a ts-morph Project using the target repo's tsconfig.json.\n */\nexport function createTranslationProject(tsconfigPath: string): Project {\n return new Project({\n tsConfigFilePath: tsconfigPath,\n skipAddingFilesFromTsConfig: true,\n });\n}\n"],"names":["Project"],"mappings":";;;;AAoCO,SAAS,oCAAA,CACd,UAAA,EACA,WAAA,EACA,UAAA,EACsB;AACtB,EAAA,MAAM,UAAgC,EAAC;AAEvC,EAAA,KAAA,MAAW,YAAA,IAAgB,UAAA,CAAW,gBAAA,EAAiB,EAAG;AACxD,IAAA,MAAM,YAAA,GAAe,aAAa,eAAA,EAAgB;AAClD,IAAA,IAAI,YAAA,CAAa,WAAW,CAAA,EAAG;AAC7B,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,WAAA,GAAc,aAAa,CAAC,CAAA;AAClC,IAAA,MAAM,UAAA,GAAa,YAAY,OAAA,EAAQ;AAEvC,IAAA,MAAM,OAAA,GAAU,6BAAA,CAA8B,UAAA,EAAY,WAAW,CAAA;AACrE,IAAA,IAAI,CAAC,OAAA,EAAS;AACZ,MAAA;AAAA,IACF;AAEA,IAAA,OAAA,CAAQ,IAAA,CAAK;AAAA,MACX,GAAG,OAAA;AAAA,MACH,WAAA;AAAA,MACA,UAAA;AAAA,MACA,UAAA,EAAY,aAAa,OAAA;AAAQ,KAClC,CAAA;AAAA,EACH;AAEA,EAAA,OAAO,OAAA;AACT;AAOA,SAAS,6BAAA,CACP,MACA,WAAA,EACyD;AAEzD,EAAA,MAAM,YAAA,GAAe,IAAA,CAAK,aAAA,EAAc,IAAK,IAAA;AAC7C,EAAA,MAAM,cAAA,GAAiB,aACpB,aAAA,EAAc,CACd,KAAK,CAAA,CAAA,KAAK,CAAA,CAAE,OAAA,EAAQ,KAAM,QAAQ,CAAA;AACrC,EAAA,IAAI,CAAC,cAAA,EAAgB;AACnB,IAAA,OAAO,MAAA;AAAA,EACT;AACA,EAAA,MAAM,UAAA,GAAa,eAAe,mBAAA,EAAoB;AACtD,EAAA,IAAI,CAAC,UAAA,EAAY;AACf,IAAA,OAAO,MAAA;AAAA,EACT;AACA,EAAA,IAAI,CAAC,UAAA,CAAW,OAAA,EAAQ,CAAE,QAAA,CAAS,6BAA6B,CAAA,EAAG;AACjE,IAAA,OAAO,MAAA;AAAA,EACT;AAGA,EAAA,MAAM,QAAA,GAAW,KAAK,gBAAA,EAAiB;AACvC,EAAA,IAAI,QAAA,CAAS,SAAS,CAAA,EAAG;AACvB,IAAA,OAAO,MAAA;AAAA,EACT;AAEA,EAAA,MAAM,CAAC,MAAA,EAAQ,YAAY,CAAA,GAAI,QAAA;AAE/B,EAAA,IAAI,CAAC,MAAA,CAAO,eAAA,EAAgB,EAAG;AAC7B,IAAA,OAAO,MAAA;AAAA,EACT;AACA,EAAA,MAAM,EAAA,GAAK,OAAO,sBAAA,EAAuB;AAGzC,EAAA,MAAM,WAAmC,EAAC;AAC1C,EAAA,KAAA,MAAW,WAAA,IAAe,YAAA,CAAa,aAAA,EAAc,EAAG;AACtD,IAAA,MAAM,GAAA,GAAM,YAAY,OAAA,EAAQ;AAEhC,IAAA,MAAM,QAAA,GAAW,WAAA,CAAY,iBAAA,CAAkB,WAAW,CAAA;AAC1D,IAAA,IAAI,QAAA,CAAS,iBAAgB,EAAG;AAC9B,MAAA,QAAA,CAAS,GAAG,CAAA,GAAI,QAAA,CAAS,sBAAA,EAAuB;AAAA,IAClD;AAAA,EACF;AAEA,EAAA,IAAI,MAAA,CAAO,IAAA,CAAK,QAAQ,CAAA,CAAE,WAAW,CAAA,EAAG;AACtC,IAAA,OAAO,MAAA;AAAA,EACT;AAEA,EAAA,OAAO,EAAE,IAAI,QAAA,EAAS;AACxB;AAKO,SAAS,yBAAyB,YAAA,EAA+B;AACtE,EAAA,OAAO,IAAIA,eAAA,CAAQ;AAAA,IACjB,gBAAA,EAAkB,YAAA;AAAA,IAClB,2BAAA,EAA6B;AAAA,GAC9B,CAAA;AACH;;;;;"}
@@ -0,0 +1,43 @@
1
+ 'use strict';
2
+
3
+ const DEFAULT_LANGUAGE = "en";
4
+ const DEFAULT_MESSAGE_PATTERN = "messages/{id}.{lang}.json";
5
+ function formatMessagePath(pattern, id, lang) {
6
+ return pattern.replace(/\{id\}/g, id).replace(/\{lang\}/g, lang);
7
+ }
8
+ function createMessagePathParser(pattern) {
9
+ validatePattern(pattern);
10
+ const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\{id\\}/g, "(?<id>[^/]+)").replace(/\\{lang\\}/g, "(?<lang>[a-z]{2})");
11
+ const regex = new RegExp(`^${escaped}$`);
12
+ return (relPath) => {
13
+ const match = relPath.match(regex);
14
+ if (!match?.groups) {
15
+ return void 0;
16
+ }
17
+ return { id: match.groups.id, lang: match.groups.lang };
18
+ };
19
+ }
20
+ function validatePattern(pattern) {
21
+ if (!pattern.includes("{id}")) {
22
+ throw new Error(
23
+ `Invalid message file pattern: must contain {id} placeholder. Got: ${pattern}`
24
+ );
25
+ }
26
+ if (!pattern.includes("{lang}")) {
27
+ throw new Error(
28
+ `Invalid message file pattern: must contain {lang} placeholder. Got: ${pattern}`
29
+ );
30
+ }
31
+ if (!pattern.endsWith(".json")) {
32
+ throw new Error(
33
+ `Invalid message file pattern: must end with .json. Got: ${pattern}`
34
+ );
35
+ }
36
+ }
37
+
38
+ exports.DEFAULT_LANGUAGE = DEFAULT_LANGUAGE;
39
+ exports.DEFAULT_MESSAGE_PATTERN = DEFAULT_MESSAGE_PATTERN;
40
+ exports.createMessagePathParser = createMessagePathParser;
41
+ exports.formatMessagePath = formatMessagePath;
42
+ exports.validatePattern = validatePattern;
43
+ //# sourceMappingURL=messageFilePath.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"messageFilePath.cjs.js","sources":["../../src/lib/messageFilePath.ts"],"sourcesContent":["/*\n * Copyright 2026 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// The default language for exported translation messages.\nexport const DEFAULT_LANGUAGE = 'en';\n\n// Default file path pattern for translation message files relative to the\n// translations directory. Supported placeholders: {id} and {lang}.\nexport const DEFAULT_MESSAGE_PATTERN = 'messages/{id}.{lang}.json';\n\n/** Formats a message file pattern into a concrete relative path. */\nexport function formatMessagePath(\n pattern: string,\n id: string,\n lang: string,\n): string {\n return pattern.replace(/\\{id\\}/g, id).replace(/\\{lang\\}/g, lang);\n}\n\n/** Creates a parser that extracts id and lang from a relative file path. */\nexport function createMessagePathParser(\n pattern: string,\n): (relativePath: string) => { id: string; lang: string } | undefined {\n validatePattern(pattern);\n\n // Build a regex from the pattern by escaping special chars and replacing\n // {id} and {lang} with named capture groups.\n const escaped = pattern\n .replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n .replace(/\\\\{id\\\\}/g, '(?<id>[^/]+)')\n .replace(/\\\\{lang\\\\}/g, '(?<lang>[a-z]{2})');\n\n const regex = new RegExp(`^${escaped}$`);\n\n return (relPath: string) => {\n const match = relPath.match(regex);\n if (!match?.groups) {\n return undefined;\n }\n return { id: match.groups.id, lang: match.groups.lang };\n };\n}\n\n/** Converts a message pattern into a glob string for discovering files. */\nexport function messagePatternToGlob(pattern: string): string {\n return pattern.replace(/\\{id\\}/g, '*').replace(/\\{lang\\}/g, '*');\n}\n\n/** Returns whether the pattern produces paths with subdirectories. */\nexport function patternHasSubdirectories(pattern: string): boolean {\n return pattern.includes('/');\n}\n\nexport function validatePattern(pattern: string) {\n if (!pattern.includes('{id}')) {\n throw new Error(\n `Invalid message file pattern: must contain {id} placeholder. Got: ${pattern}`,\n );\n }\n if (!pattern.includes('{lang}')) {\n throw new Error(\n `Invalid message file pattern: must contain {lang} placeholder. Got: ${pattern}`,\n );\n }\n if (!pattern.endsWith('.json')) {\n throw new Error(\n `Invalid message file pattern: must end with .json. Got: ${pattern}`,\n );\n }\n}\n"],"names":[],"mappings":";;AAiBO,MAAM,gBAAA,GAAmB;AAIzB,MAAM,uBAAA,GAA0B;AAGhC,SAAS,iBAAA,CACd,OAAA,EACA,EAAA,EACA,IAAA,EACQ;AACR,EAAA,OAAO,QAAQ,OAAA,CAAQ,SAAA,EAAW,EAAE,CAAA,CAAE,OAAA,CAAQ,aAAa,IAAI,CAAA;AACjE;AAGO,SAAS,wBACd,OAAA,EACoE;AACpE,EAAA,eAAA,CAAgB,OAAO,CAAA;AAIvB,EAAA,MAAM,OAAA,GAAU,OAAA,CACb,OAAA,CAAQ,qBAAA,EAAuB,MAAM,CAAA,CACrC,OAAA,CAAQ,WAAA,EAAa,cAAc,CAAA,CACnC,OAAA,CAAQ,aAAA,EAAe,mBAAmB,CAAA;AAE7C,EAAA,MAAM,KAAA,GAAQ,IAAI,MAAA,CAAO,CAAA,CAAA,EAAI,OAAO,CAAA,CAAA,CAAG,CAAA;AAEvC,EAAA,OAAO,CAAC,OAAA,KAAoB;AAC1B,IAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,KAAA,CAAM,KAAK,CAAA;AACjC,IAAA,IAAI,CAAC,OAAO,MAAA,EAAQ;AAClB,MAAA,OAAO,MAAA;AAAA,IACT;AACA,IAAA,OAAO,EAAE,IAAI,KAAA,CAAM,MAAA,CAAO,IAAI,IAAA,EAAM,KAAA,CAAM,OAAO,IAAA,EAAK;AAAA,EACxD,CAAA;AACF;AAYO,SAAS,gBAAgB,OAAA,EAAiB;AAC/C,EAAA,IAAI,CAAC,OAAA,CAAQ,QAAA,CAAS,MAAM,CAAA,EAAG;AAC7B,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,qEAAqE,OAAO,CAAA;AAAA,KAC9E;AAAA,EACF;AACA,EAAA,IAAI,CAAC,OAAA,CAAQ,QAAA,CAAS,QAAQ,CAAA,EAAG;AAC/B,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,uEAAuE,OAAO,CAAA;AAAA,KAChF;AAAA,EACF;AACA,EAAA,IAAI,CAAC,OAAA,CAAQ,QAAA,CAAS,OAAO,CAAA,EAAG;AAC9B,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,2DAA2D,OAAO,CAAA;AAAA,KACpE;AAAA,EACF;AACF;;;;;;;;"}
@@ -0,0 +1,83 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var name = "@backstage/cli-module-translations";
6
+ var version = "0.0.0-nightly-20260317031259";
7
+ var description = "CLI module for Backstage CLI";
8
+ var backstage = {
9
+ role: "cli-module"
10
+ };
11
+ var publishConfig = {
12
+ access: "public",
13
+ main: "dist/index.cjs.js",
14
+ types: "dist/index.d.ts"
15
+ };
16
+ var homepage = "https://backstage.io";
17
+ var repository = {
18
+ type: "git",
19
+ url: "https://github.com/backstage/backstage",
20
+ directory: "packages/cli-module-translations"
21
+ };
22
+ var license = "Apache-2.0";
23
+ var main = "src/index.ts";
24
+ var types = "src/index.ts";
25
+ var files = [
26
+ "dist",
27
+ "bin"
28
+ ];
29
+ var scripts = {
30
+ build: "backstage-cli package build",
31
+ clean: "backstage-cli package clean",
32
+ lint: "backstage-cli package lint",
33
+ prepack: "backstage-cli package prepack",
34
+ postpack: "backstage-cli package postpack",
35
+ test: "backstage-cli package test"
36
+ };
37
+ var dependencies = {
38
+ "@backstage/cli-common": "workspace:*",
39
+ "@backstage/cli-node": "workspace:*",
40
+ cleye: "^2.3.0",
41
+ "fs-extra": "^11.2.0",
42
+ "ts-morph": "^24.0.0"
43
+ };
44
+ var devDependencies = {
45
+ "@backstage/cli": "workspace:*",
46
+ "@types/fs-extra": "^11.0.0"
47
+ };
48
+ var bin = "bin/backstage-cli-module-translations";
49
+ var packageJson = {
50
+ name: name,
51
+ version: version,
52
+ description: description,
53
+ backstage: backstage,
54
+ publishConfig: publishConfig,
55
+ homepage: homepage,
56
+ repository: repository,
57
+ license: license,
58
+ main: main,
59
+ types: types,
60
+ files: files,
61
+ scripts: scripts,
62
+ dependencies: dependencies,
63
+ devDependencies: devDependencies,
64
+ bin: bin
65
+ };
66
+
67
+ exports.backstage = backstage;
68
+ exports.bin = bin;
69
+ exports.default = packageJson;
70
+ exports.dependencies = dependencies;
71
+ exports.description = description;
72
+ exports.devDependencies = devDependencies;
73
+ exports.files = files;
74
+ exports.homepage = homepage;
75
+ exports.license = license;
76
+ exports.main = main;
77
+ exports.name = name;
78
+ exports.publishConfig = publishConfig;
79
+ exports.repository = repository;
80
+ exports.scripts = scripts;
81
+ exports.types = types;
82
+ exports.version = version;
83
+ //# sourceMappingURL=package.json.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"package.json.cjs.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@backstage/cli-module-translations",
3
+ "version": "0.0.0-nightly-20260317031259",
4
+ "description": "CLI module for Backstage CLI",
5
+ "backstage": {
6
+ "role": "cli-module"
7
+ },
8
+ "publishConfig": {
9
+ "access": "public",
10
+ "main": "dist/index.cjs.js",
11
+ "types": "dist/index.d.ts"
12
+ },
13
+ "homepage": "https://backstage.io",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/backstage/backstage",
17
+ "directory": "packages/cli-module-translations"
18
+ },
19
+ "license": "Apache-2.0",
20
+ "main": "dist/index.cjs.js",
21
+ "types": "dist/index.d.ts",
22
+ "files": [
23
+ "dist",
24
+ "bin"
25
+ ],
26
+ "scripts": {
27
+ "build": "backstage-cli package build",
28
+ "clean": "backstage-cli package clean",
29
+ "lint": "backstage-cli package lint",
30
+ "prepack": "backstage-cli package prepack",
31
+ "postpack": "backstage-cli package postpack",
32
+ "test": "backstage-cli package test"
33
+ },
34
+ "dependencies": {
35
+ "@backstage/cli-common": "0.0.0-nightly-20260317031259",
36
+ "@backstage/cli-node": "0.0.0-nightly-20260317031259",
37
+ "cleye": "^2.3.0",
38
+ "fs-extra": "^11.2.0",
39
+ "ts-morph": "^24.0.0"
40
+ },
41
+ "devDependencies": {
42
+ "@backstage/cli": "0.0.0-nightly-20260317031259",
43
+ "@types/fs-extra": "^11.0.0"
44
+ },
45
+ "bin": "bin/backstage-cli-module-translations",
46
+ "typesVersions": {
47
+ "*": {
48
+ "package.json": [
49
+ "package.json"
50
+ ]
51
+ }
52
+ }
53
+ }