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 <poldsberg@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Patrik Oldsberg
2026-02-18 12:32:51 +01:00
parent a808c9ef3f
commit dd5f052017
6 changed files with 29 additions and 36 deletions
+3 -3
View File
@@ -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.
+3 -2
View File
@@ -447,8 +447,9 @@ Usage: backstage-cli translations export [options]
Options:
--output <dir> Output directory for exported messages and manifest (default: "translations")
--pattern <pattern> File path pattern for message files, with {id} and {lang}
placeholders (default: "{id}.{lang}.json")
--pattern <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
```
@@ -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`);
};
@@ -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, '<ref-id>', '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}'),`;
@@ -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', () => {
@@ -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(