Skip to content
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
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pistonite/celera",
"version": "0.2.4",
"version": "0.3.0",
"type": "module",
"private": true,
"description": "In-house UI framework",
Expand All @@ -16,8 +16,8 @@
},
"dependencies": {
"@pistonite/pure": "^0.29.7",
"i18next": "^26.0.8",
"react-i18next": "^17.0.6"
"i18next": "^26.0.10",
"react-i18next": "^17.0.7"
},
"peerDependencies": {
"@fluentui/react-components": "^9",
Expand Down
34 changes: 17 additions & 17 deletions pnpm-lock.yaml

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

24 changes: 13 additions & 11 deletions src/i18n/backend.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import type { BackendModule } from "i18next";

import { log } from "#util";

import { convertToSupportedLocale } from "./state.ts";
import { getTranslationLoaderForNamespace } from "./loaders.ts";
import type { LoadLanguageFn } from "./types.ts";

import { log } from "#util";

/** Create an i18next backend module given the loader functions */
export const createBackend = (
loaders: Record<string, LoadLanguageFn>,
defaultLoader: LoadLanguageFn | undefined,
fallbackLocale: string,
): BackendModule => {
const hasNonDefaultNamespace = Object.keys(loaders).some((x) => x !== "translation");
const backend: BackendModule = {
type: "backend",
init: () => {
Expand All @@ -22,14 +22,16 @@ export const createBackend = (
return undefined;
}
const locale = convertToSupportedLocale(language) || fallbackLocale;
const loader = loaders[namespace];
if (!loader) {
if (namespace !== "translation" || !hasNonDefaultNamespace) {
// only log an error if the namespace is not the default
// if there are non-default namespaces
log.error(`no loader found for namespace ${namespace}`);
const isDefaultNamespace = namespace === "translation" || !namespace;
let loader: LoadLanguageFn;
if (isDefaultNamespace) {
if (!defaultLoader) {
log.error("default namespace translation loader is not registered");
return undefined;
}
return undefined;
loader = defaultLoader;
} else {
loader = await getTranslationLoaderForNamespace(namespace);
}
try {
const strings = await loader(locale);
Expand Down
1 change: 1 addition & 0 deletions src/i18n/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export * from "./state.ts";
export * from "./helper.ts";
export * from "./integration.ts";
export * from "./init.ts";
export * from "./loaders.ts";
export * from "./types.ts";
28 changes: 5 additions & 23 deletions src/i18n/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { syncI18nextToCeleraModule } from "./integration.ts";
import type { LocaleOptions } from "./types.ts";
import { createBackend } from "./backend.ts";
import Strings from "./strings.yaml";
import { registerTranslationLoader } from "./loaders.ts";

export const CELERA_NAMESPACE = "celerans";

Expand All @@ -15,6 +16,8 @@ export const CELERA_NAMESPACE = "celerans";
* This function calls `initLocale` internally, so you don't need to do that yourself.
*/
export const initLocale = async <TLocale extends string>(options: LocaleOptions<TLocale>) => {
registerTranslationLoader(CELERA_NAMESPACE, loadCeleraTranslations);

const defaultLocale = options.default;
let instance = i18next;
const syncMode = options.sync || "full";
Expand Down Expand Up @@ -43,31 +46,10 @@ export const initLocale = async <TLocale extends string>(options: LocaleOptions<
instance = instance.use(initReactI18next);

const loader = options.loader;
if (typeof loader === "function") {
const backend = createBackend(
{
translation: loader,
[CELERA_NAMESPACE]: loadCeleraTranslations,
},
defaultLocale,
);
instance = instance.use(backend);
await instance.init();
return;
}

const backend = createBackend(
{
...loader,
[CELERA_NAMESPACE]: loadCeleraTranslations,
},
defaultLocale,
);
const backend = createBackend(loader, defaultLocale);
instance = instance.use(backend);
await instance.init({
// make sure the namespaces are registered, so translations work
// in contexts without react-i18next
ns: Object.keys(loader),
ns: [CELERA_NAMESPACE],
});
};

Expand Down
38 changes: 38 additions & 0 deletions src/i18n/loaders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { LoadLanguageFn } from "./types.ts";
import i18next from "i18next";

import { log } from "#util";

const namedspacedLoaders: Map<string, LoadLanguageFn> = new Map();
const namespacedAwaiters: Map<string, ((loader: LoadLanguageFn) => void)[]> = new Map();

/** Register a translation loader for a namespace */
export const registerTranslationLoader = (namespace: string, loader: LoadLanguageFn) => {
if (namedspacedLoaders.has(namespace)) {
log.error(`translation namespace '${namespace}' is already registered`);
return;
}
namedspacedLoaders.set(namespace, loader);
const awaiters = namespacedAwaiters.get(namespace);
if (!awaiters) {
return;
}
namespacedAwaiters.delete(namespace);
awaiters.forEach((x) => x(loader));
void i18next.loadNamespaces(namespace);
};

export const getTranslationLoaderForNamespace = (namespace: string): Promise<LoadLanguageFn> => {
const loader = namedspacedLoaders.get(namespace);
if (loader) {
return Promise.resolve(loader);
}
return new Promise((resolve) => {
const awaiters = namespacedAwaiters.get(namespace);
if (awaiters) {
awaiters.push(resolve);
} else {
namespacedAwaiters.set(namespace, [resolve]);
}
});
};
7 changes: 3 additions & 4 deletions src/i18n/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,11 @@ export interface LocaleOptions<TLocale extends string> {
sync?: "full" | "i18next-celera" | "celera-i18next";

/**
* The language loader function(s).
* The default-namespace language loader.
*
* If a function is provided, it will be called for the "translations" namespace.
* Otherwise, you can provide a record of functions for each namespace.
* To register loaders for non-default namespace, use {@link registerTranslationLoader}
*/
loader: LoadLanguageFn | Record<string, LoadLanguageFn>;
loader?: LoadLanguageFn;
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export {
useTranslation,
translate,
initLocale,
registerTranslationLoader,
type LocaleOptions,
type LoadLanguageFn,
type TranslatorFn,
Expand Down
Loading