Add react i18next to console
Signed-off-by: Jonathan <contact@grafikart.fr>
This commit is contained in:
84
apps/console/src/lib/i18n/backend.ts
Normal file
84
apps/console/src/lib/i18n/backend.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import type { BackendModule, ReadCallback, ResourceKey } from "i18next";
|
||||
|
||||
// The default namespace for app-wide chrome strings, sourced from src/_locales/.
|
||||
export const DEFAULT_NAMESPACE = "app";
|
||||
|
||||
type CatalogModule = { default: ResourceKey };
|
||||
|
||||
// Vite turns each translation JSON into its own lazily-imported chunk. The keys
|
||||
// are project-root-absolute paths, e.g.
|
||||
// "/src/pages/organizations/measures/_locales/en-US.json".
|
||||
const catalogs = import.meta.glob<CatalogModule>("/src/**/_locales/*.json");
|
||||
|
||||
const CATALOG_PATH = /^\/src\/(.*)_locales\/([^/]+)\.json$/;
|
||||
|
||||
// Map "<namespace>\u0000<language>" -> lazy importer for that catalog chunk.
|
||||
function buildLookup(): Map<string, () => Promise<CatalogModule>> {
|
||||
const lookup = new Map<string, () => Promise<CatalogModule>>();
|
||||
|
||||
for (const [path, importer] of Object.entries(catalogs)) {
|
||||
const match = CATALOG_PATH.exec(path);
|
||||
if (!match) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const [, prefix, language] = match;
|
||||
// prefix is the path between "src/" and "_locales/", e.g. "pages/foo/".
|
||||
// Drop the leading "pages/" and trailing slash; an empty prefix (src/_locales)
|
||||
// is the app-wide default namespace.
|
||||
const namespace
|
||||
= prefix.replace(/^pages\//, "").replace(/\/$/, "") || DEFAULT_NAMESPACE;
|
||||
|
||||
lookup.set(catalogKey(namespace, language), importer);
|
||||
}
|
||||
|
||||
return lookup;
|
||||
}
|
||||
|
||||
function catalogKey(namespace: string, language: string): string {
|
||||
return `${namespace}\u0000${language}`;
|
||||
}
|
||||
|
||||
const lookup = buildLookup();
|
||||
|
||||
// Custom i18next backend that resolves a (language, namespace) pair to its lazy
|
||||
// JSON chunk. A missing catalog resolves to an empty resource so i18next falls
|
||||
// through to fallbackLng rather than treating it as a hard load error.
|
||||
export const globBackend: BackendModule = {
|
||||
type: "backend",
|
||||
init() {},
|
||||
read(language: string, namespace: string, callback: ReadCallback) {
|
||||
const importer = lookup.get(catalogKey(namespace, language));
|
||||
|
||||
if (!importer) {
|
||||
callback(null, {});
|
||||
return;
|
||||
}
|
||||
|
||||
importer().then(
|
||||
module => callback(null, module.default),
|
||||
(error: unknown) =>
|
||||
callback(error instanceof Error ? error : new Error(String(error)), null),
|
||||
);
|
||||
},
|
||||
};
|
||||
47
apps/console/src/lib/i18n/i18n.ts
Normal file
47
apps/console/src/lib/i18n/i18n.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { createInstance } from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
|
||||
import { DEFAULT_NAMESPACE, globBackend } from "./backend";
|
||||
import { resolveLanguage, SUPPORTED_LANGUAGES } from "./resolveLanguage";
|
||||
|
||||
// Build a dedicated instance rather than mutating i18next's global singleton.
|
||||
// Initializing it through initReactI18next still registers it as the instance
|
||||
// react-i18next's hooks read from, so no I18nextProvider is required.
|
||||
const i18n = createInstance();
|
||||
|
||||
void i18n
|
||||
.use(globBackend)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
lng: resolveLanguage(),
|
||||
fallbackLng: "en-US",
|
||||
supportedLngs: SUPPORTED_LANGUAGES,
|
||||
load: "currentOnly",
|
||||
defaultNS: DEFAULT_NAMESPACE,
|
||||
fallbackNS: DEFAULT_NAMESPACE,
|
||||
ns: [DEFAULT_NAMESPACE],
|
||||
interpolation: { escapeValue: false },
|
||||
react: { useSuspense: true },
|
||||
});
|
||||
|
||||
export { i18n };
|
||||
76
apps/console/src/lib/i18n/resolveLanguage.ts
Normal file
76
apps/console/src/lib/i18n/resolveLanguage.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
export const SUPPORTED_LANGUAGES = [
|
||||
"en-US",
|
||||
"fr-FR",
|
||||
"de-DE",
|
||||
"es-ES",
|
||||
"id-ID",
|
||||
"it-IT",
|
||||
"ja-JP",
|
||||
"ko-KR",
|
||||
"pl-PL",
|
||||
"pt-PT",
|
||||
"tr-TR",
|
||||
"uk-UA",
|
||||
"zh-CN",
|
||||
] as const;
|
||||
|
||||
export type SupportedLanguage = (typeof SUPPORTED_LANGUAGES)[number];
|
||||
|
||||
// Maps a two-letter language prefix (lowercased) to its canonical supported
|
||||
// tag, e.g. any "de*" browser tag resolves to "de-DE".
|
||||
const PREFIX_TO_LANGUAGE: Record<string, SupportedLanguage> = {
|
||||
en: "en-US",
|
||||
fr: "fr-FR",
|
||||
de: "de-DE",
|
||||
es: "es-ES",
|
||||
id: "id-ID",
|
||||
it: "it-IT",
|
||||
ja: "ja-JP",
|
||||
ko: "ko-KR",
|
||||
pl: "pl-PL",
|
||||
pt: "pt-PT",
|
||||
tr: "tr-TR",
|
||||
uk: "uk-UA",
|
||||
zh: "zh-CN",
|
||||
};
|
||||
|
||||
// Collapse the browser's preferred languages to one of our supported locales
|
||||
// by matching the two-letter language prefix (e.g. any "fr*" tag maps to
|
||||
// fr-FR). en-US is the ultimate fallback when nothing matches. Resolving to a
|
||||
// canonical supported tag here means i18next is never asked to load an
|
||||
// unsupported locale; fallbackLng only has to cover individual missing keys.
|
||||
export function resolveLanguage(): SupportedLanguage {
|
||||
const candidates = navigator.languages?.length
|
||||
? navigator.languages
|
||||
: [navigator.language];
|
||||
|
||||
for (const tag of candidates) {
|
||||
const prefix = tag.toLowerCase().split("-")[0];
|
||||
const language = PREFIX_TO_LANGUAGE[prefix];
|
||||
if (language) {
|
||||
return language;
|
||||
}
|
||||
}
|
||||
|
||||
return "en-US";
|
||||
}
|
||||
@@ -13,10 +13,10 @@
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import { formatError } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { createUseMutation, type MutationNotifier } from "@probo/relay";
|
||||
import { useToast } from "@probo/ui";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
/**
|
||||
* Binds the shared awaitable useMutation (`@probo/relay`) to this app's
|
||||
@@ -28,7 +28,7 @@ import { useMemo } from "react";
|
||||
*/
|
||||
function useMutationNotifier(): MutationNotifier {
|
||||
const { toast } = useToast();
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return useMemo<MutationNotifier>(
|
||||
() => ({
|
||||
@@ -36,7 +36,7 @@ function useMutationNotifier(): MutationNotifier {
|
||||
toast({ title, description: "", variant: "success" });
|
||||
},
|
||||
notifyError: (error, title) => {
|
||||
const finalTitle = title ?? __("Error");
|
||||
const finalTitle = title ?? t("common.error");
|
||||
toast({
|
||||
title: finalTitle,
|
||||
description: formatError(finalTitle, error),
|
||||
@@ -44,7 +44,7 @@ function useMutationNotifier(): MutationNotifier {
|
||||
});
|
||||
},
|
||||
}),
|
||||
[toast, __],
|
||||
[toast, t],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user