Add react i18next to console

Signed-off-by: Jonathan <contact@grafikart.fr>
This commit is contained in:
Jonathan
2026-07-21 16:09:41 +02:00
committed by Bryan Frimin
parent 1b7b2594eb
commit a7ff5f07bc
420 changed files with 10251 additions and 6825 deletions

View 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),
);
},
};

View 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 };

View 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";
}

View File

@@ -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],
);
}