diff --git a/l10n/README.md b/l10n/README.md index 37912cde8..0c1b94303 100644 --- a/l10n/README.md +++ b/l10n/README.md @@ -59,3 +59,15 @@ Adding Fluent comments to explain what context strings appear in is an open task If one English string is used in multiple places and requires multiple different strings in a locale, file an issue to distinguish the IDs. Please also file an issue for any other problems you encounter with localizing - either with specific strings which need fixing, or general issues with the l10n process. + +### Finding new strings to localise + +Instead of having to manually copy strings from `./template.ftl` into your locale file, you can use the `npm run l10n merge` command to add the missing strings, allowing you to localise them inline. + +For German, as an example, running from the project root: + +``` +npm run l10n merge l10n/template.ftl l10n/locales/de.ftl +``` + +`de.ftl` will now have all the English strings it lacks. You can localise them all, or just a subset, and remove the ones you don't localise. The next run of the merge command will add the strings you removed again, allowing you to localise in batches if you'd prefer. diff --git a/l10n/cli.js b/l10n/cli.js index 93a5f324a..7332b1e30 100644 --- a/l10n/cli.js +++ b/l10n/cli.js @@ -2,7 +2,7 @@ import yargs from "yargs"; import { hideBin } from "yargs/helpers"; -import { extract } from "./parser/extractor.js"; +import { extract, merge } from "./parser/extractor.js"; import { generateQaaLocale } from "./parser/transform.js"; await yargs(hideBin(process.argv)) @@ -29,5 +29,13 @@ await yargs(hideBin(process.argv)) } }, }) + .command({ + command: "merge ", + describe: + "Merges source file into target file, adding strings which the target file doesn't have", + handler: async ({ source, target }) => { + await merge(source, target); + }, + }) .demandCommand() .parse(); diff --git a/l10n/parser/extractor.js b/l10n/parser/extractor.js index 9deb1d9cc..cf70fd60f 100644 --- a/l10n/parser/extractor.js +++ b/l10n/parser/extractor.js @@ -98,6 +98,34 @@ export async function getManualEntries(filePath) { ); } +/** + * @param {string} sourcePath + * @param {string} targetPath + * @param {{ lint?: boolean }} [options] + */ +export async function merge(sourcePath, targetPath, options) { + const sourceText = await readFile(sourcePath, "utf8"); + const targetText = await readFile(targetPath, "utf8"); + const sourceResource = parse(sourceText, {}); + const targetResource = parse(targetText, {}); + + const targetIds = new Set( + targetResource.body + .filter((entry) => entry instanceof Message) + .map((entry) => entry.id.name), + ); + + for (const entry of sourceResource.body) { + if (entry instanceof Message && !targetIds.has(entry.id.name)) { + targetResource.body.push(entry); + } + } + + const output = serialize(targetResource, {}); + if (!options?.lint) await writeFile(targetPath, output, "utf8"); + return output; +} + /** * @param {string} glob Files to scrape strings from */ diff --git a/test/unit/l10n/extractor.test.js b/test/unit/l10n/extractor.test.js index 1d685f1ac..4eabf7036 100644 --- a/test/unit/l10n/extractor.test.js +++ b/test/unit/l10n/extractor.test.js @@ -15,6 +15,7 @@ import { import { createFluentResource, getManualEntries, + merge, scrapeL10nTags, } from "../../../l10n/parser/extractor.js"; @@ -91,6 +92,22 @@ describe("l10n extractor", () => { }); }); + describe("merge", () => { + const sourcePath = path.join(__dirname, "fixtures", "merge-source.ftl"); + const targetPath = path.join(__dirname, "fixtures", "merge-target.ftl"); + + it("adds missing messages from source to target", async () => { + const output = await merge(sourcePath, targetPath, { lint: true }); + assert.match(output, /id2 = Source two/); + assert.match(output, /id3 = Source three/); + }); + + it("preserves existing messages in target", async () => { + const output = await merge(sourcePath, targetPath, { lint: true }); + assert.match(output, /id1 = Target one/); + }); + }); + describe("createFluentResource", () => { it("throws if duplicate ids with different text exist across manual and scraped strings", async () => { await assert.rejects( diff --git a/test/unit/l10n/fixtures/merge-source.ftl b/test/unit/l10n/fixtures/merge-source.ftl new file mode 100644 index 000000000..5cc9595b2 --- /dev/null +++ b/test/unit/l10n/fixtures/merge-source.ftl @@ -0,0 +1,3 @@ +id1 = Source one +id2 = Source two +id3 = Source three diff --git a/test/unit/l10n/fixtures/merge-target.ftl b/test/unit/l10n/fixtures/merge-target.ftl new file mode 100644 index 000000000..538053eff --- /dev/null +++ b/test/unit/l10n/fixtures/merge-target.ftl @@ -0,0 +1 @@ +id1 = Target one