From dd5f0520176199c71a6b9161cd8bf74f19d68937 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 18 Feb 2026 12:32:51 +0100 Subject: [PATCH] cli: make messages/ part of the configurable --pattern flag Moves the messages/ path segment from being hardcoded into the export and import commands to being part of the pattern itself. The default pattern is now messages/{id}.{lang}.json, giving full control over the directory structure under the translations output directory. Signed-off-by: Patrik Oldsberg Co-authored-by: Cursor --- docs/plugins/internationalization.md | 6 +++--- docs/tooling/cli/03-commands.md | 5 +++-- .../modules/translations/commands/export.ts | 12 ++++-------- .../modules/translations/commands/import.ts | 18 ++++++------------ .../translations/lib/messageFilePath.test.ts | 18 ++++++++++-------- .../translations/lib/messageFilePath.ts | 6 +++--- 6 files changed, 29 insertions(+), 36 deletions(-) diff --git a/docs/plugins/internationalization.md b/docs/plugins/internationalization.md index f1d5d8a81d..0c62058e5a 100644 --- a/docs/plugins/internationalization.md +++ b/docs/plugins/internationalization.md @@ -382,7 +382,7 @@ const app = createApp({ #### Custom file patterns -By default, message files use the pattern `{id}.{lang}.json` (e.g. `catalog.en.json`). You can change this with the `--pattern` option: +By default, message files use the pattern `messages/{id}.{lang}.json` (e.g. `messages/catalog.en.json`). You can change this with the `--pattern` option: ```bash yarn backstage-cli translations export --pattern '{lang}/{id}.json' @@ -391,8 +391,8 @@ yarn backstage-cli translations export --pattern '{lang}/{id}.json' This produces a directory structure grouped by language instead: ```text -translations/messages/en/catalog.json -translations/messages/zh/catalog.json +translations/en/catalog.json +translations/zh/catalog.json ``` The pattern is stored in the manifest, so the `import` command automatically uses the same layout. diff --git a/docs/tooling/cli/03-commands.md b/docs/tooling/cli/03-commands.md index efdf197113..44b9058f19 100644 --- a/docs/tooling/cli/03-commands.md +++ b/docs/tooling/cli/03-commands.md @@ -447,8 +447,9 @@ Usage: backstage-cli translations export [options] Options: --output Output directory for exported messages and manifest (default: "translations") - --pattern File path pattern for message files, with {id} and {lang} - placeholders (default: "{id}.{lang}.json") + --pattern File path pattern for message files relative to the output + directory, with {id} and {lang} placeholders + (default: "messages/{id}.{lang}.json") -h, --help display help for command ``` diff --git a/packages/cli/src/modules/translations/commands/export.ts b/packages/cli/src/modules/translations/commands/export.ts index ca8bd2c60b..cf6630ae39 100644 --- a/packages/cli/src/modules/translations/commands/export.ts +++ b/packages/cli/src/modules/translations/commands/export.ts @@ -39,12 +39,8 @@ export default async (options: ExportOptions) => { paths.targetRoot, ); - const messagesDir = resolvePath(paths.targetDir, options.output, 'messages'); - const manifestPath = resolvePath( - paths.targetDir, - options.output, - 'manifest.json', - ); + const outputDir = resolvePath(paths.targetDir, options.output); + const manifestPath = resolvePath(outputDir, 'manifest.json'); const tsconfigPath = paths.resolveTargetRoot('tsconfig.json'); if (!(await fs.pathExists(tsconfigPath))) { @@ -104,7 +100,7 @@ export default async (options: ExportOptions) => { ref.id, DEFAULT_LANGUAGE, ); - const filePath = resolvePath(messagesDir, relPath); + const filePath = resolvePath(outputDir, relPath); await fs.ensureDir(dirname(filePath)); await fs.writeJson(filePath, ref.messages, { spaces: 2 }); } @@ -132,6 +128,6 @@ export default async (options: ExportOptions) => { console.log( `\nExported ${allRefs.length} translation ref(s) to ${options.output}/`, ); - console.log(` Messages: ${options.output}/messages/${examplePath}`); + console.log(` Messages: ${options.output}/${examplePath}`); console.log(` Manifest: ${options.output}/manifest.json`); }; diff --git a/packages/cli/src/modules/translations/commands/import.ts b/packages/cli/src/modules/translations/commands/import.ts index 35b554ce30..cab2030d1f 100644 --- a/packages/cli/src/modules/translations/commands/import.ts +++ b/packages/cli/src/modules/translations/commands/import.ts @@ -48,7 +48,6 @@ export default async (options: ImportOptions) => { await readTargetPackage(paths.targetDir, paths.targetRoot); const inputDir = resolvePath(paths.targetDir, options.input); - const messagesDir = resolvePath(inputDir, 'messages'); const manifestPath = resolvePath(inputDir, 'manifest.json'); const outputPath = resolvePath(paths.targetDir, options.output); @@ -59,13 +58,6 @@ export default async (options: ImportOptions) => { ); } - if (!(await fs.pathExists(messagesDir))) { - throw new Error( - `No messages directory found at ${messagesDir}. ` + - 'Run "backstage-cli translations export" first.', - ); - } - const manifest: Manifest = await fs.readJson(manifestPath); if (!manifest.pattern) { @@ -77,8 +69,10 @@ export default async (options: ImportOptions) => { const parsePath = createMessagePathParser(pattern); - // Discover all JSON files under the messages directory - const allFiles = await collectJsonFiles(messagesDir); + // Discover all JSON files under the translations directory + const allFiles = (await collectJsonFiles(inputDir)).filter( + f => f !== 'manifest.json', + ); // Parse each file to extract id + lang, filtering out default language files const translationsByRef = new Map< @@ -120,7 +114,7 @@ export default async (options: ImportOptions) => { console.log('No translated message files found.'); const example = formatMessagePath(pattern, '', 'sv'); console.log( - `Add translated files as messages/${example} in the translations directory.`, + `Add translated files as ${example} in the translations directory.`, ); return; } @@ -150,7 +144,7 @@ export default async (options: ImportOptions) => { const jsonRelPath = posixPath.normalize( relativePath( resolvePath(outputPath, '..'), - resolvePath(messagesDir, relPath), + resolvePath(inputDir, relPath), ), ); return ` ${JSON.stringify(lang)}: () => import('./${jsonRelPath}'),`; diff --git a/packages/cli/src/modules/translations/lib/messageFilePath.test.ts b/packages/cli/src/modules/translations/lib/messageFilePath.test.ts index 6a6f3a8873..4c276e114c 100644 --- a/packages/cli/src/modules/translations/lib/messageFilePath.test.ts +++ b/packages/cli/src/modules/translations/lib/messageFilePath.test.ts @@ -26,13 +26,13 @@ describe('messageFilePath', () => { describe('formatMessagePath', () => { it('formats the default pattern', () => { expect(formatMessagePath(DEFAULT_MESSAGE_PATTERN, 'org', 'en')).toBe( - 'org.en.json', + 'messages/org.en.json', ); }); it('formats with a different language', () => { expect(formatMessagePath(DEFAULT_MESSAGE_PATTERN, 'catalog', 'sv')).toBe( - 'catalog.sv.json', + 'messages/catalog.sv.json', ); }); @@ -52,12 +52,12 @@ describe('messageFilePath', () => { describe('createMessagePathParser', () => { it('parses the default pattern', () => { const parse = createMessagePathParser(DEFAULT_MESSAGE_PATTERN); - expect(parse('org.en.json')).toEqual({ id: 'org', lang: 'en' }); + expect(parse('messages/org.en.json')).toEqual({ id: 'org', lang: 'en' }); }); it('parses dotted ref IDs in the default pattern', () => { const parse = createMessagePathParser(DEFAULT_MESSAGE_PATTERN); - expect(parse('plugin.notifications.sv.json')).toEqual({ + expect(parse('messages/plugin.notifications.sv.json')).toEqual({ id: 'plugin.notifications', lang: 'sv', }); @@ -71,7 +71,7 @@ describe('messageFilePath', () => { it('returns undefined for non-matching paths', () => { const parse = createMessagePathParser(DEFAULT_MESSAGE_PATTERN); expect(parse('not-a-match.txt')).toBeUndefined(); - expect(parse('dir/org.en.json')).toBeUndefined(); + expect(parse('other/org.en.json')).toBeUndefined(); }); it('returns undefined for invalid language code', () => { @@ -100,7 +100,9 @@ describe('messageFilePath', () => { describe('messagePatternToGlob', () => { it('converts the default pattern', () => { - expect(messagePatternToGlob(DEFAULT_MESSAGE_PATTERN)).toBe('*.*.json'); + expect(messagePatternToGlob(DEFAULT_MESSAGE_PATTERN)).toBe( + 'messages/*.*.json', + ); }); it('converts a language-directory pattern', () => { @@ -109,8 +111,8 @@ describe('messageFilePath', () => { }); describe('patternHasSubdirectories', () => { - it('returns false for flat patterns', () => { - expect(patternHasSubdirectories(DEFAULT_MESSAGE_PATTERN)).toBe(false); + it('returns true for the default pattern', () => { + expect(patternHasSubdirectories(DEFAULT_MESSAGE_PATTERN)).toBe(true); }); it('returns true for patterns with directories', () => { diff --git a/packages/cli/src/modules/translations/lib/messageFilePath.ts b/packages/cli/src/modules/translations/lib/messageFilePath.ts index 5f84c1ad13..0eb2547759 100644 --- a/packages/cli/src/modules/translations/lib/messageFilePath.ts +++ b/packages/cli/src/modules/translations/lib/messageFilePath.ts @@ -17,9 +17,9 @@ // The default language for exported translation messages. export const DEFAULT_LANGUAGE = 'en'; -// Default file path pattern for translation message files. -// Supported placeholders: {id} (ref ID) and {lang} (language code). -export const DEFAULT_MESSAGE_PATTERN = '{id}.{lang}.json'; +// Default file path pattern for translation message files relative to the +// translations directory. Supported placeholders: {id} and {lang}. +export const DEFAULT_MESSAGE_PATTERN = 'messages/{id}.{lang}.json'; /** Formats a message file pattern into a concrete relative path. */ export function formatMessagePath(