diff --git a/packages/cli/package.json b/packages/cli/package.json
index 66c7a14af..e563709de 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -41,6 +41,7 @@
"@babel/types": "^7.11.5",
"@lingui/babel-plugin-extract-messages": "^3.15.0",
"@lingui/conf": "^3.15.0",
+ "@lingui/core": "^3.15.0",
"babel-plugin-macros": "^3.0.1",
"bcp-47": "^1.0.7",
"chalk": "^4.1.0",
@@ -53,7 +54,6 @@
"glob": "^7.1.4",
"inquirer": "^7.3.3",
"make-plural": "^6.2.2",
- "messageformat-parser": "^4.1.3",
"micromatch": "4.0.2",
"mkdirp": "^1.0.4",
"node-gettext": "^3.0.0",
diff --git a/packages/cli/src/api/__snapshots__/compile.test.ts.snap b/packages/cli/src/api/__snapshots__/compile.test.ts.snap
index b60c649ca..eccc39549 100644
--- a/packages/cli/src/api/__snapshots__/compile.test.ts.snap
+++ b/packages/cli/src/api/__snapshots__/compile.test.ts.snap
@@ -1,39 +1,25 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`compile should report failed message on error 1`] = `
-Can't parse message. Please check correct syntax: "{value, plural, one {Book} other {Books"
-
- 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:JSON.parse("{\\"nested\\":{\\"one\\":\\"Uno\\",\\"two\\":\\"Dos\\",\\"three\\":\\"Tres\\",\\"hello\\":[\\"Hola \\",[\\"name\\"]]}}")};`;
-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:JSON.parse("{\\"Hello\\":\\"Alohà\\"}")};`;
-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:JSON.parse("{\\"Hello\\":\\"Aloh\\xE0\\"}")};`;
-exports[`createCompiledCatalog options.compilerBabelOptions should return catalog without ASCII chars 1`] = `/*eslint-disable*/module.exports={messages:{"Hello":"Aloh\\xE0"}};`;
+exports[`createCompiledCatalog options.namespace should compile with es 1`] = `/*eslint-disable*/export const messages=JSON.parse("{}");`;
-exports[`createCompiledCatalog options.namespace should compile with es 1`] = `/*eslint-disable*/export const messages={};`;
+exports[`createCompiledCatalog options.namespace should compile with global 1`] = `/*eslint-disable*/global.test={messages:JSON.parse("{}")};`;
-exports[`createCompiledCatalog options.namespace should compile with global 1`] = `/*eslint-disable*/global.test={messages:{}};`;
+exports[`createCompiledCatalog options.namespace should compile with ts 1`] = `/*eslint-disable*/export const messages=JSON.parse("{}");`;
-exports[`createCompiledCatalog options.namespace should compile with ts 1`] = `/*eslint-disable*/export const messages={};`;
-
-exports[`createCompiledCatalog options.namespace should compile with window 1`] = `/*eslint-disable*/window.test={messages:{}};`;
+exports[`createCompiledCatalog options.namespace should compile with window 1`] = `/*eslint-disable*/window.test={messages:JSON.parse("{}")};`;
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 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.pseudoLocale should return catalog with pseudolocalized messages 1`] = `/*eslint-disable*/module.exports={messages:JSON.parse("{\\"Hello\\":\\"ÀĥōĴ\\"}")};`;
-exports[`createCompiledCatalog options.pure should return pure catalog 1`] = `
-Object {
- Hello: Ahoj,
-}
-`;
+exports[`createCompiledCatalog options.pseudoLocale should return compiled catalog when pseudoLocale doesn't match current locale 1`] = `/*eslint-disable*/module.exports={messages:JSON.parse("{\\"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 return message key as a fallback translation 1`] = `/*eslint-disable*/module.exports={messages:JSON.parse("{\\"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:"____"}]]}};`;
+exports[`createCompiledCatalog options.strict should't return message key as a fallback in strict mode 1`] = `/*eslint-disable*/module.exports={messages:JSON.parse("{\\"Hello\\":\\"Ahoj\\",\\"Missing\\":\\"\\",\\"Select\\":[[\\"id\\",\\"select\\",{\\"Gen\\":\\"Genesis\\",\\"1John\\":\\"1 John\\",\\"other\\":\\"____\\"}]]}")};`;
diff --git a/packages/cli/src/api/catalog.test.ts b/packages/cli/src/api/catalog.test.ts
index 1ad7e38fb..23de94357 100644
--- a/packages/cli/src/api/catalog.test.ts
+++ b/packages/cli/src/api/catalog.test.ts
@@ -1023,76 +1023,29 @@ describe("order", function () {
})
describe("writeCompiled", function () {
- it("saves ES modules to .mjs files", function () {
- const localeDir = copyFixture(fixture("locales", "initial/"))
- const catalog = new Catalog(
- {
- name: "messages",
- path: path.join(localeDir, "{locale}", "messages"),
- include: [],
- exclude: [],
- },
- mockConfig()
- )
-
- const namespace = "es"
- const compiledCatalog = createCompiledCatalog("en", {}, { namespace })
+ const localeDir = copyFixture(fixture("locales", "initial/"))
+ const catalog = new Catalog(
+ {
+ name: "messages",
+ path: path.join(localeDir, "{locale}", "messages"),
+ include: [],
+ exclude: []
+ },
+ mockConfig()
+ )
+
+ it.each([
+ {namespace: "es", extension: /\.mjs$/},
+ {namespace: "ts", extension: /\.ts$/},
+ {namespace: undefined, extension: /\.js$/},
+ {namespace: 'cjs', extension: /\.js$/},
+ {namespace: 'window.test', extension: /\.js$/},
+ {namespace: 'global.test', extension: /\.js$/}
+ ])('Should save namespace $namespace in $extension extension', ({namespace, extension}) => {
+ const compiledCatalog = createCompiledCatalog("en", {}, {namespace})
// Test that the file extension of the compiled catalog is `.mjs`
expect(catalog.writeCompiled("en", compiledCatalog, namespace)).toMatch(
- /\.mjs$/
- )
- })
-
- it("saves TS modules to .ts files", function () {
- const localeDir = copyFixture(fixture("locales", "initial/"))
- const catalog = new Catalog(
- {
- name: "messages",
- path: path.join(localeDir, "{locale}", "messages"),
- include: [],
- exclude: [],
- },
- mockConfig()
- )
-
- const namespace = "ts"
- const compiledCatalog = createCompiledCatalog("en", {}, { namespace })
- expect(catalog.writeCompiled("en", compiledCatalog, namespace)).toMatch(
- /\.ts$/
- )
- })
-
- it("saves anything else than ES modules to .js files", function () {
- const localeDir = copyFixture(fixture("locales", "initial/"))
- const catalog = new Catalog(
- {
- name: "messages",
- path: path.join(localeDir, "{locale}", "messages"),
- include: [],
- exclude: [],
- },
- mockConfig()
- )
-
- let compiledCatalog = createCompiledCatalog("en", {}, {})
- // Test that the file extension of the compiled catalog is `.js`
- expect(catalog.writeCompiled("en", compiledCatalog)).toMatch(/\.js$/)
-
- compiledCatalog = createCompiledCatalog("en", {}, { namespace: "cjs" })
- expect(catalog.writeCompiled("en", compiledCatalog)).toMatch(/\.js$/)
-
- compiledCatalog = createCompiledCatalog(
- "en",
- {},
- { namespace: "window.test" }
- )
- expect(catalog.writeCompiled("en", compiledCatalog)).toMatch(/\.js$/)
-
- compiledCatalog = createCompiledCatalog(
- "en",
- {},
- { namespace: "global.test" }
+ extension
)
- expect(catalog.writeCompiled("en", compiledCatalog)).toMatch(/\.js$/)
})
})
diff --git a/packages/cli/src/api/compile.test.ts b/packages/cli/src/api/compile.test.ts
index 380f28dd4..adf3fff7f 100644
--- a/packages/cli/src/api/compile.test.ts
+++ b/packages/cli/src/api/compile.test.ts
@@ -1,133 +1,45 @@
-import generate from "@babel/generator"
import { compile, createCompiledCatalog } from "./compile"
describe("compile", () => {
- const getSource = (message: string) =>
- generate(compile(message) as any, {
- compact: true,
- minified: true,
- jsescOption: { minimal: true },
- }).code
-
- it("should optimize string only messages", () => {
- expect(getSource("Hello World")).toEqual('"Hello World"')
- })
-
- it("should allow escaping syntax characters", () => {
- expect(getSource("'{name}'")).toEqual('"{name}"')
- expect(getSource("''")).toEqual('"\'"')
- })
-
- it("should compile arguments", () => {
- expect(getSource("{name}")).toEqual('[["name"]]')
-
- expect(getSource("B4 {name} A4")).toEqual('["B4 ",["name"]," A4"]')
- })
-
- it("should compile arguments with formats", () => {
- expect(getSource("{name, number}")).toEqual('[["name","number"]]')
-
- expect(getSource("{name, number, percent}")).toEqual(
- '[["name","number","percent"]]'
- )
- })
-
- it("should compile plural", () => {
- expect(getSource("{name, plural, one {Book} other {Books}}")).toEqual(
- '[["name","plural",{one:"Book",other:"Books"}]]'
- )
-
- expect(
- getSource("{name, plural, one {Book} other {{name} Books}}")
- ).toEqual('[["name","plural",{one:"Book",other:[["name"]," Books"]}]]')
-
- expect(getSource("{name, plural, one {Book} other {# Books}}")).toEqual(
- '[["name","plural",{one:"Book",other:["#"," Books"]}]]'
- )
-
- expect(
- getSource(
- "{name, plural, offset:1 =0 {No Books} one {Book} other {# Books}}"
- )
- ).toEqual(
- '[["name","plural",{offset:1,0:"No Books",one:"Book",other:["#"," Books"]}]]'
- )
- })
-
- it("should compile select", () => {
- expect(getSource("{name, select, male {He} female {She}}")).toEqual(
- '[["name","select",{male:"He",female:"She"}]]'
- )
-
- expect(getSource("{name, select, male {He} female {{name} She}}")).toEqual(
- '[["name","select",{male:"He",female:[["name"]," She"]}]]'
- )
-
- expect(getSource("{name, select, male {He} female {# She}}")).toEqual(
- '[["name","select",{male:"He",female:"# She"}]]'
- )
- })
-
- 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
+ const getPSource = (message: string) => compile(message, true)
it("should pseudolocalize strings", () => {
- expect(getPSource("Martin Černý")).toEqual('"Màŕţĩń Čēŕńý"')
+ 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("{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"]')
+ 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}")).toEqual([["name", "number"]])
expect(getPSource("{name, number, percent}")).toEqual(
- '[["name","number","percent"]]'
+ [["name", "number", "percent"]]
)
})
it("should not pseudolocalize HTML tags", () => {
expect(getPSource('Martin Černý')).toEqual(
- JSON.stringify('Màŕţĩń Čēŕńý')
+ 'Màŕţĩń Čēŕńý'
)
expect(
getPSource("Martin Cerny 123aČerný")
).toEqual(
- JSON.stringify("Màŕţĩń Ćēŕńŷ 123àČēŕńý")
+ "Màŕţĩń Ćēŕńŷ 123àČēŕńý"
)
expect(getPSource("Martin a")).toEqual(
- JSON.stringify("Màŕţĩń à")
+ "Màŕţĩń à"
)
expect(getPSource("text")).toEqual(
- JSON.stringify("ţēxţ")
+ "ţēxţ"
)
})
@@ -135,7 +47,7 @@ describe("compile", () => {
it("with value", () => {
expect(
getPSource("{value, plural, one {# book} other {# books}}")
- ).toEqual('[["value","plural",{one:["#"," ƀōōķ"],other:["#"," ƀōōķś"]}]]')
+ ).toEqual([["value","plural",{one:["#"," ƀōōķ"],other:["#"," ƀōōķś"]}]])
})
it("with variable placeholder", () => {
@@ -144,7 +56,7 @@ describe("compile", () => {
"{count, plural, one {{countString} book} other {{countString} books}}"
)
).toEqual(
- '[["count","plural",{one:[["countString"]," ƀōōķ"],other:[["countString"]," ƀōōķś"]}]]'
+ [["count","plural",{one:[["countString"]," ƀōōķ"],other:[["countString"]," ƀōōķś"]}]]
)
})
@@ -154,7 +66,7 @@ describe("compile", () => {
"{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"]}]]'
+ [["count","plural",{offset:1,zero:"Ţĥēŕē àŕē ńō mēśśàĝēś",other:["Ţĥēŕē àŕē ","#"," mēśśàĝēś ĩń ŷōũŕ ĩńƀōx"]}]]
)
})
@@ -164,7 +76,7 @@ describe("compile", () => {
"{count, plural, zero {There's # message} other {There are # messages}}"
)
).toEqual(
- '[["count","plural",{zero:["Ţĥēŕē\'ś ","#"," mēśśàĝē"],other:["Ţĥēŕē àŕē ","#"," mēśśàĝēś"]}]]'
+ [["count","plural",{zero:["Ţĥēŕē\'ś ","#"," mēśśàĝē"],other:["Ţĥēŕē àŕē ","#"," mēśśàĝēś"]}]]
)
})
@@ -174,7 +86,7 @@ describe("compile", () => {
"{count, plural, =0 {There's # message} other {There are # messages}}"
)
).toEqual(
- '[["count","plural",{0:["Ţĥēŕē\'ś ","#"," mēśśàĝē"],other:["Ţĥēŕē àŕē ","#"," mēśśàĝēś"]}]]'
+ [["count","plural",{0:["Ţĥēŕē\'ś ","#"," mēśśàĝē"],other:["Ţĥēŕē àŕē ","#"," mēśśàĝēś"]}]]
)
})
})
@@ -185,7 +97,7 @@ describe("compile", () => {
"{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:["#","ţĥ"]}]]'
+ [["count","selectordinal",{offset:1,one:["#","śţ"],two:["#","ńď"],few:["#","ŕď"],4:"4ţĥ",many:"ţēśţMàńŷ",other:["#","ţĥ"]}]]
)
})
@@ -195,13 +107,13 @@ describe("compile", () => {
"{gender, select, male {He} female {She} other {Other}}"
)
).toEqual(
- '[["gender","select",{male:"Ĥē",female:"Śĥē",other:"Ōţĥēŕ"}]]'
+ [["gender","select",{male:"Ĥē",female:"Śĥē",other:"Ōţĥēŕ"}]]
)
})
it("should not pseudolocalize variables", () => {
- expect(getPSource("replace {count}")).toEqual('["ŕēƥĺàćē ",["count"]]')
- expect(getPSource("replace { count }")).toEqual('["ŕēƥĺàćē ",["count"]]')
+ expect(getPSource("replace {count}")).toEqual(["ŕēƥĺàćē ",["count"]])
+ expect(getPSource("replace { count }")).toEqual(["ŕēƥĺàćē ",["count"]])
})
it("Multiple Plurals", () => {
@@ -210,7 +122,7 @@ describe("compile", () => {
"{bcount, plural, one {boy} other {# boys}} {gcount, plural, one {girl} other {# girls}}"
)
).toEqual(
- '[["bcount","plural",{one:"ƀōŷ",other:["#"," ƀōŷś"]}]," ",["gcount","plural",{one:"ĝĩŕĺ",other:["#"," ĝĩŕĺś"]}]]'
+ [["bcount","plural",{one:"ƀōŷ",other:["#"," ƀōŷś"]}]," ",["gcount","plural",{one:"ĝĩŕĺ",other:["#"," ĝĩŕĺś"]}]]
)
})
})
@@ -309,27 +221,6 @@ 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(
diff --git a/packages/cli/src/api/compile.ts b/packages/cli/src/api/compile.ts
index 42e0882c0..5b4daee2c 100644
--- a/packages/cli/src/api/compile.ts
+++ b/packages/cli/src/api/compile.ts
@@ -1,11 +1,8 @@
import * as t from "@babel/types"
-import generate, { GeneratorOptions } from "@babel/generator"
-import { parse } from "messageformat-parser"
-import * as R from "ramda"
-
+import generate, {GeneratorOptions} from "@babel/generator"
+import {compileMessage} from "@lingui/core"
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
type CompiledCatalogType = {
@@ -17,57 +14,57 @@ export type CreateCompileCatalogOptions = {
namespace?: CompiledCatalogNamespace
pseudoLocale?: string
compilerBabelOptions?: GeneratorOptions
- pure?: boolean
-}
-
-/**
- * Transform a single key/value translation into a Babel expression,
- * applying pseudolocalization where necessary.
- */
-function compileSingleKey(key: string, translation: string, shouldPseudolocalize: boolean): t.ObjectProperty {
- return t.objectProperty(t.stringLiteral(key), compile(translation, shouldPseudolocalize))
}
export function createCompiledCatalog(
locale: string,
messages: CompiledCatalogType,
options: CreateCompileCatalogOptions
-) {
- const { strict = false, namespace = "cjs", pseudoLocale, compilerBabelOptions = {}, pure = false } = options
+): string {
+ const {strict = false, namespace = "cjs", pseudoLocale, compilerBabelOptions = {}} = options
const shouldPseudolocalize = locale === pseudoLocale
- const compiledMessages = R.keys(messages).map((key: string) => {
- const value = messages[key];
+ const compiledMessages = Object.keys(messages).reduce((obj, key: string) => {
+ const value = messages[key]
- // If the current ID's value is a context object, create a nested
+ // If the current ID's value is a context object, create a nested
// expression, and assign the current ID to that expression
if (typeof value === "object") {
- const contextExpression = t.objectExpression(Object.keys(value).map((contextKey) => {
- const contextTranslation = value[contextKey];
- return compileSingleKey(contextKey, contextTranslation, shouldPseudolocalize)
- }))
- return t.objectProperty(t.stringLiteral(key), contextExpression);
+ obj[key] = Object.keys(value).reduce((obj, contextKey) => {
+ obj[contextKey] = compile(value[contextKey], shouldPseudolocalize)
+ return obj
+ }, {})
+
+ return obj
}
// Don't use `key` as a fallback translation in strict mode.
- let translation = (messages[key] || (!strict ? key : "")) as string
- return compileSingleKey(key, translation, shouldPseudolocalize)
- })
-
- const ast = pure ? t.objectExpression(compiledMessages) : buildExportStatement(
- t.objectExpression(compiledMessages),
+ const translation = (messages[key] || (!strict ? key : "")) as string
+
+ obj[key] = compile(translation, shouldPseudolocalize)
+ return obj
+ }, {})
+
+ const ast = buildExportStatement(
+ //build JSON.parse() statement
+ t.callExpression(
+ t.memberExpression(
+ t.identifier('JSON'), t.identifier('parse')
+ ),
+ [t.stringLiteral(JSON.stringify(compiledMessages))]
+ ),
namespace
)
const code = generate(ast, {
minified: true,
jsescOption: {
- minimal: true,
+ minimal: true
},
- ...compilerBabelOptions,
+ ...compilerBabelOptions
}).code
- return pure ? JSON.parse(code) : ("/*eslint-disable*/" + code)
+ return "/*eslint-disable*/" + code;
}
function buildExportStatement(expression, namespace: CompiledCatalogNamespace) {
@@ -114,93 +111,7 @@ function buildExportStatement(expression, namespace: CompiledCatalogNamespace) {
* JS arrays, which are handled in client.
*/
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}`
- )
- }
- const ast = processTokens(tokens, shouldPseudolocalize)
-
- if (isString(ast)) return t.stringLiteral(ast)
-
- return ast
-}
-
-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) {
- 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(
- shouldPseudolocalize ? pseudoLocalize(token) : token
- )
-
- // # in plural case
- } else if (token.type === "octothorpe") {
- return t.stringLiteral("#")
-
- // simple argument
- } else if (token.type === "argument") {
- return t.arrayExpression([t.stringLiteral(token.arg)])
-
- // argument with custom format (date, number)
- } else if (token.type === "function") {
- const params = [t.stringLiteral(token.arg), t.stringLiteral(token.key)]
-
- const format = token.param && token.param.tokens[0]
- if (format) {
- params.push(t.stringLiteral(format.trim()))
- }
- return t.arrayExpression(params)
- }
-
- // complex argument with cases
- const formatProps = []
-
- if (token.offset) {
- formatProps.push(
- t.objectProperty(
- t.identifier("offset"),
- t.numericLiteral(parseInt(token.offset))
- )
- )
- }
-
- token.cases.forEach((item) => {
- const inlineTokens = processTokens(item.tokens, shouldPseudolocalize)
- formatProps.push(
- t.objectProperty(
- // if starts with number must be wrapped with quotes
- INVALID_OBJECT_KEY_REGEX.test(item.key) ? t.stringLiteral(item.key) : t.identifier(item.key),
- isString(inlineTokens)
- ? t.stringLiteral(inlineTokens)
- : inlineTokens
- )
- )
- })
-
- const params = [
- t.stringLiteral(token.arg),
- t.stringLiteral(token.type),
- t.objectExpression(formatProps),
- ]
-
- return t.arrayExpression(params)
- })
+ return compileMessage(message, (value) =>
+ shouldPseudolocalize ? pseudoLocalize(value) : value
)
}
-
-const isString = (s) => typeof s === "string"
diff --git a/packages/cli/src/api/formats/index.ts b/packages/cli/src/api/formats/index.ts
index 36ecd9a94..9af84a2a9 100644
--- a/packages/cli/src/api/formats/index.ts
+++ b/packages/cli/src/api/formats/index.ts
@@ -15,7 +15,10 @@ const formats: Record = {
"po-gettext": poGettext,
}
-type CatalogFormatOptionsInternal = {
+/**
+ * @internal
+ */
+export type CatalogFormatOptionsInternal = {
locale: string
} & CatalogFormatOptions
diff --git a/packages/cli/src/api/formats/po-gettext.ts b/packages/cli/src/api/formats/po-gettext.ts
index 4e4a9c08f..1f7e9594e 100644
--- a/packages/cli/src/api/formats/po-gettext.ts
+++ b/packages/cli/src/api/formats/po-gettext.ts
@@ -1,6 +1,6 @@
import { format as formatDate } from "date-fns"
import fs from "fs"
-import ICUParser from "messageformat-parser"
+import {parse as parseIcu, Select, SelectCase, Token} from "@messageformat/parser"
import pluralsCldr from "plurals-cldr"
import PO from "pofile"
import * as R from "ramda"
@@ -8,7 +8,7 @@ import gettextPlurals from "node-gettext/lib/plurals"
import { CatalogType, MessageType } from "../catalog"
import { joinOrigin, splitOrigin, writeFileIfChanged } from "../utils"
-import { CatalogFormatter } from "."
+import type {CatalogFormatOptionsInternal, CatalogFormatter} from "./"
// Workaround because pofile doesn't support es6 modules, see https://github.com/rubenv/pofile/pull/38#issuecomment-623119284
type POItemType = InstanceType
@@ -25,11 +25,11 @@ function getCreateHeaders(language = "no") {
}
// Attempts to turn a single tokenized ICU plural case back into a string.
-function stringifyICUCase(icuCase) {
+function stringifyICUCase(icuCase: SelectCase): string {
return icuCase.tokens
.map((token) => {
- if (typeof token === "string") {
- return token
+ if (token.type === "content") {
+ return token.value
} else if (token.type === "octothorpe") {
return "#"
} else if (token.type === "argument") {
@@ -51,7 +51,7 @@ const LINE_ENDINGS = /\r?\n/g
// Prefix that is used to identitify context information used by this module in PO's "extracted comments".
const CTX_PREFIX = "js-lingui:"
-const serialize = (items: CatalogType, options) =>
+const serialize = (items: CatalogType, options: CatalogFormatOptionsInternal & {disableSelectWarning: boolean}) =>
R.compose(
R.values,
R.mapObjIndexed((message: MessageType, key) => {
@@ -62,7 +62,7 @@ const serialize = (items: CatalogType, options) =>
// The extractedComments array may be modified in this method, so create a new array with the message's elements.
// Destructuring `undefined` is forbidden, so fallback to `[]` if the message has no extracted comments.
item.extractedComments = [...(message.extractedComments ?? [])]
-
+
if (message.context) {
item.msgctxt = message.context
}
@@ -89,7 +89,7 @@ const serialize = (items: CatalogType, options) =>
// Quick check to see if original message is a plural localization.
if (ICU_PLURAL_REGEX.test(_simplifiedMessage)) {
try {
- const [messageAst] = ICUParser.parse(icuMessage)
+ const messageAst = parseIcu(icuMessage)[0] as Select
// Check if any of the plural cases contain plurals themselves.
if (
@@ -133,7 +133,7 @@ const serialize = (items: CatalogType, options) =>
// plural (above) already causes `pofile` to automatically generate `msgstr[0]` and `msgstr[1]`.
if (message.translation?.length > 0) {
try {
- const [ast] = ICUParser.parse(message.translation)
+ const ast = parseIcu(message.translation)[0] as Select
if (ast.cases == null) {
console.warn(
`Found translation without plural cases for key "${key}". ` +
diff --git a/packages/core/package.json b/packages/core/package.json
index 266cc65b2..ad2ee1992 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -46,6 +46,6 @@
"dependencies": {
"@babel/runtime": "^7.11.2",
"make-plural": "^6.2.2",
- "messageformat-parser": "^4.1.3"
+ "@messageformat/parser": "^5.0.0"
}
}
diff --git a/packages/core/src/compileMessage.test.ts b/packages/core/src/compileMessage.test.ts
new file mode 100644
index 000000000..d2de5bc70
--- /dev/null
+++ b/packages/core/src/compileMessage.test.ts
@@ -0,0 +1,154 @@
+import {compileMessage as compile} from "./compileMessage"
+import { mockEnv, mockConsole } from "@lingui/jest-mocks"
+import { interpolate } from "./context"
+import {Locale, Locales} from "./i18n"
+
+describe("compile", () => {
+ const englishPlurals = {
+ plurals(value: number, ordinal: boolean) {
+ if (ordinal) {
+ return { "1": "one", "2": "two", "3": "few" }[value] || "other"
+ } else {
+ return value === 1 ? "one" : "other"
+ }
+ },
+ }
+
+ const prepare = (translation: string, locale?: Locale, locales?: Locales) => {
+ const tokens = compile(translation);
+ return interpolate(tokens, locale || "en", locales, englishPlurals)
+ }
+
+ it("should handle an error if message has syntax errors", () => {
+ mockConsole((console) => {
+ expect(compile("Invalid {message")).toEqual("Invalid {message")
+ expect(console.error).toBeCalledWith(expect.stringMatching('Unexpected message end at line'))
+ })
+ })
+
+ it("should process string chunks with provided map fn", () => {
+ const tokens = compile("Message {value, plural, one {{value} Book} other {# Books}}", (text) => `<${text}>`)
+ expect(tokens).toEqual([
+ "",
+ [
+ "value",
+ "plural",
+ {
+ "one": [
+ [
+ "value"
+ ],
+ "< Book>"
+ ],
+ "other": [
+ "#",
+ "< Books>"
+ ]
+ }
+ ]
+ ])
+ })
+
+
+ it("should compile static message", () => {
+ const cache = compile("Static message")
+ expect(cache).toEqual("Static message")
+
+ mockEnv("production", () => {
+ const cache = compile("Static message")
+ expect(cache).toEqual("Static message")
+ })
+ })
+
+ it("should compile message with variable", () => {
+ const cache = compile("Hey {name}!")
+ expect(interpolate(cache, "en", [], {})({ name: "Joe" })).toEqual(
+ "Hey Joe!"
+ )
+ })
+
+ it("should not interpolate escaped placeholder", () => {
+ const msg = prepare("Hey '{name}'!")
+
+ expect(msg({})).toEqual(
+ "Hey {name}!"
+ )
+ })
+
+ it("should compile plurals", () => {
+ const plural = prepare(
+ "{value, plural, one {{value} Book} other {# Books}}"
+ )
+ expect(plural({ value: 1 })).toEqual("1 Book")
+ expect(plural({ value: 2 })).toEqual("2 Books")
+
+ const offset = prepare(
+ "{value, plural, offset:1 =0 {No Books} one {# Book} other {# Books}}"
+ )
+ expect(offset({ value: 0 })).toEqual("No Books")
+ expect(offset({ value: 2 })).toEqual("1 Book")
+ expect(offset({ value: 3 })).toEqual("2 Books")
+ })
+
+ it("should compile selectordinal", () => {
+ const cache = prepare(
+ "{value, selectordinal, one {#st Book} two {#nd Book}}"
+ )
+ expect(cache({ value: 1 })).toEqual("1st Book")
+ expect(cache({ value: 2 })).toEqual("2nd Book")
+ })
+
+ it("should compile select", () => {
+ const cache = prepare("{value, select, female {She} other {They}}")
+ expect(cache({ value: "female" })).toEqual("She")
+ expect(cache({ value: "n/a" })).toEqual("They")
+ })
+
+ describe("Custom format", () => {
+ const testVector = [
+ ["en", null, "0.1", "10%", "20%", "3/4/2017", "€0.10", "€1.00"],
+ ["fr", null, "0,1", "10 %", "20 %", "04/03/2017", "0,10 €", "1,00 €"],
+ ["fr", "fr-CH", "0,1", "10%", "20%", "04.03.2017", "0.10 €", "1.00 €"]
+ ]
+ testVector.forEach((tc) => {
+ const [
+ locale,
+ locales,
+ expectedNumber,
+ expectedPercent1,
+ expectedPercent2,
+ expectedDate,
+ expectedCurrency1,
+ expectedCurrency2,
+ ] = tc
+
+ it(
+ `should compile custom format for locale=${locale} and locales=${locales}`,
+ () => {
+ const number = prepare("{value, number}", locale, locales)
+ expect(number({ value: 0.1 })).toEqual(expectedNumber)
+
+ const percent = prepare("{value, number, percent}", locale, locales)
+ expect(percent({ value: 0.1 })).toEqual(expectedPercent1)
+ expect(percent({ value: 0.2 })).toEqual(expectedPercent2)
+
+ const now = new Date("3/4/2017")
+ const date = prepare("{value, date}", locale, locales)
+ expect(date({ value: now })).toEqual(expectedDate)
+
+ const formats = {
+ currency: {
+ style: "currency",
+ currency: "EUR",
+ minimumFractionDigits: 2
+ } as Intl.NumberFormatOptions
+ }
+ const currency = prepare("{value, number, currency}", locale, locales)
+ expect(currency({value: 0.1}, formats)).toEqual(expectedCurrency1)
+ expect(currency({value: 1}, formats)).toEqual(expectedCurrency2)
+ }
+ )
+
+ })
+ })
+})
diff --git a/packages/core/src/compileMessage.ts b/packages/core/src/compileMessage.ts
new file mode 100644
index 000000000..d2809f962
--- /dev/null
+++ b/packages/core/src/compileMessage.ts
@@ -0,0 +1,65 @@
+import {Content, parse, Token} from "@messageformat/parser"
+import {CompiledMessage, CompiledMessageToken} from "./i18n"
+
+type MapTextFn = (value: string) => string;
+
+// [Tokens] -> (CTX -> String)
+function processTokens(tokens: Array, mapText?: MapTextFn): CompiledMessage {
+ if (!tokens.filter((token) => token.type !== "content").length) {
+ return tokens.map(token => mapText((token as Content).value)).join("")
+ }
+
+ return tokens.map((token) => {
+ if (token.type === 'content') {
+ return mapText(token.value)
+
+ // # in plural case
+ } else if (token.type === "octothorpe") {
+ return "#"
+
+ // simple argument
+ } else if (token.type === "argument") {
+ return [token.arg]
+
+ // argument with custom format (date, number)
+ } else if (token.type === "function") {
+ const _param = token?.param?.[0] as Content
+
+ if (_param) {
+ return [token.arg, token.key, _param.value.trim()]
+ } else {
+ return [token.arg, token.key]
+ }
+ }
+
+ const offset = token.pluralOffset
+
+ // complex argument with cases
+ const formatProps = {}
+ token.cases.forEach((item) => {
+ formatProps[item.key.replace(/^=(.)+/, "$1")] = processTokens(item.tokens, mapText)
+ })
+
+ return [
+ token.arg,
+ token.type,
+ {
+ offset,
+ ...formatProps,
+ } as any,
+ ] as CompiledMessageToken
+ })
+}
+
+// Message -> (Params -> String)
+export function compileMessage(
+ message: string,
+ mapText: MapTextFn = (v) => v,
+): CompiledMessage {
+ try {
+ return processTokens(parse(message), mapText)
+ } catch (e) {
+ console.error(`${e.message} \n\nMessage: ${message}`)
+ return message
+ }
+}
diff --git a/packages/core/src/context.ts b/packages/core/src/context.ts
index 602c77d75..e14152a2b 100644
--- a/packages/core/src/context.ts
+++ b/packages/core/src/context.ts
@@ -1,20 +1,22 @@
-import { CompiledMessage, Locales } from "./i18n"
+import {CompiledMessage, Formats, LocaleData, Locales, Values} from "./i18n"
import { date, number } from "./formats"
import { isString, isFunction } from "./essentials"
export const UNICODE_REGEX = /\\u[a-fA-F0-9]{4}|\\x[a-fA-F0-9]{2}/g;
const defaultFormats = (
- locale,
- locales,
- localeData = { plurals: undefined },
- formats = {}
+ locale: string,
+ locales: Locales,
+ localeData: LocaleData = { plurals: undefined },
+ formats: Formats = {}
) => {
locales = locales || locale
const { plurals } = localeData
- const style = (format) =>
- isString(format) ? formats[format] || { style: format } : format
- const replaceOctothorpe = (value, message) => {
+ const style = (format: string | T): T =>
+ isString(format)
+ ? formats[format] || { style: format }
+ : format as any
+ const replaceOctothorpe = (value: number, message) => {
return (ctx) => {
const msg = isFunction(message) ? message(ctx) : message
const norm = Array.isArray(msg) ? msg : [msg]
@@ -29,26 +31,28 @@ const defaultFormats = (
}
return {
- plural: (value, { offset = 0, ...rules }) => {
- const message = rules[value] || rules[plurals?.(value - offset)] || rules.other
+ plural: (value: number, { offset = 0, ...rules }) => {
+ const message = rules[value] || rules[plurals?.(value - offset)] || rules.other
+
return replaceOctothorpe(value - offset, message)
},
- selectordinal: (value, { offset = 0, ...rules }) => {
- const message = rules[value] || rules[plurals?.(value - offset, true)] || rules.other
+ selectordinal: (value: number, { offset = 0, ...rules }) => {
+ const message = rules[value] || rules[plurals?.(value - offset, true)] || rules.other
return replaceOctothorpe(value - offset, message)
},
- select: (value, rules) => rules[value] || rules.other,
+ select: (value: string, rules) => rules[value] || rules.other,
- number: (value, format) => number(locales, style(format))(value),
+ number: (value: number, format: string | Intl.NumberFormatOptions) => number(locales, style(format))(value),
- date: (value, format) => date(locales, style(format))(value),
+ date: (value: string, format: string | Intl.DateTimeFormatOptions) => date(locales, style(format))(value),
- undefined: (value) => value,
+ undefined: (value: unknown) => value,
}
}
+
// Params -> CTX
/**
* Creates a context object, which formats ICU MessageFormat arguments based on
@@ -61,7 +65,13 @@ const defaultFormats = (
* @param formats - Custom format styles
* @returns {function(string, string, any)}
*/
-function context({ locale, locales, values, formats, localeData }) {
+function context(
+ locale: string,
+ locales: Locales,
+ values: Values,
+ formats: Formats,
+ localeData: LocaleData
+) {
const formatters = defaultFormats(locale, locales, localeData, formats)
const ctx = (name: string, type: string, format: any): string => {
@@ -78,21 +88,21 @@ export function interpolate(
translation: CompiledMessage,
locale: string,
locales: Locales,
- localeData: Object
+ localeData: LocaleData
) {
- return (values: Object, formats: Object = {}): string => {
- const ctx = context({
+ return (values: Values, formats: Formats = {}): string => {
+ const ctx = context(
locale,
locales,
- localeData,
- formats,
values,
- })
+ formats,
+ localeData,
+ )
- const formatMessage = (message) => {
+ const formatMessage = (message: CompiledMessage): string => {
if (!Array.isArray(message)) return message
- return message.reduce((message, token) => {
+ return message.reduce((message, token) => {
if (isString(token)) return message + token
const [name, type, format] = token
diff --git a/packages/core/src/dev/compile.test.ts b/packages/core/src/dev/compile.test.ts
deleted file mode 100644
index c232ad762..000000000
--- a/packages/core/src/dev/compile.test.ts
+++ /dev/null
@@ -1,121 +0,0 @@
-import compile from "./compile"
-import { mockEnv, mockConsole } from "@lingui/jest-mocks"
-import { interpolate } from "../context"
-
-describe("compile", function () {
- const englishPlurals = {
- plurals(value, ordinal) {
- if (ordinal) {
- return { "1": "one", "2": "two", "3": "few" }[value] || "other"
- } else {
- return value === 1 ? "one" : "other"
- }
- },
- }
-
- const prepare = (translation, locale?, locales?) =>
- interpolate(compile(translation), locale || "en", locales, englishPlurals)
-
- it("should handle an error if message has syntax errors", function () {
- mockConsole((console) => {
- expect(compile("Invalid {{message}}")).toEqual("Invalid {{message}}")
- expect(console.error).toBeCalledWith(
- "Message cannot be parsed due to syntax errors: Invalid {{message}}"
- )
- })
- })
-
- it("should compile static message", function () {
- const cache = compile("Static message")
- expect(cache).toEqual("Static message")
-
- mockEnv("production", () => {
- const cache = compile("Static message")
- expect(cache).toEqual("Static message")
- })
- })
-
- it("should compile message with variable", function () {
- const cache = compile("Hey {name}!")
- expect(interpolate(cache, "en", [], {})({ name: "Joe" })).toEqual(
- "Hey Joe!"
- )
- })
-
- it("should compile plurals", function () {
- const plural = prepare(
- "{value, plural, one {{value} Book} other {# Books}}"
- )
- expect(plural({ value: 1 })).toEqual("1 Book")
- expect(plural({ value: 2 })).toEqual("2 Books")
-
- const offset = prepare(
- "{value, plural, offset:1 =0 {No Books} one {# Book} other {# Books}}"
- )
- expect(offset({ value: 0 })).toEqual("No Books")
- expect(offset({ value: 2 })).toEqual("1 Book")
- expect(offset({ value: 3 })).toEqual("2 Books")
- })
-
- it("should compile selectordinal", function () {
- const cache = prepare(
- "{value, selectordinal, one {#st Book} two {#nd Book}}"
- )
- expect(cache({ value: 1 })).toEqual("1st Book")
- expect(cache({ value: 2 })).toEqual("2nd Book")
- })
-
- it("should compile select", function () {
- const cache = prepare("{value, select, female {She} other {They}}")
- expect(cache({ value: "female" })).toEqual("She")
- expect(cache({ value: "n/a" })).toEqual("They")
- })
-
- const testVector = [
- ["en", null, "0.1", "10%", "20%", "3/4/2017", "€0.10", "€1.00"],
- ["fr", null, "0,1", "10 %", "20 %", "04/03/2017", "0,10 €", "1,00 €"],
- ["fr", "fr-CH", "0,1", "10%", "20%", "04.03.2017", "0.10 €", "1.00 €"],
- ]
- testVector.forEach((tc) => {
- const [
- locale,
- locales,
- expectedNumber,
- expectedPercent1,
- expectedPercent2,
- expectedDate,
- expectedCurrency1,
- expectedCurrency2,
- ] = tc
-
- it(
- "should compile custom format for locale=" +
- locale +
- " and locales=" +
- locales,
- function () {
- const number = prepare("{value, number}", locale, locales)
- expect(number({ value: 0.1 })).toEqual(expectedNumber)
-
- const percent = prepare("{value, number, percent}", locale, locales)
- expect(percent({ value: 0.1 })).toEqual(expectedPercent1)
- expect(percent({ value: 0.2 })).toEqual(expectedPercent2)
-
- const now = new Date("3/4/2017")
- const date = prepare("{value, date}", locale, locales)
- expect(date({ value: now })).toEqual(expectedDate)
-
- const formats = {
- currency: {
- style: "currency",
- currency: "EUR",
- minimumFractionDigits: 2,
- },
- }
- const currency = prepare("{value, number, currency}", locale, locales)
- expect(currency({ value: 0.1 }, formats)).toEqual(expectedCurrency1)
- expect(currency({ value: 1 }, formats)).toEqual(expectedCurrency2)
- }
- )
- })
-})
diff --git a/packages/core/src/dev/compile.ts b/packages/core/src/dev/compile.ts
deleted file mode 100644
index 93d045a44..000000000
--- a/packages/core/src/dev/compile.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import { parse } from "messageformat-parser"
-import { isString } from "../essentials"
-import { CompiledMessage } from "../i18n"
-
-// [Tokens] -> (CTX -> String)
-function processTokens(tokens) {
- if (!tokens.filter((token) => !isString(token)).length) {
- return tokens.join("")
- }
-
- return tokens.map((token) => {
- if (isString(token)) {
- return token
-
- // # in plural case
- } else if (token.type === "octothorpe") {
- return "#"
-
- // simple argument
- } else if (token.type === "argument") {
- return [token.arg]
-
- // argument with custom format (date, number)
- } else if (token.type === "function") {
- const _param = token.param && token.param.tokens[0]
- const param = typeof _param === "string" ? _param.trim() : _param
- return [token.arg, token.key, param].filter(Boolean)
- }
-
- const offset = token.offset ? parseInt(token.offset) : undefined
-
- // complex argument with cases
- const formatProps = {}
- token.cases.forEach((item) => {
- formatProps[item.key] = processTokens(item.tokens)
- })
-
- return [
- token.arg,
- token.type,
- {
- offset,
- ...formatProps,
- },
- ]
- })
-}
-
-// Message -> (Params -> String)
-export default function compile(
- message: string
-): CompiledMessage {
- try {
- return processTokens(parse(message))
- } catch (e) {
- console.error(`Message cannot be parsed due to syntax errors: ${message}`)
- return message
- }
-}
diff --git a/packages/core/src/dev/index.ts b/packages/core/src/dev/index.ts
index e3b6d5b01..32ce04ffa 100644
--- a/packages/core/src/dev/index.ts
+++ b/packages/core/src/dev/index.ts
@@ -1,4 +1,4 @@
-import compile from "./compile"
+import {compileMessage as compile} from "../compileMessage"
import loadLocaleData from "./loadLocaleData"
export { compile, loadLocaleData }
diff --git a/packages/core/src/formats.ts b/packages/core/src/formats.ts
index 7be2099a1..e4707c947 100644
--- a/packages/core/src/formats.ts
+++ b/packages/core/src/formats.ts
@@ -59,4 +59,4 @@ function cacheKey(
) {
const localeKey = Array.isArray(locales) ? locales.sort().join('-') : locales
return `${localeKey}-${JSON.stringify(options)}`
-}
\ No newline at end of file
+}
diff --git a/packages/core/src/i18n.ts b/packages/core/src/i18n.ts
index 49538b62c..f0359b8a4 100644
--- a/packages/core/src/i18n.ts
+++ b/packages/core/src/i18n.ts
@@ -3,27 +3,30 @@ import { isString, isFunction } from "./essentials"
import { date, number } from "./formats"
import * as icu from "./dev"
import { EventEmitter } from "./eventEmitter"
+import type {PluralCategory} from "make-plural"
export type MessageOptions = {
message?: string
context?: string
- formats?: Object
+ formats?: Formats
}
export type Locale = string
export type Locales = Locale | Locale[]
+export type Formats = Record
+
+export type Values = Record;
export type LocaleData = {
- plurals?: Function
+ plurals?: (n: number, ordinal?: boolean) => PluralCategory
}
export type AllLocaleData = Record
-export type CompiledMessage =
- | string
- | Array<
- string | Array>
- >
+export type CompiledIcuChoices = Record & {offset: number};
+export type CompiledMessageToken = string | [name: string, type?: string, format?: null | string | CompiledIcuChoices];
+
+export type CompiledMessage = string | CompiledMessageToken[]
export type Messages = Record
@@ -48,7 +51,7 @@ type setupI18nProps = {
locales?: Locales
messages?: AllMessages
localeData?: AllLocaleData
- missing?: string | ((message, id, context) => string)
+ missing?: string | ((message: string, id: string, context: string) => string)
}
type Events = {
@@ -57,11 +60,11 @@ type Events = {
}
export class I18n extends EventEmitter {
- _locale: Locale
- _locales: Locales
- _localeData: AllLocaleData
- _messages: AllMessages
- _missing: string | ((message, id, context) => string)
+ private _locale: Locale
+ private _locales: Locales
+ private _localeData: AllLocaleData
+ private _messages: AllMessages
+ private _missing: string | ((message, id, context) => string)
constructor(params: setupI18nProps) {
super()
@@ -93,7 +96,7 @@ export class I18n extends EventEmitter {
return this._localeData[this._locale] ?? {}
}
- _loadLocaleData(locale: Locale, localeData: LocaleData) {
+ private _loadLocaleData(locale: Locale, localeData: LocaleData) {
if (this._localeData[locale] == null) {
this._localeData[locale] = localeData
} else {
@@ -120,7 +123,7 @@ export class I18n extends EventEmitter {
this.emit("change")
}
- _load(locale: Locale, messages: Messages) {
+ private _load(locale: Locale, messages: Messages) {
if (this._messages[locale] == null) {
this._messages[locale] = messages
} else {
@@ -168,7 +171,7 @@ export class I18n extends EventEmitter {
// method for translation and formatting
_(
id: MessageDescriptor | string,
- values: Object | undefined = {},
+ values: Values | undefined = {},
{ message, formats, context }: MessageOptions | undefined = {}
) {
if (!isString(id)) {
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index 1e1db5314..d29ca6e6a 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -10,6 +10,10 @@ export {
Locales,
} from "./i18n"
+export {
+ compileMessage
+} from './compileMessage'
+
// Default i18n object
import { setupI18n } from "./i18n"
export const i18n = setupI18n()
diff --git a/packages/jest-mocks/index.ts b/packages/jest-mocks/index.ts
index 321c1f2cc..895fe89d8 100644
--- a/packages/jest-mocks/index.ts
+++ b/packages/jest-mocks/index.ts
@@ -12,7 +12,7 @@ export function getConsoleMockCalls({ mock }) {
return mock.calls.map((call) => call[0]).join("\n")
}
-export function mockConsole(testCase, mock = {}) {
+export function mockConsole(testCase: (console: jest.Mocked) => any, mock = {}) {
function restoreConsole() {
global.console = originalConsole
}
@@ -25,15 +25,14 @@ export function mockConsole(testCase, mock = {}) {
error: jest.fn(),
}
- // @ts-ignore: Lot of console methods are missing
global.console = {
...defaults,
...mock,
- }
+ } as any
let result
try {
- result = testCase(global.console)
+ result = testCase(global.console as jest.Mocked)
} catch (e) {
restoreConsole()
throw e
diff --git a/packages/react/src/Trans.tsx b/packages/react/src/Trans.tsx
index 339d0950d..c84a76584 100644
--- a/packages/react/src/Trans.tsx
+++ b/packages/react/src/Trans.tsx
@@ -14,10 +14,10 @@ export type TransRenderProps = {
export type TransProps = {
id: string
message?: string
- values: Object
+ values: Record
context?: string
components: { [key: string]: React.ElementType | any }
- formats?: Object
+ formats?: Record
children?: React.ReactNode
component?: React.ComponentType
render?: (props: TransRenderProps) => React.ReactElement | null
diff --git a/packages/remote-loader/package.json b/packages/remote-loader/package.json
index 4f7e77972..e3dbcf30c 100644
--- a/packages/remote-loader/package.json
+++ b/packages/remote-loader/package.json
@@ -40,10 +40,7 @@
"esm/"
],
"dependencies": {
- "@babel/generator": "^7.14.5",
- "@babel/types": "^7.14.5",
- "json5": "^2.2.0",
- "messageformat-parser": "^4.1.3",
+ "@lingui/core": "^3.15.0",
"ramda": "^0.27.1"
}
}
diff --git a/packages/remote-loader/src/browserCompiler.ts b/packages/remote-loader/src/browserCompiler.ts
index e9393e8ca..af0544262 100644
--- a/packages/remote-loader/src/browserCompiler.ts
+++ b/packages/remote-loader/src/browserCompiler.ts
@@ -1,112 +1,23 @@
-import * as R from "ramda"
-import * as t from "@babel/types"
-import JSON5 from "json5"
-import generate from "@babel/generator"
-import { parse } from "messageformat-parser"
+import { compileMessage } from "@lingui/core"
export function createBrowserCompiledCatalog(messages: Record) {
- const compiledMessages = R.keys(messages).map((key) => {
- let translation = messages[key] || key
-
- return t.objectProperty(t.stringLiteral(key.toString()), compile(translation))
- })
-
- const ast = t.objectExpression(compiledMessages)
- const code = generate(ast as any, {
- minified: true,
- jsescOption: {
- minimal: true,
+ return Object.keys(messages).reduce((obj, key: string) => {
+ const value = messages[key]
+
+ // If the current ID's value is a context object, create a nested
+ // expression, and assign the current ID to that expression
+ if (typeof value === "object") {
+ obj[key] = Object.keys(value).reduce((obj, contextKey) => {
+ obj[contextKey] = compileMessage(value[contextKey])
+ return obj
+ }, {})
+
+ return obj
}
- }).code
-
- return JSON5.parse(code)
-}
-
-/**
- * Compile string message into AST tree. Message format is parsed/compiled into
- * JS arrays, which are handled in client.
- */
-export function compile(message: string) {
- let tokens: any
- 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}`,
- )
- }
- const ast = processTokens(tokens)
+ const translation = value || key
- if (isString(ast)) return t.stringLiteral(ast)
-
- return ast
+ obj[key] = compileMessage(translation)
+ return obj
+ }, {})
}
-
-function processTokens(tokens: any) {
- // Shortcut - if the message doesn't include any formatting,
- // simply join all string chunks into one message
- if (!tokens.filter((token: any) => !isString(token)).length) {
- return tokens.join("")
- }
-
- return t.arrayExpression(
- tokens.map((token: any) => {
- if (isString(token)) {
- return t.stringLiteral(token)
-
- // # in plural case
- } else if (token.type === "octothorpe") {
- return t.stringLiteral("#")
-
- // simple argument
- } else if (token.type === "argument") {
- return t.arrayExpression([t.stringLiteral(token.arg)])
-
- // argument with custom format (date, number)
- } else if (token.type === "function") {
- const params = [t.stringLiteral(token.arg), t.stringLiteral(token.key)]
-
- const format = token.param && token.param.tokens[0]
- if (format) {
- params.push(t.stringLiteral(format.trim()))
- }
- return t.arrayExpression(params)
- }
-
- // complex argument with cases
- const formatProps = []
-
- if (token.offset) {
- formatProps.push(
- t.objectProperty(
- t.identifier("offset"),
- t.numericLiteral(parseInt(token.offset))
- )
- )
- }
-
- token.cases.forEach((item: any) => {
- const inlineTokens = processTokens(item.tokens)
- formatProps.push(
- t.objectProperty(
- t.identifier(item.key),
- isString(inlineTokens)
- ? t.stringLiteral(inlineTokens)
- : inlineTokens
- )
- )
- })
-
- const params = [
- t.stringLiteral(token.arg),
- t.stringLiteral(token.type),
- t.objectExpression(formatProps),
- ]
-
- return t.arrayExpression(params)
- })
- )
-}
-
-const isString = (s: any) => typeof s === "string"
diff --git a/packages/remote-loader/src/index.ts b/packages/remote-loader/src/index.ts
index e3b8b7f9d..32416c5a5 100644
--- a/packages/remote-loader/src/index.ts
+++ b/packages/remote-loader/src/index.ts
@@ -4,8 +4,8 @@ import PARSERS from "./minimalParser"
type RemoteLoaderOpts = {
format?: "minimal"
- fallbackMessages?: string | Record | T
- messages: string | Record | T
+ fallbackMessages?: string | Record | T
+ messages: string | Record | T
}
function remoteLoader({ format = "minimal", fallbackMessages, messages}: RemoteLoaderOpts) {
@@ -28,6 +28,7 @@ function remoteLoader({ format = "minimal", fallbackMessages, messages}: Remo
`)
}
+ // todo: that will not work with context
const mapTranslationsToInterporlatedString = R.mapObjIndexed(
(_, key) => {
// if there's fallback and translation is empty, return the fallback
diff --git a/packages/remote-loader/test/index.test.ts b/packages/remote-loader/test/index.test.ts
index df5ad4941..068782206 100644
--- a/packages/remote-loader/test/index.test.ts
+++ b/packages/remote-loader/test/index.test.ts
@@ -13,6 +13,7 @@ describe("remote-loader", () => {
someVariable,
select,
Object {
+ offset: undefined,
other: SomeOtherText,
someVarValue: SomeTextHere,
},
@@ -51,6 +52,7 @@ describe("remote-loader", () => {
someVariable,
select,
Object {
+ offset: undefined,
other: SomeOtherText,
someVarValue: SomeTextHere,
},
diff --git a/packages/snowpack-plugin/test/__snapshots__/index.ts.snap b/packages/snowpack-plugin/test/__snapshots__/index.ts.snap
index 98036548b..72f3587f8 100644
--- a/packages/snowpack-plugin/test/__snapshots__/index.ts.snap
+++ b/packages/snowpack-plugin/test/__snapshots__/index.ts.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`snowpack-plugin should return compiled catalog 1`] = `/*eslint-disable*/module.exports={messages:{"Hello World":"Hello World","My name is {name}":["My name is ",["name"]]}};`;
+exports[`snowpack-plugin should return compiled catalog 1`] = `/*eslint-disable*/module.exports={messages:JSON.parse("{\\"Hello World\\":\\"Hello World\\",\\"My name is {name}\\":[\\"My name is \\",[\\"name\\"]]}")};`;
exports[`snowpack-plugin should return error if import doesn't contain extension 1`] = `@lingui/snowpack-plugin: File extension is mandatory, for ex: import('./locales/en/messages.po')`;
diff --git a/scripts/build/modules.js b/scripts/build/modules.js
index f5269b95a..0d3baf6c8 100644
--- a/scripts/build/modules.js
+++ b/scripts/build/modules.js
@@ -12,7 +12,7 @@ const importSideEffects = Object.freeze({
"@babel/runtime-corejs2/helpers/typeof": HAS_NO_SIDE_EFFECTS_ON_IMPORT,
"babel-runtime/core-js/object/get-own-property-names": HAS_NO_SIDE_EFFECTS_ON_IMPORT,
"babel-runtime/helpers/slicedToArray": HAS_NO_SIDE_EFFECTS_ON_IMPORT,
- "messageformat-parser": HAS_NO_SIDE_EFFECTS_ON_IMPORT,
+ "@messageformat/parser": HAS_NO_SIDE_EFFECTS_ON_IMPORT,
"make-plural/umd/plurals": HAS_NO_SIDE_EFFECTS_ON_IMPORT,
"@lingui/core": HAS_NO_SIDE_EFFECTS_ON_IMPORT,
deepFreezeAndThrowOnMutationInDev: HAS_NO_SIDE_EFFECTS_ON_IMPORT
diff --git a/yarn.lock b/yarn.lock
index 5014d966a..03139f1ec 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2004,6 +2004,13 @@
npmlog "^4.1.2"
write-file-atomic "^2.3.0"
+"@messageformat/parser@^5.0.0":
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/@messageformat/parser/-/parser-5.0.0.tgz#5737e69d7d4a469998b527710f1891174fc1b262"
+ integrity sha512-WiDKhi8F0zQaFU8cXgqq69eYFarCnTVxKcvhAONufKf0oUxbqLMW6JX6rV4Hqh+BEQWGyKKKHY4g1XA6bCLylA==
+ dependencies:
+ moo "^0.5.1"
+
"@mrmlnc/readdir-enhanced@^2.2.1":
version "2.2.1"
resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde"
@@ -8888,11 +8895,6 @@ merge2@^1.2.3, merge2@^1.3.0:
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
-messageformat-parser@^4.1.3:
- version "4.1.3"
- resolved "https://registry.yarnpkg.com/messageformat-parser/-/messageformat-parser-4.1.3.tgz#b824787f57fcda7d50769f5b63e8d4fda68f5b9e"
- integrity sha512-2fU3XDCanRqeOCkn7R5zW5VQHWf+T3hH65SzuqRvjatBK7r4uyFa5mEX+k6F9Bd04LVM5G4/BHBTUJsOdW7uyg==
-
micromatch@4.0.2, micromatch@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259"
@@ -9082,6 +9084,11 @@ modify-values@^1.0.0:
resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022"
integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==
+moo@^0.5.1:
+ version "0.5.2"
+ resolved "https://registry.yarnpkg.com/moo/-/moo-0.5.2.tgz#f9fe82473bc7c184b0d32e2215d3f6e67278733c"
+ integrity sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==
+
move-concurrently@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"