Skip to content

Commit f85c543

Browse files
authored
feat: decentralized loader registration (#10)
1 parent 9571580 commit f85c543

8 files changed

Lines changed: 81 additions & 58 deletions

File tree

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@pistonite/celera",
3-
"version": "0.2.4",
3+
"version": "0.3.0",
44
"type": "module",
55
"private": true,
66
"description": "In-house UI framework",
@@ -16,8 +16,8 @@
1616
},
1717
"dependencies": {
1818
"@pistonite/pure": "^0.29.7",
19-
"i18next": "^26.0.8",
20-
"react-i18next": "^17.0.6"
19+
"i18next": "^26.0.10",
20+
"react-i18next": "^17.0.7"
2121
},
2222
"peerDependencies": {
2323
"@fluentui/react-components": "^9",

pnpm-lock.yaml

Lines changed: 17 additions & 17 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/i18n/backend.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
import type { BackendModule } from "i18next";
22

3+
import { log } from "#util";
4+
35
import { convertToSupportedLocale } from "./state.ts";
6+
import { getTranslationLoaderForNamespace } from "./loaders.ts";
47
import type { LoadLanguageFn } from "./types.ts";
58

6-
import { log } from "#util";
7-
89
/** Create an i18next backend module given the loader functions */
910
export const createBackend = (
10-
loaders: Record<string, LoadLanguageFn>,
11+
defaultLoader: LoadLanguageFn | undefined,
1112
fallbackLocale: string,
1213
): BackendModule => {
13-
const hasNonDefaultNamespace = Object.keys(loaders).some((x) => x !== "translation");
1414
const backend: BackendModule = {
1515
type: "backend",
1616
init: () => {
@@ -22,14 +22,16 @@ export const createBackend = (
2222
return undefined;
2323
}
2424
const locale = convertToSupportedLocale(language) || fallbackLocale;
25-
const loader = loaders[namespace];
26-
if (!loader) {
27-
if (namespace !== "translation" || !hasNonDefaultNamespace) {
28-
// only log an error if the namespace is not the default
29-
// if there are non-default namespaces
30-
log.error(`no loader found for namespace ${namespace}`);
25+
const isDefaultNamespace = namespace === "translation" || !namespace;
26+
let loader: LoadLanguageFn;
27+
if (isDefaultNamespace) {
28+
if (!defaultLoader) {
29+
log.error("default namespace translation loader is not registered");
30+
return undefined;
3131
}
32-
return undefined;
32+
loader = defaultLoader;
33+
} else {
34+
loader = await getTranslationLoaderForNamespace(namespace);
3335
}
3436
try {
3537
const strings = await loader(locale);

src/i18n/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ export * from "./state.ts";
22
export * from "./helper.ts";
33
export * from "./integration.ts";
44
export * from "./init.ts";
5+
export * from "./loaders.ts";
56
export * from "./types.ts";

src/i18n/init.ts

Lines changed: 5 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { syncI18nextToCeleraModule } from "./integration.ts";
66
import type { LocaleOptions } from "./types.ts";
77
import { createBackend } from "./backend.ts";
88
import Strings from "./strings.yaml";
9+
import { registerTranslationLoader } from "./loaders.ts";
910

1011
export const CELERA_NAMESPACE = "celerans";
1112

@@ -15,6 +16,8 @@ export const CELERA_NAMESPACE = "celerans";
1516
* This function calls `initLocale` internally, so you don't need to do that yourself.
1617
*/
1718
export const initLocale = async <TLocale extends string>(options: LocaleOptions<TLocale>) => {
19+
registerTranslationLoader(CELERA_NAMESPACE, loadCeleraTranslations);
20+
1821
const defaultLocale = options.default;
1922
let instance = i18next;
2023
const syncMode = options.sync || "full";
@@ -43,31 +46,10 @@ export const initLocale = async <TLocale extends string>(options: LocaleOptions<
4346
instance = instance.use(initReactI18next);
4447

4548
const loader = options.loader;
46-
if (typeof loader === "function") {
47-
const backend = createBackend(
48-
{
49-
translation: loader,
50-
[CELERA_NAMESPACE]: loadCeleraTranslations,
51-
},
52-
defaultLocale,
53-
);
54-
instance = instance.use(backend);
55-
await instance.init();
56-
return;
57-
}
58-
59-
const backend = createBackend(
60-
{
61-
...loader,
62-
[CELERA_NAMESPACE]: loadCeleraTranslations,
63-
},
64-
defaultLocale,
65-
);
49+
const backend = createBackend(loader, defaultLocale);
6650
instance = instance.use(backend);
6751
await instance.init({
68-
// make sure the namespaces are registered, so translations work
69-
// in contexts without react-i18next
70-
ns: Object.keys(loader),
52+
ns: [CELERA_NAMESPACE],
7153
});
7254
};
7355

src/i18n/loaders.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import type { LoadLanguageFn } from "./types.ts";
2+
import i18next from "i18next";
3+
4+
import { log } from "#util";
5+
6+
const namedspacedLoaders: Map<string, LoadLanguageFn> = new Map();
7+
const namespacedAwaiters: Map<string, ((loader: LoadLanguageFn) => void)[]> = new Map();
8+
9+
/** Register a translation loader for a namespace */
10+
export const registerTranslationLoader = (namespace: string, loader: LoadLanguageFn) => {
11+
if (namedspacedLoaders.has(namespace)) {
12+
log.error(`translation namespace '${namespace}' is already registered`);
13+
return;
14+
}
15+
namedspacedLoaders.set(namespace, loader);
16+
const awaiters = namespacedAwaiters.get(namespace);
17+
if (!awaiters) {
18+
return;
19+
}
20+
namespacedAwaiters.delete(namespace);
21+
awaiters.forEach((x) => x(loader));
22+
void i18next.loadNamespaces(namespace);
23+
};
24+
25+
export const getTranslationLoaderForNamespace = (namespace: string): Promise<LoadLanguageFn> => {
26+
const loader = namedspacedLoaders.get(namespace);
27+
if (loader) {
28+
return Promise.resolve(loader);
29+
}
30+
return new Promise((resolve) => {
31+
const awaiters = namespacedAwaiters.get(namespace);
32+
if (awaiters) {
33+
awaiters.push(resolve);
34+
} else {
35+
namespacedAwaiters.set(namespace, [resolve]);
36+
}
37+
});
38+
};

src/i18n/types.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,11 @@ export interface LocaleOptions<TLocale extends string> {
6464
sync?: "full" | "i18next-celera" | "celera-i18next";
6565

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

7574
/**

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export {
3030
useTranslation,
3131
translate,
3232
initLocale,
33+
registerTranslationLoader,
3334
type LocaleOptions,
3435
type LoadLanguageFn,
3536
type TranslatorFn,

0 commit comments

Comments
 (0)