Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add setMessagesCompiler method #2035

Merged
merged 5 commits into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
timofei-iatsenko marked this conversation as resolved.
Show resolved Hide resolved
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
Loading