From bd2014643d31d4f8f8f3558d7f8baf3262df4766 Mon Sep 17 00:00:00 2001 From: Timofei Iatsenko Date: Tue, 21 Mar 2023 11:14:51 +0100 Subject: [PATCH] feat(core): add i18n.setCatalogAndActivate for easier nextjs integration --- packages/core/src/i18n.test.ts | 65 ++++++++++++++++++++++++++++++++++ packages/core/src/i18n.ts | 28 +++++++++++++++ website/docs/ref/core.md | 58 +++++++++++++++++++----------- 3 files changed, 130 insertions(+), 21 deletions(-) diff --git a/packages/core/src/i18n.test.ts b/packages/core/src/i18n.test.ts index 9493e0ac9..349f4615a 100644 --- a/packages/core/src/i18n.test.ts +++ b/packages/core/src/i18n.test.ts @@ -125,6 +125,71 @@ describe("I18n", () => { }) }) + describe("I18n.loadAndActivate", () => { + it("should set locale and messages", () => { + const i18n = setupI18n() + + const cbChange = jest.fn() + i18n.on("change", cbChange) + + i18n.loadAndActivate("en", { + message: "My Message", + }) + + expect(i18n.locale).toEqual("en") + expect(i18n.locales).toBeNull() + + expect(cbChange).toBeCalled() + }) + + it("should don't emit event if notify = false", () => { + const i18n = setupI18n() + + const cbChange = jest.fn() + i18n.on("change", cbChange) + + i18n.loadAndActivate( + "en", + { + message: "My Message", + }, + false + ) + + expect(cbChange).not.toBeCalled() + }) + + it("should support locales as array", () => { + const i18n = setupI18n() + + i18n.loadAndActivate(["en-GB", "en"], { + message: "My Message", + }) + + expect(i18n.locale).toEqual("en-GB") + expect(i18n.locales).toEqual(["en-GB", "en"]) + }) + + it("should override existing data", () => { + const i18n = setupI18n({ + locale: "en", + locales: ["en-GB", "en"], + messages: { + en: { + message: "My Message", + }, + }, + }) + + i18n.loadAndActivate("ru", { + message: "My Message", + }) + + expect(i18n.locale).toEqual("ru") + expect(i18n.locales).toBeNull() + }) + }) + it("._ should format message from catalog", () => { const messages = { Hello: "Salut", diff --git a/packages/core/src/i18n.ts b/packages/core/src/i18n.ts index db6f355c8..59a706111 100644 --- a/packages/core/src/i18n.ts +++ b/packages/core/src/i18n.ts @@ -178,6 +178,34 @@ export class I18n extends EventEmitter { this.emit("change") } + /** + * @param locales one locale or array of locales. + * If array of locales is passed they would be used as fallback + * locales for date and number formatting + * @param messages compiled message catalog + * @param notify Should emit `change` event for all subscribers. + * This is useful for integration with frameworks as NextJS to avoid race-conditions during initialization. + */ + loadAndActivate( + locales: Locale | Locales, + messages: Messages, + notify = true + ) { + if (Array.isArray(locales)) { + this._locale = locales[0] + this._locales = locales + } else { + this._locale = locales + this._locales = null + } + + this._messages[this._locale] = messages + + if (notify) { + this.emit("change") + } + } + activate(locale: Locale, locales?: Locales) { if (process.env.NODE_ENV !== "production") { if (!this._messages[locale]) { diff --git a/website/docs/ref/core.md b/website/docs/ref/core.md index 6533f2e7c..f4f909b72 100644 --- a/website/docs/ref/core.md +++ b/website/docs/ref/core.md @@ -12,7 +12,7 @@ npm install --save @lingui/core `@lingui/core` package exports the global instance of `i18n` object. Simply import it and use it: -```tsx +```ts import { i18n } from "@lingui/core" /** @@ -22,8 +22,7 @@ import { i18n } from "@lingui/core" */ async function activate(locale: string) { const { messages } = await import(`${locale}/messages.js`) - i18n.load(locale, messages) - i18n.activate(locale) + i18n.loadAndActivate(locale, messages) } activate("cs") @@ -34,7 +33,7 @@ const translation = i18n._("Hello World") If you don't want to use the global `i18n` instance and you want to setup your own, you can use [`setupI18n`](#setupi18n) method. You also need to set [`runtimeConfigModule`](/docs/ref/conf.md#runtimeconfigmodule) for macros to work correctly: -``` js +```ts // If you import `i18n` object from custom module like this: import { i18n } from "./custom-i18n-config" @@ -51,7 +50,7 @@ import { i18n } from "./custom-i18n-config" Load catalog for given locale or load multiple catalogs at once. -``` js +```ts import { i18n } from "@lingui/core" const messages = { @@ -90,7 +89,7 @@ Formatting of messages as strings (e.g: `"My name is {name}"`) works in developm The same example would in real application look like this: -``` js +```ts import { i18n } from "@lingui/core" // File generated by `lingui compile` @@ -104,7 +103,7 @@ i18n.load('en', messagesEn) Activate a locale and locales. `_` from now on will return messages in given locale. -``` js +```ts import { i18n } from "@lingui/core" i18n.activate("en") @@ -114,6 +113,23 @@ i18n.activate("cs") i18n._("Hello") // Return "Hello" in Czech ``` +### `i18n.loadAndActivate(locales: Locale | Locales, messages: Messages, notify = true)` {#i18n.loadAndActivate} + +Load catalog and activate given locale. This method is `i18n.load()` + `i18n.activate()` in one pass. + +`locales` Could be one locale or array of locales. If array of locales is passed they would be used as fallback locales for date and number formatting. + +`messages` **compiled** message catalog. + +`notify` Should emit `change` event for all subscribers. This is useful for integration with frameworks as NextJS to avoid race-conditions during initialization. + +```ts +import { i18n } from "@lingui/core" + +const { messages } = await import(`${locale}/messages.js`) +i18n.loadAndActivate(locale, messages) +``` + ### `i18n._(messageId[, values[, options]])` {#i18n._} The core method for translating and formatting messages. @@ -124,7 +140,7 @@ The core method for translating and formatting messages. `options.message` is the default translation (optional). This is mostly used when application doesn't use message IDs in natural language (e.g.: `msg.id` or `Component.title`). -``` js +```ts import { i18n } from "@lingui/core" // Simple message @@ -145,7 +161,7 @@ i18n._("msg.id", { name: "Tom" }, { message: "My name is {name}" }) `format` is an object passed to the `options` argument of the [Intl.DateTimeFormat constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat) (optional). -``` js +```ts import { i18n } from "@lingui/core" const d = new Date("2021-07-23T16:23:00") @@ -175,7 +191,7 @@ Format a number using the conventional format for the active language. `format` is an object passed to the `options` argument of the [Intl.NumberFormat constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat) (optional). -``` js +```ts import { i18n } from "@lingui/core" i18n.activate("en") @@ -206,7 +222,7 @@ In most cases you can use the global `i18n` object exported from `@lingui/core` However, if you do need to setup your own `i18n` instance, remember to also set [`runtimeConfigModule`](/docs/ref/conf.md#runtimeconfigmodule) work macros to work properly: -``` js +```ts // If you import `i18n` object from custom module like this: import { i18n } from "./custom-i18n-config" @@ -215,7 +231,7 @@ import { i18n } from "./custom-i18n-config" ``` ::: -``` js +```ts import { setupI18n } from "@lingui/core" const i18n = setupI18n() @@ -227,7 +243,7 @@ The factory function accepts one optional parameter, `options`: Initial active locale. -``` jsx +```tsx import { setupI18n } from "@lingui/core" const i18n = setupI18n({ locale: "en" }) @@ -241,7 +257,7 @@ const i18n = setupI18n({ locale: "en" }) List of alternative locales (BCP 47 language tags) which are used for number and date formatting (some countries use more than one number/date format). If not set, active locale is used instead. -``` jsx +```tsx import { setupI18n } from "@lingui/core" const i18n = setupI18n({ @@ -258,7 +274,7 @@ const i18n = setupI18n({ Initial [`Messages`](#messages). -``` jsx +```tsx import { setupI18n } from "@lingui/core" const messages: { @@ -276,7 +292,7 @@ const i18n = setupI18n({ messages }) Custom message to be returned when translation is missing. This is useful for debugging: -``` jsx +```tsx import { setupI18n } from "@lingui/core" const i18n = setupI18n({ missing: "🚨" }) @@ -285,7 +301,7 @@ i18n._('missing translation') === "🚨" This might be also a function which is called with active locale and message ID: -``` jsx +```tsx import { setupI18n } from "@lingui/core" function missing(locale, id) { @@ -301,7 +317,7 @@ i18n._('missing translation') // raises alert Type of `catalogs` parameters in [`I18n.load`](#i18n.load(catalogs)) method: -``` js +```ts type Catalogs = {[locale: string]: Catalog} // Example: @@ -325,7 +341,7 @@ const catalogs: Catalogs = { Message catalog contains messages and language data (plurals). This object is usually generated in CLI: -``` js +```ts type Catalog = { languageData: { plurals: Function @@ -338,7 +354,7 @@ type Catalog = { Type of messages in [`Catalogs`](#catalogs). It's a mapping of a **messageId** to a translation in given language. This may be a function if messages are compiled. -``` js +```ts type Messages = {[messageId: string]: string | Function} // Example @@ -358,7 +374,7 @@ Triggered **after** locale is changed or new catalog is loaded. There are no arg Triggered when a translation is requested with [`i18n._`](/docs/ref/core.md#i18n._) that does not exist in the active locale's messages. Information about the locale and message are available from the event. -``` js +```ts i18n.on('missing', (event) => { alert(`alert(`Translation in ${event.locale} for ${event.id} is missing!`)`) })