From 7e04168c8613ee4095750ba4ee37a92b2640d66b Mon Sep 17 00:00:00 2001 From: Vojtech Novak Date: Wed, 26 Apr 2023 10:03:59 +0200 Subject: [PATCH] feat: refactor loadAndActivate (#1609) --- examples/create-react-app/src/i18n.ts | 2 +- examples/nextjs-babel/src/i18n.ts | 21 +++++++- examples/nextjs-swc/src/utils.ts | 21 +++++++- examples/vite-project-react-babel/src/i18n.ts | 2 +- packages/core/src/i18n.test.ts | 43 +++++++---------- packages/core/src/i18n.ts | 35 ++++++-------- website/docs/ref/core.md | 48 ++++++++++--------- 7 files changed, 97 insertions(+), 75 deletions(-) diff --git a/examples/create-react-app/src/i18n.ts b/examples/create-react-app/src/i18n.ts index 59d0b7c94..53663066e 100644 --- a/examples/create-react-app/src/i18n.ts +++ b/examples/create-react-app/src/i18n.ts @@ -35,7 +35,7 @@ const catalogs: Record Promise> = { */ export async function dynamicActivate(locale: string) { const messages = await catalogs[locale as any]() - i18n.loadAndActivate(locale, messages) + i18n.loadAndActivate({ locale, messages }) } // If not we can just load all the catalogs and do a simple i18n.active(localeToActive) diff --git a/examples/nextjs-babel/src/i18n.ts b/examples/nextjs-babel/src/i18n.ts index 7cc981a52..062c3c5c5 100644 --- a/examples/nextjs-babel/src/i18n.ts +++ b/examples/nextjs-babel/src/i18n.ts @@ -1,5 +1,6 @@ import { i18n, Messages } from "@lingui/core" import { useRouter } from "next/router" +import { useEffect } from "react" /** * Load messages for requested locale and activate it. @@ -14,7 +15,25 @@ export async function loadCatalog(locale: string) { export function useLinguiInit(messages: Messages) { const router = useRouter() const locale = router.locale || router.defaultLocale! - i18n.loadAndActivate(locale, messages, false) + const isClient = typeof window !== "undefined" + + if (!isClient && locale !== i18n.locale) { + // there is single instance of i18n on the server + // note: on the server, we could have an instance of i18n per supported locale + // to avoid calling loadAndActivate for (worst case) each request, but right now that's what we do + i18n.loadAndActivate({ locale, messages }) + } + if (isClient && i18n.locale === undefined) { + // first client render + i18n.loadAndActivate({ locale, messages }) + } + + useEffect(() => { + const localeDidChange = locale !== i18n.locale + if (localeDidChange) { + i18n.loadAndActivate({ locale, messages }) + } + }, [locale]) return i18n } diff --git a/examples/nextjs-swc/src/utils.ts b/examples/nextjs-swc/src/utils.ts index 0709c859c..db3c8c628 100644 --- a/examples/nextjs-swc/src/utils.ts +++ b/examples/nextjs-swc/src/utils.ts @@ -1,5 +1,6 @@ import { i18n, Messages } from '@lingui/core' import { useRouter } from 'next/router' +import { useEffect } from 'react' export async function loadCatalog(locale: string) { const catalog = await import(`@lingui/loader!./locales/${locale}.po`) @@ -9,7 +10,25 @@ export async function loadCatalog(locale: string) { export function useLinguiInit(messages: Messages) { const router = useRouter() const locale = router.locale || router.defaultLocale! - i18n.loadAndActivate(locale, messages, false) + const isClient = typeof window !== 'undefined' + + if (!isClient && locale !== i18n.locale) { + // there is single instance of i18n on the server + // note: on the server, we could have an instance of i18n per supported locale + // to avoid calling loadAndActivate for (worst case) each request, but right now that's what we do + i18n.loadAndActivate({ locale, messages }) + } + if (isClient && i18n.locale === undefined) { + // first client render + i18n.loadAndActivate({ locale, messages }) + } + + useEffect(() => { + const localeDidChange = locale !== i18n.locale + if (localeDidChange) { + i18n.loadAndActivate({ locale, messages }) + } + }, [locale]) return i18n } diff --git a/examples/vite-project-react-babel/src/i18n.ts b/examples/vite-project-react-babel/src/i18n.ts index 1d60f3bb1..55867f597 100644 --- a/examples/vite-project-react-babel/src/i18n.ts +++ b/examples/vite-project-react-babel/src/i18n.ts @@ -7,5 +7,5 @@ import { i18n } from "@lingui/core" */ export async function loadCatalog(locale: string) { const catalog = await import(`./locales/${locale}.po`) - i18n.loadAndActivate(locale, catalog.messages) + i18n.loadAndActivate({ locale, messages: catalog.messages }) } diff --git a/packages/core/src/i18n.test.ts b/packages/core/src/i18n.test.ts index 709894add..73d47e23b 100644 --- a/packages/core/src/i18n.test.ts +++ b/packages/core/src/i18n.test.ts @@ -132,42 +132,28 @@ describe("I18n", () => { const cbChange = jest.fn() i18n.on("change", cbChange) - i18n.loadAndActivate("en", { - message: "My Message", + i18n.loadAndActivate({ + locale: "en", + messages: { message: "My Message" }, }) expect(i18n.locale).toEqual("en") - expect(i18n.locales).toBeNull() + expect(i18n.locales).toBeUndefined() 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", + i18n.loadAndActivate({ + locale: "ar", + locales: ["en-UK", "ar-AS"], + messages: { message: "My Message" }, }) - expect(i18n.locale).toEqual("en-GB") - expect(i18n.locales).toEqual(["en-GB", "en"]) + expect(i18n.locale).toEqual("ar") + expect(i18n.locales).toEqual(["en-UK", "ar-AS"]) }) it("should override existing data", () => { @@ -181,12 +167,15 @@ describe("I18n", () => { }, }) - i18n.loadAndActivate("ru", { - message: "My Message", + i18n.loadAndActivate({ + locale: "ru", + messages: { + message: "My Message", + }, }) expect(i18n.locale).toEqual("ru") - expect(i18n.locales).toBeNull() + expect(i18n.locales).toBeUndefined() }) }) diff --git a/packages/core/src/i18n.ts b/packages/core/src/i18n.ts index 89038c2c3..c83bf7a76 100644 --- a/packages/core/src/i18n.ts +++ b/packages/core/src/i18n.ts @@ -70,6 +70,15 @@ type Events = { missing: (event: MissingMessageEvent) => void } +type LoadAndActivateOptions = { + /** initial active locale */ + locale: Locale + /** list of alternative locales (BCP 47 language tags) which are used for number and date formatting */ + locales?: Locales + /** compiled message catalog */ + messages: Messages +} + export class I18n extends EventEmitter { private _locale: Locale private _locales: Locales @@ -173,31 +182,15 @@ export class I18n extends EventEmitter { } /** - * @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. + * @param options {@link LoadAndActivateOptions} */ - 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 - } + loadAndActivate({ locale, locales, messages }: LoadAndActivateOptions) { + this._locale = locale + this._locales = locales || undefined this._messages[this._locale] = messages - if (notify) { - this.emit("change") - } + this.emit("change") } activate(locale: Locale, locales?: Locales) { diff --git a/website/docs/ref/core.md b/website/docs/ref/core.md index 96f2795d1..3785cf5bd 100644 --- a/website/docs/ref/core.md +++ b/website/docs/ref/core.md @@ -45,10 +45,29 @@ import { i18n } from "./custom-i18n-config" ### Class `i18n()` {#i18n} -### `i18n.load(catalogs: Catalogs)` {#i18n.load(catalogs)} -### `i18n.load(locale: string, catalog: Catalog)` {#i18n.load} +### `i18n.loadAndActivate(options)` {#i18n.loadAndActivate} -Load catalog for given locale or load multiple catalogs at once. +`options` is an object with following properties: + +- `locale`: initial active locale +- `locales`: list of alternative locales (BCP 47 language tags) which are used for number and date formatting +- `messages`: **compiled** message catalog + +Sets (overwrites) the catalog for given locale and activates the locale. + +```ts +import { i18n } from "@lingui/core" + +const { messages } = await import(`${locale}/messages.js`) +i18n.loadAndActivate({ locale, messages }) +``` + +### `i18n.load(allMessages: AllMessages)` {#i18n.load(allMessages)} +### `i18n.load(locale: string, messages: Messages)` {#i18n.load} + +Load messages for given locale or load multiple message catalogs at once. + +When some messages for the provided locale are already loaded, calling `i18n.load` will merge the new messages with the existing ones using `Object.assign`. ```ts import { i18n } from "@lingui/core" @@ -101,7 +120,7 @@ i18n.load('en', messagesEn) ### `i18n.activate(locale[, locales])` {#i18n.activate} -Activate a locale and locales. `_` from now on will return messages in given locale. +Activate a locale and locales. From now on, calling `i18n._` will return messages in given locale. ```ts import { i18n } from "@lingui/core" @@ -113,23 +132,6 @@ 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. @@ -299,7 +301,7 @@ const i18n = setupI18n({ // This is a shortcut for: // const i18n = setupI18n() -// i18n.activate("en", ["en-UK", "ar-AS"]) +// i18n.activate("ar", ["en-UK", "ar-AS"]) ``` ### `options.messages` @@ -347,7 +349,7 @@ i18n._('missing translation') // raises alert ### Catalogs -Type of `catalogs` parameters in [`I18n.load`](#i18n.load(catalogs)) method: +Type of `catalogs` parameters in [`I18n.load`](#i18n.load) method: ```ts type Catalogs = {[locale: string]: Catalog}