feat(i18n): add Lingui runtime and development tooling
- add SSR-safe I18nProvider, Translate, locale hooks, catalog activation, and typed runtime APIs - add consumer-owned Lingui configuration helpers and a CLI for locale creation, extraction, compilation, and project discovery - add catalog read/write services plus a Vite development API plugin scoped to each consuming application - add the portal-based I18nDevtool, message editor, dark-theme tracking, and standalone Message Studio - document the consumer workflow and cover runtime, configuration, catalog, CLI, and Devtool behavior with tests
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
import * as React from "react"
|
||||
import { setupI18n, type AllMessages, type I18n } from "@lingui/core"
|
||||
import {
|
||||
I18nProvider as LinguiI18nProvider,
|
||||
type I18nProviderProps as LinguiI18nProviderProps,
|
||||
} from "@lingui/react"
|
||||
|
||||
import { I18nRuntimeContext } from "./context"
|
||||
import type { LocaleDefinition, LocaleDirection, LocaleInput } from "./types"
|
||||
|
||||
const RTL_LANGUAGES = new Set([
|
||||
"ar",
|
||||
"ckb",
|
||||
"dv",
|
||||
"fa",
|
||||
"he",
|
||||
"ku",
|
||||
"ps",
|
||||
"sd",
|
||||
"ug",
|
||||
"ur",
|
||||
"yi",
|
||||
])
|
||||
|
||||
function inferLocaleDirection(locale: string): LocaleDirection {
|
||||
const language = locale.split("-")[0]?.toLowerCase()
|
||||
|
||||
return language && RTL_LANGUAGES.has(language) ? "rtl" : "ltr"
|
||||
}
|
||||
|
||||
function normalizeLocales(
|
||||
locale: string,
|
||||
locales: readonly LocaleInput[] | undefined,
|
||||
catalogs: AllMessages
|
||||
): readonly LocaleDefinition[] {
|
||||
const inputs =
|
||||
locales && locales.length > 0
|
||||
? locales
|
||||
: Array.from(new Set([locale, ...Object.keys(catalogs)]))
|
||||
|
||||
const definitions = inputs.map<LocaleDefinition>((input) => {
|
||||
const definition = typeof input === "string" ? { locale: input } : input
|
||||
|
||||
return {
|
||||
...definition,
|
||||
direction:
|
||||
definition.direction ?? inferLocaleDirection(definition.locale),
|
||||
label: definition.label ?? definition.locale,
|
||||
}
|
||||
})
|
||||
|
||||
if (!definitions.some((definition) => definition.locale === locale)) {
|
||||
return [
|
||||
...definitions,
|
||||
{
|
||||
direction: inferLocaleDirection(locale),
|
||||
label: locale,
|
||||
locale,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
return definitions
|
||||
}
|
||||
|
||||
export interface I18nProviderProps extends Pick<
|
||||
LinguiI18nProviderProps,
|
||||
"children" | "defaultComponent"
|
||||
> {
|
||||
catalogs?: AllMessages
|
||||
i18n?: I18n
|
||||
locale: string
|
||||
locales?: readonly LocaleInput[]
|
||||
missing?: ConstructorParameters<typeof I18n>[0]["missing"]
|
||||
}
|
||||
|
||||
export function I18nProvider({
|
||||
catalogs = {},
|
||||
children,
|
||||
defaultComponent,
|
||||
i18n: providedI18n,
|
||||
locale,
|
||||
locales,
|
||||
missing,
|
||||
}: I18nProviderProps) {
|
||||
const i18n = React.useMemo(() => {
|
||||
const instance =
|
||||
providedI18n ??
|
||||
setupI18n({
|
||||
locale,
|
||||
messages: catalogs,
|
||||
missing,
|
||||
})
|
||||
|
||||
if (providedI18n) {
|
||||
providedI18n.load(catalogs)
|
||||
providedI18n.activate(locale)
|
||||
}
|
||||
|
||||
return instance
|
||||
}, [catalogs, locale, missing, providedI18n])
|
||||
const normalizedLocales = React.useMemo(
|
||||
() => normalizeLocales(locale, locales, catalogs),
|
||||
[catalogs, locale, locales]
|
||||
)
|
||||
const contextValue = React.useMemo(
|
||||
() => ({
|
||||
catalogs,
|
||||
locale,
|
||||
locales: normalizedLocales,
|
||||
}),
|
||||
[catalogs, locale, normalizedLocales]
|
||||
)
|
||||
|
||||
return (
|
||||
<I18nRuntimeContext.Provider value={contextValue}>
|
||||
<LinguiI18nProvider i18n={i18n} defaultComponent={defaultComponent}>
|
||||
{children}
|
||||
</LinguiI18nProvider>
|
||||
</I18nRuntimeContext.Provider>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user