diff --git a/packages/cli/src/api/__snapshots__/compile.test.ts.snap b/packages/cli/src/api/__snapshots__/compile.test.ts.snap index c16a7b146..b60c649ca 100644 --- a/packages/cli/src/api/__snapshots__/compile.test.ts.snap +++ b/packages/cli/src/api/__snapshots__/compile.test.ts.snap @@ -6,6 +6,8 @@ Can't parse message. Please check correct syntax: "{value, plural, one {Book} ot Messageformat-parser trace: Expected "#", "{", "}", doubled apostrophe, escaped string, or plain char but end of input found. `; +exports[`createCompiledCatalog nested message 1`] = `/*eslint-disable*/module.exports={messages:{"nested":{"one":"Uno","two":"Dos","three":"Tres","hello":["Hola ",["name"]]}}};`; + exports[`createCompiledCatalog options.compilerBabelOptions by default should return catalog without ASCII chars 1`] = `/*eslint-disable*/module.exports={messages:{"Hello":"Alohà"}};`; exports[`createCompiledCatalog options.compilerBabelOptions should return catalog without ASCII chars 1`] = `/*eslint-disable*/module.exports={messages:{"Hello":"Aloh\\xE0"}};`; @@ -20,10 +22,18 @@ exports[`createCompiledCatalog options.namespace should compile with window 1`] exports[`createCompiledCatalog options.namespace should error with invalid value 1`] = `Invalid namespace param: "global"`; -exports[`createCompiledCatalog options.pseudoLocale should return catalog with pseudolocalized messages 1`] = `/*eslint-disable*/module.exports={messages:{"Hello":"Ĥēĺĺō"}};`; +exports[`createCompiledCatalog options.pseudoLocale should return catalog with pseudolocalized messages 1`] = `/*eslint-disable*/module.exports={messages:{"Hello":"ÀĥōĴ"}};`; exports[`createCompiledCatalog options.pseudoLocale should return compiled catalog when pseudoLocale doesn't match current locale 1`] = `/*eslint-disable*/module.exports={messages:{"Hello":"Ahoj"}};`; +exports[`createCompiledCatalog options.pure should return code catalog 1`] = `/*eslint-disable*/module.exports={messages:{"Hello":"Ahoj"}};`; + +exports[`createCompiledCatalog options.pure should return pure catalog 1`] = ` +Object { + Hello: Ahoj, +} +`; + exports[`createCompiledCatalog options.strict should return message key as a fallback translation 1`] = `/*eslint-disable*/module.exports={messages:{"Hello":"Ahoj","Missing":"Missing","Select":[["id","select",{Gen:"Genesis","1John":"1 John",other:"____"}]]}};`; exports[`createCompiledCatalog options.strict should't return message key as a fallback in strict mode 1`] = `/*eslint-disable*/module.exports={messages:{"Hello":"Ahoj","Missing":"","Select":[["id","select",{Gen:"Genesis","1John":"1 John",other:"____"}]]}};`; diff --git a/packages/cli/src/api/compile.test.ts b/packages/cli/src/api/compile.test.ts index 7e41102a0..380f28dd4 100644 --- a/packages/cli/src/api/compile.test.ts +++ b/packages/cli/src/api/compile.test.ts @@ -2,10 +2,11 @@ import generate from "@babel/generator" import { compile, createCompiledCatalog } from "./compile" describe("compile", () => { - const getSource = (message) => + const getSource = (message: string) => generate(compile(message) as any, { compact: true, minified: true, + jsescOption: { minimal: true }, }).code it("should optimize string only messages", () => { @@ -67,14 +68,172 @@ describe("compile", () => { ) }) + it("should compile multiple plurals", () => { + expect( + getSource( + "{bcount, plural, one {boy} other {# boys}} {gcount, plural, one {girl} other {# girls}}" + ) + ).toEqual( + '[["bcount","plural",{one:"boy",other:["#"," boys"]}]," ",["gcount","plural",{one:"girl",other:["#"," girls"]}]]' + ) + }) + it("should report failed message on error", () => { expect(() => getSource("{value, plural, one {Book} other {Books") ).toThrowErrorMatchingSnapshot() }) + + describe("with pseudo-localization", () => { + const getPSource = (message: string) => + generate(compile(message, true) as any, { + compact: true, + minified: true, + jsescOption: { minimal: true }, + }).code + + it("should pseudolocalize strings", () => { + expect(getPSource("Martin Černý")).toEqual('"Màŕţĩń Čēŕńý"') + }) + + it("should pseudolocalize escaping syntax characters", () => { + // TODO: should this turn into pseudoLocale string? + expect(getPSource("'{name}'")).toEqual('"{name}"') + // expect(getPSource("'{name}'")).toEqual('"{ńàmē}"') + }) + + it("should not pseudolocalize arguments", () => { + expect(getPSource("{name}")).toEqual('[["name"]]') + expect(getPSource("B4 {name} A4")).toEqual('["ß4 ",["name"]," À4"]') + }) + + it("should not pseudolocalize arguments nor formats", () => { + expect(getPSource("{name, number}")).toEqual('[["name","number"]]') + expect(getPSource("{name, number, percent}")).toEqual( + '[["name","number","percent"]]' + ) + }) + + it("should not pseudolocalize HTML tags", () => { + expect(getPSource('Martin Černý')).toEqual( + JSON.stringify('Màŕţĩń Čēŕńý') + ) + expect( + getPSource("Martin Cerny 123aČerný") + ).toEqual( + JSON.stringify("Màŕţĩń Ćēŕńŷ 123àČēŕńý") + ) + expect(getPSource("Martin a")).toEqual( + JSON.stringify("Màŕţĩń à") + ) + expect(getPSource("text")).toEqual( + JSON.stringify("ţēxţ") + ) + }) + + describe("Plurals", () => { + it("with value", () => { + expect( + getPSource("{value, plural, one {# book} other {# books}}") + ).toEqual('[["value","plural",{one:["#"," ƀōōķ"],other:["#"," ƀōōķś"]}]]') + }) + + it("with variable placeholder", () => { + expect( + getPSource( + "{count, plural, one {{countString} book} other {{countString} books}}" + ) + ).toEqual( + '[["count","plural",{one:[["countString"]," ƀōōķ"],other:[["countString"]," ƀōōķś"]}]]' + ) + }) + + it("with offset", () => { + expect( + getPSource( + "{count, plural, offset:1 zero {There are no messages} other {There are # messages in your inbox}}" + ) + ).toEqual( + '[["count","plural",{offset:1,zero:"Ţĥēŕē àŕē ńō mēśśàĝēś",other:["Ţĥēŕē àŕē ","#"," mēśśàĝēś ĩń ŷōũŕ ĩńƀōx"]}]]' + ) + }) + + it("with HTML tags", () => { + expect( + getPSource( + "{count, plural, zero {There's # message} other {There are # messages}}" + ) + ).toEqual( + '[["count","plural",{zero:["Ţĥēŕē\'ś ","#"," mēśśàĝē"],other:["Ţĥēŕē àŕē ","#"," mēśśàĝēś"]}]]' + ) + }) + + it("with exact number", () => { + expect( + getPSource( + "{count, plural, =0 {There's # message} other {There are # messages}}" + ) + ).toEqual( + '[["count","plural",{0:["Ţĥēŕē\'ś ","#"," mēśśàĝē"],other:["Ţĥēŕē àŕē ","#"," mēśśàĝēś"]}]]' + ) + }) + }) + + it("SelectOrdinal", () => { + expect( + getPSource( + "{count, selectordinal, offset:1 one {#st} two {#nd} few {#rd} =4 {4th} many {testMany} other {#th}}" + ) + ).toEqual( + '[["count","selectordinal",{offset:1,one:["#","śţ"],two:["#","ńď"],few:["#","ŕď"],4:"4ţĥ",many:"ţēśţMàńŷ",other:["#","ţĥ"]}]]' + ) + }) + + it("Select", () => { + expect( + getPSource( + "{gender, select, male {He} female {She} other {Other}}" + ) + ).toEqual( + '[["gender","select",{male:"Ĥē",female:"Śĥē",other:"Ōţĥēŕ"}]]' + ) + }) + + it("should not pseudolocalize variables", () => { + expect(getPSource("replace {count}")).toEqual('["ŕēƥĺàćē ",["count"]]') + expect(getPSource("replace { count }")).toEqual('["ŕēƥĺàćē ",["count"]]') + }) + + it("Multiple Plurals", () => { + expect( + getPSource( + "{bcount, plural, one {boy} other {# boys}} {gcount, plural, one {girl} other {# girls}}" + ) + ).toEqual( + '[["bcount","plural",{one:"ƀōŷ",other:["#"," ƀōŷś"]}]," ",["gcount","plural",{one:"ĝĩŕĺ",other:["#"," ĝĩŕĺś"]}]]' + ) + }) + }) }) describe("createCompiledCatalog", () => { + it("nested message", () => { + expect( + createCompiledCatalog( + "cs", + { + nested: { + one: "Uno", + two: "Dos", + three: "Tres", + hello: "Hola {name}", + }, + }, + {} + ) + ).toMatchSnapshot() + }) + describe("options.namespace", () => { const getCompiledCatalog = (namespace) => createCompiledCatalog( @@ -113,7 +272,7 @@ describe("createCompiledCatalog", () => { { Hello: "Ahoj", Missing: "", - Select: "{id, select, Gen {Genesis} 1John {1 John} other {____}}" + Select: "{id, select, Gen {Genesis} 1John {1 John} other {____}}", }, { strict, @@ -150,6 +309,27 @@ describe("createCompiledCatalog", () => { }) }) + describe("options.pure", () => { + const getCompiledCatalog = (pure) => + createCompiledCatalog( + "ps", + { + Hello: "Ahoj", + }, + { + pure, + } + ) + + it("should return pure catalog", () => { + expect(getCompiledCatalog(true)).toMatchSnapshot() + }) + + it("should return code catalog", () => { + expect(getCompiledCatalog(false)).toMatchSnapshot() + }) + }) + describe("options.compilerBabelOptions", () => { const getCompiledCatalog = (opts = {}) => createCompiledCatalog( @@ -165,13 +345,15 @@ describe("createCompiledCatalog", () => { }) it("should return catalog without ASCII chars", () => { - expect(getCompiledCatalog({ - compilerBabelOptions: {  - jsescOption: { - minimal: false, - } - } - })).toMatchSnapshot() + expect( + getCompiledCatalog({ + compilerBabelOptions: { + jsescOption: { + minimal: false, + }, + }, + }) + ).toMatchSnapshot() }) }) }) diff --git a/packages/cli/src/api/compile.ts b/packages/cli/src/api/compile.ts index 0ebd8add7..42e0882c0 100644 --- a/packages/cli/src/api/compile.ts +++ b/packages/cli/src/api/compile.ts @@ -5,7 +5,6 @@ import * as R from "ramda" import pseudoLocalize from "./pseudoLocalize" - const INVALID_OBJECT_KEY_REGEX = /^(\d+[a-zA-Z]|[a-zA-Z]+\d)(\d|[a-zA-Z])*/ export type CompiledCatalogNamespace = "cjs" | "es" | "ts" | string @@ -26,11 +25,7 @@ export type CreateCompileCatalogOptions = { * applying pseudolocalization where necessary. */ function compileSingleKey(key: string, translation: string, shouldPseudolocalize: boolean): t.ObjectProperty { - if (shouldPseudolocalize) { - translation = pseudoLocalize(key) - } - - return t.objectProperty(t.stringLiteral(key), compile(translation)) + return t.objectProperty(t.stringLiteral(key), compile(translation, shouldPseudolocalize)) } export function createCompiledCatalog( @@ -118,34 +113,40 @@ function buildExportStatement(expression, namespace: CompiledCatalogNamespace) { * Compile string message into AST tree. Message format is parsed/compiled into * JS arrays, which are handled in client. */ -export function compile(message: string) { +export function compile(message: string, shouldPseudolocalize: boolean = false) { let tokens try { tokens = parse(message) } catch (e) { throw new Error( - `Can't parse message. Please check correct syntax: "${message}" \n \n Messageformat-parser trace: ${e.message}`, + `Can't parse message. Please check correct syntax: "${message}" \n \n Messageformat-parser trace: ${e.message}` ) } - const ast = processTokens(tokens) + const ast = processTokens(tokens, shouldPseudolocalize) if (isString(ast)) return t.stringLiteral(ast) return ast } -function processTokens(tokens) { +function processTokens(tokens, shouldPseudolocalize: boolean) { // Shortcut - if the message doesn't include any formatting, // simply join all string chunks into one message if (!tokens.filter((token) => !isString(token)).length) { - return tokens.join("") + if (shouldPseudolocalize) { + return tokens.map((token) => pseudoLocalize(token)).join("") + } else { + return tokens.join("") + } } return t.arrayExpression( tokens.map((token) => { if (isString(token)) { - return t.stringLiteral(token) + return t.stringLiteral( + shouldPseudolocalize ? pseudoLocalize(token) : token + ) // # in plural case } else if (token.type === "octothorpe") { @@ -179,7 +180,7 @@ function processTokens(tokens) { } token.cases.forEach((item) => { - const inlineTokens = processTokens(item.tokens) + const inlineTokens = processTokens(item.tokens, shouldPseudolocalize) formatProps.push( t.objectProperty( // if starts with number must be wrapped with quotes diff --git a/packages/cli/src/api/pseudoLocalize.test.ts b/packages/cli/src/api/pseudoLocalize.test.ts index 4dd0b187b..1b9c7d207 100644 --- a/packages/cli/src/api/pseudoLocalize.test.ts +++ b/packages/cli/src/api/pseudoLocalize.test.ts @@ -50,20 +50,20 @@ describe("PseudoLocalization", () => { it("with HTML tags", () => { expect( pseudoLocalize( - "{count, plural, zero {There's # message} other {There are # messages}" + "{count, plural, zero {There's # message} other {There are # messages}}" ) ).toEqual( - "{count, plural, zero {Ţĥēŕē'ś # mēśśàĝē} other {Ţĥēŕē àŕē # mēśśàĝēś}" + "{count, plural, zero {Ţĥēŕē'ś # mēśśàĝē} other {Ţĥēŕē àŕē # mēśśàĝēś}}" ) }) it("with exact number", () => { expect( pseudoLocalize( - "{count, plural, =0 {There's # message} other {There are # messages}" + "{count, plural, =0 {There's # message} other {There are # messages}}" ) ).toEqual( - "{count, plural, =0 {Ţĥēŕē'ś # mēśśàĝē} other {Ţĥēŕē àŕē # mēśśàĝēś}" + "{count, plural, =0 {Ţĥēŕē'ś # mēśśàĝē} other {Ţĥēŕē àŕē # mēśśàĝēś}}" ) }) }) @@ -92,4 +92,14 @@ describe("PseudoLocalization", () => { expect(pseudoLocalize("replace {count}")).toEqual("ŕēƥĺàćē {count}") expect(pseudoLocalize("replace { count }")).toEqual("ŕēƥĺàćē { count }") }) + + it("multiple plurals pseudolocalize gives wrong ICU message", () => { + expect( + pseudoLocalize( + "{bcount, plural, one {boy} other {# boys}} {gcount, plural, one {girl} other {# girls}}" + ) + ).not.toEqual( + "{bcount, plural, one {ƀōŷ} other {# ƀōŷś}} {gcount, plural, one {ĝĩŕĺ} other {# ĝĩŕĺś}}" + ) + }) })