Skip to content

Commit

Permalink
feat: add setMessagesCompiler method (#2035)
Browse files Browse the repository at this point in the history
  • Loading branch information
timofei-iatsenko authored Oct 14, 2024
1 parent 927c319 commit baec0ab
Show file tree
Hide file tree
Showing 19 changed files with 247 additions and 82 deletions.
12 changes: 6 additions & 6 deletions packages/cli/src/api/__snapshots__/compile.test.ts.snap
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

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:JSON.parse("{\\"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:JSON.parse("{\\"Hello\\":[\\"Aloh\\xE0\\"]}")};`;

exports[`createCompiledCatalog options.namespace should compile with es 1`] = `/*eslint-disable*/export const messages=JSON.parse("{\\"key\\":[\\"Hello \\",[\\"name\\"]]}");`;

Expand All @@ -16,10 +16,10 @@ 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:JSON.parse("{\\"Hello\\":\\"ÀĥōĴ\\"}")};`;
exports[`createCompiledCatalog options.pseudoLocale should return catalog with pseudolocalized messages 1`] = `/*eslint-disable*/module.exports={messages:JSON.parse("{\\"Hello\\":[\\"ÀĥōĴ\\"]}")};`;

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.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:JSON.parse("{\\"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:JSON.parse("{\\"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\\":[\\"____\\"]}]]}")};`;
36 changes: 18 additions & 18 deletions packages/cli/src/api/compile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ describe("compile", () => {
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ē}"')
})

Expand All @@ -31,18 +31,18 @@ describe("compile", () => {
})

it("should not pseudolocalize HTML tags", () => {
expect(getPSource('Martin <span id="spanId">Černý</span>')).toEqual(
'Màŕţĩń <span id="spanId">Čēŕńý</span>'
)
expect(getPSource('Martin <span id="spanId">Černý</span>')).toEqual([
'Màŕţĩń <span id="spanId">Čēŕńý</span>',
])
expect(
getPSource("Martin Cerny 123a<span id='id'>Černý</span>")
).toEqual("Màŕţĩń Ćēŕńŷ 123à<span id='id'>Čēŕńý</span>")
expect(getPSource("Martin <a title='>>a'>a</a>")).toEqual(
"Màŕţĩń <a title='>>a'>à</a>"
)
expect(getPSource("<a title=TITLE>text</a>")).toEqual(
"<a title=TITLE>ţēxţ</a>"
)
).toEqual(["Màŕţĩń Ćēŕńŷ 123à<span id='id'>Čēŕńý</span>"])
expect(getPSource("Martin <a title='>>a'>a</a>")).toEqual([
"Màŕţĩń <a title='>>a'>à</a>",
])
expect(getPSource("<a title=TITLE>text</a>")).toEqual([
"<a title=TITLE>ţēxţ</a>",
])
})

describe("Plurals", () => {
Expand Down Expand Up @@ -82,7 +82,7 @@ describe("compile", () => {
"plural",
{
offset: 1,
zero: "Ţĥēŕē àŕē ńō mēśśàĝēś",
zero: ["Ţĥēŕē àŕē ńō mēśśàĝēś"],
other: ["Ţĥēŕē àŕē ", "#", " mēśśàĝēś ĩń ŷōũŕ ĩńƀōx"],
},
],
Expand Down Expand Up @@ -138,8 +138,8 @@ describe("compile", () => {
one: ["#", "śţ"],
two: ["#", "ńď"],
few: ["#", "ŕď"],
4: "4ţĥ",
many: "ţēśţMàńŷ",
4: ["4ţĥ"],
many: ["ţēśţMàńŷ"],
other: ["#", "ţĥ"],
},
],
Expand All @@ -155,7 +155,7 @@ describe("compile", () => {
[
"gender",
"select",
{ male: "Ĥē", female: "Śĥē", other: "<span>Ōţĥēŕ</span>" },
{ male: ["Ĥē"], female: ["Śĥē"], other: ["<span>Ōţĥēŕ</span>"] },
],
])
})
Expand All @@ -171,9 +171,9 @@ describe("compile", () => {
"{bcount, plural, one {boy} other {# boys}} {gcount, plural, one {girl} other {# girls}}"
)
).toEqual([
["bcount", "plural", { one: "ƀōŷ", other: ["#", " ƀōŷś"] }],
["bcount", "plural", { one: ["ƀōŷ"], other: ["#", " ƀōŷś"] }],
" ",
["gcount", "plural", { one: "ĝĩŕĺ", other: ["#", " ĝĩŕĺś"] }],
["gcount", "plural", { one: ["ĝĩŕĺ"], other: ["#", " ĝĩŕĺś"] }],
])
})
})
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1 +1 @@
/*eslint-disable*/module.exports={messages:JSON.parse("{\"D+XV65\":\"index page message\"}")};
/*eslint-disable*/module.exports={messages:JSON.parse("{\"D+XV65\":[\"index page message\"]}")};
Original file line number Diff line number Diff line change
@@ -1 +1 @@
/*eslint-disable*/module.exports={messages:JSON.parse("{\"D+XV65\":\"index page message\"}")};
/*eslint-disable*/module.exports={messages:JSON.parse("{\"D+XV65\":[\"index page message\"]}")};

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

68 changes: 64 additions & 4 deletions packages/core/src/i18n.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { setupI18n } from "./i18n"
import { mockConsole, mockEnv } from "@lingui/jest-mocks"
import { compileMessage } from "@lingui/message-utils/compileMessage"

describe("I18n", () => {
describe("I18n.load", () => {
Expand Down Expand Up @@ -262,7 +263,7 @@ describe("I18n", () => {
).toEqual("Mi 'nombre' es {name}")
})

it("._ shouldn't compile messages in production", () => {
it("._ shouldn't compile uncompiled messages in production", () => {
const messages = {
Hello: "Salut",
"My name is {name}": "Je m'appelle {name}",
Expand All @@ -281,23 +282,82 @@ describe("I18n", () => {
})
})

it("._ shouldn't compiled message from catalogs in development", () => {
it("._ should use compiled message in production", () => {
const messages = {
Hello: "Salut",
"My name is {name}": compileMessage("Je m'appelle {name}"),
}

mockEnv("production", () => {
const { setupI18n } = require("@lingui/core")
const i18n = setupI18n({
locale: "fr",
messages: { fr: messages },
})

expect(i18n._("My name is {name}", { name: "Fred" })).toEqual(
"Je m'appelle Fred"
)
})
})

it("._ shouldn't double compile message in development", () => {
const messages = {
Hello: "Salut",
"My name is {name}": compileMessage("Je m'appelle '{name}'"),
}

const { setupI18n } = require("@lingui/core")
const i18n = setupI18n({
locale: "fr",
messages: { fr: messages },
})

expect(i18n._("My name is {name}", { name: "Fred" })).toEqual(
"Je m'appelle {name}"
)
})

it("setMessagesCompiler should register a message compiler for production", () => {
const messages = {
Hello: "Salut",
"My name is {name}": "Je m'appelle {name}",
}

mockEnv("development", () => {
mockEnv("production", () => {
const { setupI18n } = require("@lingui/core")
const i18n = setupI18n({
locale: "fr",
messages: { fr: messages },
})

expect(i18n._("My name is {name}")).toEqual("Je m'appelle {name}")
i18n.setMessagesCompiler(compileMessage)
expect(i18n._("My name is {name}", { name: "Fred" })).toEqual(
"Je m'appelle Fred"
)
})
})

it("should print warning if uncompiled message is used", () => {
expect.assertions(1)

const messages = {
Hello: "Salut",
}

mockEnv("production", () => {
mockConsole((console) => {
const { setupI18n } = require("@lingui/core")
const i18n = setupI18n({
locale: "fr",
messages: { fr: messages },
})

i18n._("Hello")
expect(console.warn).toBeCalled()
})
})
})
it("._ should emit missing event for missing translation", () => {
const i18n = setupI18n({
locale: "en",
Expand Down
59 changes: 48 additions & 11 deletions packages/core/src/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ export type LocaleData = {
*/
export type AllLocaleData = Record<Locale, LocaleData>

export type Messages = Record<string, CompiledMessage>
export type UncompiledMessage = string
export type Messages = Record<string, UncompiledMessage | CompiledMessage>

export type AllMessages = Record<Locale, Messages>

Expand Down Expand Up @@ -79,16 +80,23 @@ type LoadAndActivateOptions = {
messages: Messages
}

export type MessageCompiler = (message: string) => CompiledMessage

export class I18n extends EventEmitter<Events> {
private _locale: Locale = ""
private _locales?: Locales
private _localeData: AllLocaleData = {}
private _messages: AllMessages = {}
private _missing?: MissingHandler
private _messageCompiler?: MessageCompiler

constructor(params: I18nProps) {
super()

if (process.env.NODE_ENV !== "production") {
this.setMessagesCompiler(compileMessage)
}

if (params.missing != null) this._missing = params.missing
if (params.messages != null) this.load(params.messages)
if (params.localeData != null) this.loadLocaleData(params.localeData)
Expand Down Expand Up @@ -125,6 +133,26 @@ export class I18n extends EventEmitter<Events> {
}
}

/**
* Registers a `MessageCompiler` to enable the use of uncompiled catalogs at runtime.
*
* In production builds, the `MessageCompiler` is typically excluded to reduce bundle size.
* By default, message catalogs should be precompiled during the build process. However,
* if you need to compile catalogs at runtime, you can use this method to set a message compiler.
*
* Example usage:
*
* ```ts
* import { compileMessage } from "@lingui/message-utils/compileMessage";
*
* i18n.setMessagesCompiler(compileMessage);
* ```
*/
setMessagesCompiler(compiler: MessageCompiler) {
this._messageCompiler = compiler
return this
}

/**
* @deprecated Plurals automatically used from Intl.PluralRules you can safely remove this call. Deprecated in v4
*/
Expand Down Expand Up @@ -237,16 +265,25 @@ export class I18n extends EventEmitter<Events> {
this.emit("missing", { id, locale: this._locale })
}

// To avoid double compilation, skip compilation for `messageForId`, because message from catalog should be already compiled
// ref: https://github.com/lingui/js-lingui/issues/1901
const translation =
messageForId ||
(() => {
const trans: CompiledMessage | string = message || id
return process.env.NODE_ENV !== "production"
? compileMessage(trans)
: trans
})()
let translation = messageForId || message || id

// Compiled message is always an array (`["Ola!"]`).
// If a message comes as string - it's not compiled, and we need to compile it beforehand.
if (isString(translation)) {
if (this._messageCompiler) {
translation = this._messageCompiler(translation)
} else {
console.warn(`Uncompiled message detected! Message:
> ${translation}
That means you use raw catalog or your catalog doesn't have a translation for the message and fallback was used.
ICU features such as interpolation and plurals will not work properly for that message.
Please compile your catalog first.
`)
}
}

// hack for parsing unicode values inside a string to get parsed in react native environments
if (isString(translation) && UNICODE_REGEX.test(translation))
Expand Down
20 changes: 15 additions & 5 deletions packages/loader/test/__snapshots__/loader.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

exports[`lingui-loader should compile catalog in json format 1`] = `
{
key: Message,
key: [
Message,
],
key2: [
Hello ,
[
Expand All @@ -14,26 +16,34 @@ exports[`lingui-loader should compile catalog in json format 1`] = `

exports[`lingui-loader should compile catalog in po format 1`] = `
{
ED2Xk0: String from template,
ED2Xk0: [
String from template,
],
mVmaLu: [
My name is ,
[
name,
],
],
mY42CM: Hello World,
mY42CM: [
Hello World,
],
}
`;

exports[`lingui-loader should compile catalog with relative path with no warnings 1`] = `
{
ED2Xk0: String from template,
ED2Xk0: [
String from template,
],
mVmaLu: [
My name is ,
[
name,
],
],
mY42CM: Hello World,
mY42CM: [
Hello World,
],
}
`;
Loading

0 comments on commit baec0ab

Please sign in to comment.