From dfa86e3d19b875fcd294399d0b7b2f3245804d75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Chr=C3=A1stek?= Date: Sun, 12 Feb 2023 12:25:27 +0100 Subject: [PATCH 1/2] feat(cli): Extract - Flatten ICU messages --- jest.config.integration.js | 2 - .../api/__snapshots__/catalog.test.ts.snap | 107 ++++++++++++++++++ packages/cli/src/api/catalog.test.ts | 42 +++++++ packages/cli/src/api/catalog.ts | 22 ++-- .../api/fixtures/collect-flatten/component.js | 2 + packages/cli/src/lingui-extract-template.ts | 3 + packages/cli/src/lingui-extract.ts | 3 + packages/cli/src/tests.ts | 1 + packages/core/flatten.entry.ts | 4 + packages/core/flatten.js | 5 + packages/core/package.json | 10 ++ .../core/src/flatten/flattenMessage.test.ts | 50 ++++++++ packages/core/src/flatten/flattenMessage.ts | 51 +++++++++ packages/core/src/flatten/index.ts | 1 + packages/core/src/flatten/printICU.test.ts | 60 ++++++++++ packages/core/src/flatten/printICU.ts | 107 ++++++++++++++++++ scripts/build/bundles.ts | 8 +- test/node-api/index.js | 1 + tsconfig.json | 5 +- website/docs/ref/cli.md | 35 ++++++ 20 files changed, 507 insertions(+), 12 deletions(-) create mode 100644 packages/cli/src/api/fixtures/collect-flatten/component.js create mode 100644 packages/core/flatten.entry.ts create mode 100644 packages/core/flatten.js create mode 100644 packages/core/src/flatten/flattenMessage.test.ts create mode 100644 packages/core/src/flatten/flattenMessage.ts create mode 100644 packages/core/src/flatten/index.ts create mode 100644 packages/core/src/flatten/printICU.test.ts create mode 100644 packages/core/src/flatten/printICU.ts diff --git a/jest.config.integration.js b/jest.config.integration.js index a113b9925..9a074a878 100644 --- a/jest.config.integration.js +++ b/jest.config.integration.js @@ -8,8 +8,6 @@ module.exports = { roots: ["/packages/"], testPathIgnorePatterns: ["/node_modules/"], - // Redirect imports to the compiled bundles - moduleNameMapper: {}, setupFiles: ["set-tz/utc"], // Exclude the build output from transforms diff --git a/packages/cli/src/api/__snapshots__/catalog.test.ts.snap b/packages/cli/src/api/__snapshots__/catalog.test.ts.snap index fc1dd47de..5aa16d778 100644 --- a/packages/cli/src/api/__snapshots__/catalog.test.ts.snap +++ b/packages/cli/src/api/__snapshots__/catalog.test.ts.snap @@ -244,6 +244,78 @@ Object { } `; +exports[`Catalog make should collect and write catalogs with flatten ICU messages 1`] = ` +Object { + cs: null, + en: null, +} +`; + +exports[`Catalog make should collect and write catalogs with flatten ICU messages 2`] = ` +Object { + cs: Object { + {count, plural, one {This is my #st cat.} two {This is my #nd cat.} other {This is my #rd cat.}}: Object { + comments: Array [], + context: null, + extractedComments: Array [], + flags: Array [], + obsolete: false, + origin: Array [ + Array [ + collect-flatten/component.js, + 1, + ], + ], + translation: , + }, + {gender, select, female {She added a new image to the system.} male {He added a new image to the system.} other {They added a new image to the system.}}: Object { + comments: Array [], + context: null, + extractedComments: Array [], + flags: Array [], + obsolete: false, + origin: Array [ + Array [ + collect-flatten/component.js, + 2, + ], + ], + translation: , + }, + }, + en: Object { + {count, plural, one {This is my #st cat.} two {This is my #nd cat.} other {This is my #rd cat.}}: Object { + comments: Array [], + context: null, + extractedComments: Array [], + flags: Array [], + obsolete: false, + origin: Array [ + Array [ + collect-flatten/component.js, + 1, + ], + ], + translation: , + }, + {gender, select, female {She added a new image to the system.} male {He added a new image to the system.} other {They added a new image to the system.}}: Object { + comments: Array [], + context: null, + extractedComments: Array [], + flags: Array [], + obsolete: false, + origin: Array [ + Array [ + collect-flatten/component.js, + 2, + ], + ], + translation: , + }, + }, +} +`; + exports[`Catalog make should merge with existing catalogs 1`] = ` Object { cs: Object { @@ -546,6 +618,41 @@ Object { } `; +exports[`Catalog makeTemplate should collect and write a template with flatten ICU messages 1`] = `null`; + +exports[`Catalog makeTemplate should collect and write a template with flatten ICU messages 2`] = ` +Object { + {count, plural, one {This is my #st cat.} two {This is my #nd cat.} other {This is my #rd cat.}}: Object { + comments: Array [], + context: null, + extractedComments: Array [], + flags: Array [], + obsolete: false, + origin: Array [ + Array [ + collect-flatten/component.js, + 1, + ], + ], + translation: , + }, + {gender, select, female {She added a new image to the system.} male {He added a new image to the system.} other {They added a new image to the system.}}: Object { + comments: Array [], + context: null, + extractedComments: Array [], + flags: Array [], + obsolete: false, + origin: Array [ + Array [ + collect-flatten/component.js, + 2, + ], + ], + translation: , + }, +} +`; + exports[`Catalog read should read file in given format 1`] = ` Object { obsolete: Object { diff --git a/packages/cli/src/api/catalog.test.ts b/packages/cli/src/api/catalog.test.ts index 41735f0e0..d604cf66d 100644 --- a/packages/cli/src/api/catalog.test.ts +++ b/packages/cli/src/api/catalog.test.ts @@ -111,6 +111,27 @@ describe("Catalog", () => { await catalog.make(defaultMakeOptions) expect(catalog.readAll()).toMatchSnapshot() }) + + it("should collect and write catalogs with flatten ICU messages", async () => { + const localeDir = copyFixture(fixture("locales", "initial")) + const catalog = new Catalog( + { + name: "messages", + path: path.join(localeDir, "{locale}", "messages"), + include: [fixture("collect-flatten/")], + exclude: [], + }, + mockConfig({ + locales: ["en", "cs"], + }) + ) + + // Everything should be empty + expect(catalog.readAll()).toMatchSnapshot() + + await catalog.make({ ...defaultMakeOptions, flatten: true }) + expect(catalog.readAll()).toMatchSnapshot() + }) }) describe("makeTemplate", () => { @@ -137,6 +158,27 @@ describe("Catalog", () => { await catalog.makeTemplate(defaultMakeTemplateOptions) expect(catalog.readTemplate()).toMatchSnapshot() }) + + it("should collect and write a template with flatten ICU messages", async () => { + const localeDir = copyFixture(fixture("locales", "initial")) + const catalog = new Catalog( + { + name: "messages", + path: path.join(localeDir, "{locale}", "messages"), + include: [fixture("collect-flatten/")], + exclude: [], + }, + mockConfig({ + locales: ["en", "cs"], + }) + ) + + // Everything should be empty + expect(catalog.readTemplate()).toMatchSnapshot() + + await catalog.makeTemplate({ ...defaultMakeOptions, flatten: true }) + expect(catalog.readTemplate()).toMatchSnapshot() + }) }) describe("POT Flow", () => { diff --git a/packages/cli/src/api/catalog.ts b/packages/cli/src/api/catalog.ts index 93960c20c..3f876aaea 100644 --- a/packages/cli/src/api/catalog.ts +++ b/packages/cli/src/api/catalog.ts @@ -20,6 +20,7 @@ import extract, { ExtractedMessage } from "./extractors" import { CliExtractOptions } from "../lingui-extract" import { CliExtractTemplateOptions } from "../lingui-extract-template" import { CompiledCatalogNamespace } from "./compile" +import { flattenMessage } from "@lingui/core/flatten" import { prettyOrigin } from "./utils" import chalk from "chalk" @@ -177,15 +178,20 @@ export class Catalog { const fileSuccess = await extract( filename, (next: ExtractedMessage) => { - if (!messages[next.id]) { - messages[next.id] = { - message: next.message, + const nextId = options.flatten ? flattenMessage(next.id) : next.id + const nextMessage = options.flatten + ? flattenMessage(next.message) + : next.message + + if (!messages[nextId]) { + messages[nextId] = { + message: nextMessage, extractedComments: [], origin: [], } } - const prev = messages[next.id] + const prev = messages[nextId] const filename = path .relative(this.config.rootDir, next.origin[0]) @@ -193,17 +199,17 @@ export class Catalog { const origin: MessageOrigin = [filename, next.origin[1]] - if (prev.message && next.message && prev.message !== next.message) { + if (prev.message && nextMessage && prev.message !== nextMessage) { throw new Error( `Encountered different default translations for message ${chalk.yellow( - next.id + nextId )}` + `\n${chalk.yellow(prettyOrigin(prev.origin))} ${prev.message}` + - `\n${chalk.yellow(prettyOrigin([origin]))} ${next.message}` + `\n${chalk.yellow(prettyOrigin([origin]))} ${nextMessage}` ) } - messages[next.id] = { + messages[nextId] = { ...prev, extractedComments: next.comment ? [...prev.extractedComments, next.comment] diff --git a/packages/cli/src/api/fixtures/collect-flatten/component.js b/packages/cli/src/api/fixtures/collect-flatten/component.js new file mode 100644 index 000000000..ca7b36fab --- /dev/null +++ b/packages/cli/src/api/fixtures/collect-flatten/component.js @@ -0,0 +1,2 @@ +/*i18n*/ i18n._("This is my {count, plural, one {#st} two {#nd} other {#rd}} cat.", { count }) +/*i18n*/ i18n._("{gender, select, female {She} male {He} other {They}} added a new image to the system.", { gender }) diff --git a/packages/cli/src/lingui-extract-template.ts b/packages/cli/src/lingui-extract-template.ts index c384d20f4..f5f2cc2c0 100644 --- a/packages/cli/src/lingui-extract-template.ts +++ b/packages/cli/src/lingui-extract-template.ts @@ -12,6 +12,7 @@ export type CliExtractTemplateOptions = { configPath: string extractors?: ExtractorType[] files?: string[] + flatten?: boolean } export default async function command( @@ -61,6 +62,7 @@ export default async function command( if (require.main === module) { program .option("--config ", "Path to the config file") + .option("--flatten", "Flattens ICU messages") .option("--verbose", "Verbose output") .parse(process.argv) @@ -69,6 +71,7 @@ if (require.main === module) { }) const result = command(config, { + flatten: program.flatten || false, verbose: program.verbose || false, configPath: program.config || process.env.LINGUI_CONFIG, }).then(() => { diff --git a/packages/cli/src/lingui-extract.ts b/packages/cli/src/lingui-extract.ts index 91b69180a..fa48d16fa 100644 --- a/packages/cli/src/lingui-extract.ts +++ b/packages/cli/src/lingui-extract.ts @@ -20,6 +20,7 @@ export type CliExtractOptions = { locale: string prevFormat: string | null watch?: boolean + flatten?: boolean } export default async function command( @@ -108,6 +109,7 @@ if (require.main === module) { "Convert from previous format of message catalogs" ) .option("--watch", "Enables Watch Mode") + .option("--flatten", "Flattens ICU messages") // Obsolete options .option( "--babelOptions", @@ -169,6 +171,7 @@ if (require.main === module) { locale: program.locale, configPath: program.config || process.env.LINGUI_CONFIG, watch: program.watch || false, + flatten: program.flatten || false, files: filePath?.length ? filePath : undefined, prevFormat, }) diff --git a/packages/cli/src/tests.ts b/packages/cli/src/tests.ts index 2f21dbc89..c0cde5fd5 100644 --- a/packages/cli/src/tests.ts +++ b/packages/cli/src/tests.ts @@ -30,6 +30,7 @@ export const defaultMakeOptions: MakeOptions = { prevFormat: null, configPath: null, orderBy: "messageId", + flatten: false, } export const defaultMakeTemplateOptions: MakeTemplateOptions = { diff --git a/packages/core/flatten.entry.ts b/packages/core/flatten.entry.ts new file mode 100644 index 000000000..56dea88c3 --- /dev/null +++ b/packages/core/flatten.entry.ts @@ -0,0 +1,4 @@ +/** + * this is Rollup Entry + */ +export * from "./src/compile" diff --git a/packages/core/flatten.js b/packages/core/flatten.js new file mode 100644 index 000000000..1c0fad5a5 --- /dev/null +++ b/packages/core/flatten.js @@ -0,0 +1,5 @@ +/** + * This entry is for old runtimes which do not support `exports` field in package.json + * https://github.com/facebook/metro/issues/670 + */ +module.exports = require("./build/cjs/flatten.js") diff --git a/packages/core/package.json b/packages/core/package.json index 64e57b87a..517fb2f0b 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -40,6 +40,16 @@ "default": "./build/esm/index.js" } }, + "./flatten": { + "require": { + "types": "./build/flatten.d.ts", + "default": "./build/cjs/flatten.js" + }, + "import": { + "types": "./build/flatten.d.ts", + "default": "./build/esm/flatten.js" + } + }, "./compile": { "require": { "types": "./build/compile.d.ts", diff --git a/packages/core/src/flatten/flattenMessage.test.ts b/packages/core/src/flatten/flattenMessage.test.ts new file mode 100644 index 000000000..7db6f2087 --- /dev/null +++ b/packages/core/src/flatten/flattenMessage.test.ts @@ -0,0 +1,50 @@ +import { mockConsole } from "@lingui/jest-mocks" +import { flattenMessage as flatten } from "./flattenMessage" + +describe("flatten", () => { + it("should flatten selectordinal", () => { + const message = flatten( + "It's my dog's {year, selectordinal, one {#st} two {#nd} few {#rd} other {#th}} birthday!" + ) + expect(message).toEqual( + "{year, selectordinal, one {It's my dog's #st birthday!} two {It's my dog's #nd birthday!} few {It's my dog's #rd birthday!} other {It's my dog's #th birthday!}}" + ) + }) + + it("should flatten plural", () => { + const message = flatten( + "I have {value, plural, one {{value} book} other {# books}}" + ) + expect(message).toEqual( + "{value, plural, one {I have {value} book} other {I have # books}}" + ) + }) + + it("should flatten select with a placeholder in a previous sentence", () => { + const message = flatten( + "Hello, Your friend {friend} is now online. {gender, select, female {She} male {He} other {They}} added a new image!" + ) + expect(message).toEqual( + "{gender, select, female {Hello, Your friend {friend} is now online. She added a new image!} male {Hello, Your friend {friend} is now online. He added a new image!} other {Hello, Your friend {friend} is now online. They added a new image!}}" + ) + }) + + it("should flatten plural with number", () => { + const message = flatten( + "You have {count, plural, one {{count, number} dog} other {{count, number} dogs}}" + ) + expect(message).toEqual( + "{count, plural, one {You have {count, number} dog} other {You have {count, number} dogs}}" + ) + }) + + it("should throw error for invalid ICU message", () => { + mockConsole((console) => { + flatten("{foo, plural, =a{e1} other{baz}}") + + expect(console.error).toBeCalledWith( + expect.stringContaining(`invalid syntax`) + ) + }) + }) +}) diff --git a/packages/core/src/flatten/flattenMessage.ts b/packages/core/src/flatten/flattenMessage.ts new file mode 100644 index 000000000..3ad56e9a8 --- /dev/null +++ b/packages/core/src/flatten/flattenMessage.ts @@ -0,0 +1,51 @@ +import { parse, Token } from "@messageformat/parser" +import { printICU, isPluralOrSelect } from "./printICU" + +/** @internal */ +export function flattenMessage(message: string): string { + try { + let tokens = parse(message) as Token[] + const flattenedTokens = flattenTokens(tokens) + return printICU(flattenedTokens) + } catch (e) { + console.error(`${(e as Error).message} \n\nMessage: ${message}`) + return message + } +} + +function flattenTokens(tokens: Array): Array { + for (let i = 0; i < tokens.length; i++) { + const token = tokens[i] + if (isPluralOrSelect(token)) { + const cloned = cloneDeep(token) + const { cases } = cloned + cloned.cases = cases.reduce((all, k, index) => { + const newValue = flattenTokens([ + ...tokens.slice(0, i), + ...cases[index].tokens, + ...tokens.slice(i + 1), + ]) + all[index] = { ...k, tokens: newValue } + return all + }, []) + + return [cloned] + } + } + return tokens +} + +function cloneDeep(obj: T): T { + if (Array.isArray(obj)) { + // @ts-expect-error + return [...obj.map(cloneDeep)] + } + if (obj !== null && typeof obj === "object") { + // @ts-expect-error + return Object.keys(obj).reduce((cloned, k) => { + cloned[k] = cloneDeep(obj[k]) + return cloned + }, {}) + } + return obj +} diff --git a/packages/core/src/flatten/index.ts b/packages/core/src/flatten/index.ts new file mode 100644 index 000000000..c38c8c5c3 --- /dev/null +++ b/packages/core/src/flatten/index.ts @@ -0,0 +1 @@ +export * from "./flattenMessage" diff --git a/packages/core/src/flatten/printICU.test.ts b/packages/core/src/flatten/printICU.test.ts new file mode 100644 index 000000000..6ba00127a --- /dev/null +++ b/packages/core/src/flatten/printICU.test.ts @@ -0,0 +1,60 @@ +import { parse } from "@messageformat/parser" +import { printICU } from "./printICU" + +describe("printICU", () => { + it("should properly escape strings", () => { + expect(printICU(parse("'This name {name} is banned'"))).toEqual( + "'This name {name} is banned'" + ) + expect(printICU(parse("'Your name is: ''{name}''."))).toEqual( + "'Your name is: ''{name}''." + ) + expect( + printICU( + parse( + "My brother {brotherName} is taller than {friendName} who is Peter's friend." + ) + ) + ).toEqual( + "My brother {brotherName} is taller than {friendName} who is Peter's friend." + ) + }) + + it("should print parameters correctly", () => { + expect(printICU(parse("{var,date, y-M-d HH:mm:ss zzzz}"))).toEqual( + "{var, date, y-M-d HH:mm:ss zzzz}" + ) + }) + + it("should print variable correctly", () => { + expect(printICU(parse("Hello {name}"))).toEqual("Hello {name}") + }) + + it("should print parameters with variable correctly", () => { + expect(printICU(parse("{foo, date, {bar}}"))).toEqual("{foo, date, {bar}}") + }) + + it("should print without whitespace before variable", () => { + expect(printICU(parse("{foo, date, {bar}}"))).toEqual( + "{foo, date, {bar}}" + ) + }) + + it("should print parameters with select correctly", () => { + expect(printICU(parse("{foo, date,{bar, select, other{baz}}}"))).toEqual( + "{foo, date, {bar, select, other {baz}}}" + ) + }) + + it("should print plural with offset correctly", () => { + expect(printICU(parse("{foo, plural, offset:4 other{baz}}"))).toEqual( + "{foo, plural, offset:4 other {baz}}" + ) + }) + + it("should print plural with exact forms correctly", () => { + expect( + printICU(parse("{foo, plural,=0{e0} =1{e1} =2{e2} other{baz}}")) + ).toEqual("{foo, plural, =0 {e0} =1 {e1} =2 {e2} other {baz}}") + }) +}) diff --git a/packages/core/src/flatten/printICU.ts b/packages/core/src/flatten/printICU.ts new file mode 100644 index 000000000..17b7d759e --- /dev/null +++ b/packages/core/src/flatten/printICU.ts @@ -0,0 +1,107 @@ +import { + Token, + Select, + SelectCase, + PlainArg, + Content, + FunctionArg, +} from "@messageformat/parser" + +/** @internal */ +export function printICU(tokens: Token[]): string { + return doPrintICU(tokens, false) +} + +function doPrintICU(tokens: Token[], isInPlural: boolean): string { + const printedNodes = tokens.map((token, i) => { + if (token.type === "content") { + return printContentToken( + token, + isInPlural, + i === 0, + i === tokens.length - 1 + ) + } else if (token.type === "argument") { + return printArgumentToken(token) + } else if (token.type === "function") { + return printFunctionToken(token) + } else if (token.type === "octothorpe") { + return "#" + } else if (isPluralOrSelect(token)) { + return printSelectToken(token) + } + }) + + return printedNodes.join("") +} + +function printEscapedMessage(message: string): string { + return message.replace(/([{}](?:.*[{}])?)/su, `'$1'`) +} + +function printContentToken( + token: Content, + isInPlural: boolean, + isFirstToken: boolean, + isLastToken: boolean +) { + let escaped = token.value + // If text starts with a ' and it is not the first token, + // then the token before is non-string and the `'` needs to be unescaped + if (!isFirstToken && escaped[0] === `'`) { + escaped = `''${escaped.slice(1)}` + } + // Same logic but for last token + if (!isLastToken && escaped[escaped.length - 1] === `'`) { + escaped = `${escaped.slice(0, escaped.length - 1)}''` + } + escaped = printEscapedMessage(escaped) + return isInPlural ? escaped.replace("#", "'#'") : escaped +} + +function printArgumentToken(token: PlainArg) { + return `{${token.arg}}` +} + +function printFunctionToken(token: FunctionArg) { + return `{${token.arg}, ${token.key}${ + token.param ? `, ${printFunctionParamToken(token.param)}` : "" + }}` +} + +function printFunctionParamToken(tokens: FunctionArg["param"]) { + return tokens + .map((token) => { + if (token.type === "content") { + return printEscapedMessage(token.value) + } else { + return doPrintICU([token], false) + } + }) + .join("") + .trimStart() +} + +function printSelectToken(token: Select) { + const msg = [ + token.arg, + token.type, + [ + token.pluralOffset ? `offset:${token.pluralOffset}` : "", + ...token.cases.map( + (tokenCase: SelectCase) => + `${tokenCase.key} {${doPrintICU( + tokenCase.tokens, + token.type === "plural" || token.type === "selectordinal" + )}}` + ), + ] + .filter(Boolean) + .join(" "), + ].join(", ") + return `{${msg}}` +} + +export function isPluralOrSelect(object: Token): object is Select { + return "cases" in object +} diff --git a/scripts/build/bundles.ts b/scripts/build/bundles.ts index f664fb306..a7b7fc280 100644 --- a/scripts/build/bundles.ts +++ b/scripts/build/bundles.ts @@ -31,7 +31,13 @@ export const bundles: readonly BundleDef[] = [ { type: BundleType.UNIVERSAL, packageName: "core", - externals: ["@lingui/core/compile"], + externals: ["@lingui/core/compile", "@lingui/core/flatten"], + }, + { + type: BundleType.UNIVERSAL, + packageName: "core", + entry: "flatten.entry.ts", + label: "flatten", }, { type: BundleType.UNIVERSAL, diff --git a/test/node-api/index.js b/test/node-api/index.js index ecb8b7499..056823a5c 100644 --- a/test/node-api/index.js +++ b/test/node-api/index.js @@ -5,3 +5,4 @@ assert(require.resolve("@lingui/cli/api/extractors/babel")) assert(require.resolve("@lingui/core")) assert(require.resolve("@lingui/core/compile")) +assert(require.resolve("@lingui/core/flatten")) diff --git a/tsconfig.json b/tsconfig.json index 65e53cb3d..a49b2ccf6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,9 +11,12 @@ "resolveJsonModule": true, "target": "es2017", "paths": { - "@lingui/babel-plugin-extract-messages": ["./packages/babel-plugin-extract-messages/src"], + "@lingui/babel-plugin-extract-messages": [ + "./packages/babel-plugin-extract-messages/src" + ], "@lingui/core": ["./packages/core/src"], "@lingui/core/compile": ["./packages/core/src/compile/index.ts"], + "@lingui/core/flatten": ["./packages/core/src/flatten/index.ts"], "@lingui/cli/api": ["./packages/cli/src/api"], "@lingui/react": ["./packages/react/src"], "@lingui/conf": ["./packages/conf"], diff --git a/website/docs/ref/cli.md b/website/docs/ref/cli.md index 87c2ae517..0d18a52a2 100644 --- a/website/docs/ref/cli.md +++ b/website/docs/ref/cli.md @@ -40,6 +40,7 @@ lingui extract [files...] [--convert-from ] [--verbose] [--watch [--debounce ]] + [--flatten] ``` This command extracts messages from source files and creates a message catalog for each language using the following steps: @@ -106,10 +107,28 @@ Remember to use this only in development as this command do not clean obsolete t Debounce, when used with `--debounce `, delays extraction for `` milliseconds, bundling multiple file changes together. +#### `--flatten` {#extract-flatten} + +Flattens the ICU message in the following way: + +``` +I have {value, plural, one {one book} other {# books}} +``` + +is changed to + +``` +{value, plural, one {I have one book} other {I have # books}} +``` + +This provides translators with full sentences for all cases. + + ## `extract-template` ``` shell lingui extract-template [--verbose] + [--flatten] ``` This command extracts messages from source files and creates a `.pot` template file. @@ -118,6 +137,22 @@ This command extracts messages from source files and creates a `.pot` template f Prints additional information. +#### `--flatten` {#extract-template-flatten} + +Flattens the ICU message in the following way: + +``` +I have {value, plural, one {one book} other {# books}} +``` + +is changed to + +``` +{value, plural, one {I have one book} other {I have # books}} +``` + +This provides translators with full sentences for all cases. + ## `compile` ``` shell From 4e4a4ce0631869790f459615d397805b4551377c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Chr=C3=A1stek?= Date: Sun, 12 Feb 2023 13:09:01 +0100 Subject: [PATCH 2/2] Fix lint issues --- website/docs/ref/cli.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/website/docs/ref/cli.md b/website/docs/ref/cli.md index 0d18a52a2..199292ab3 100644 --- a/website/docs/ref/cli.md +++ b/website/docs/ref/cli.md @@ -111,19 +111,18 @@ Debounce, when used with `--debounce `, delays extraction for `` m Flattens the ICU message in the following way: -``` +``` none I have {value, plural, one {one book} other {# books}} ``` is changed to -``` +``` none {value, plural, one {I have one book} other {I have # books}} ``` This provides translators with full sentences for all cases. - ## `extract-template` ``` shell @@ -141,13 +140,13 @@ Prints additional information. Flattens the ICU message in the following way: -``` +``` none I have {value, plural, one {one book} other {# books}} ``` is changed to -``` +``` none {value, plural, one {I have one book} other {I have # books}} ```