Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/locale/i18next.server.ts
T

81 lines
2.9 KiB
TypeScript

import { existsSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { createCookie } from "react-router";
import { loadRequiredWebConfig, resolveSessionCookiePolicy } from "@toolctrl/config";
import { RemixI18Next } from "remix-i18next/server";
import { i18nConfig, i18nNamespaces } from "./i18n";
import Backend from "i18next-fs-backend";
const webConfig = loadRequiredWebConfig();
type ResolveServerLocalePathOptions = {
moduleUrl?: string;
cwd?: string;
exists?: (candidate: string) => boolean;
};
export function resolveServerLocalePath({ moduleUrl = import.meta.url, cwd = process.cwd(), exists = existsSync }: ResolveServerLocalePathOptions = {}) {
// `path.resolve(path.dirname(fileURLToPath(url)), "../../...")` instead
// of `new URL("...", moduleUrl)` because vitest 3 resolves relative
// URL constructors through the Vite dev server (returning http://...);
// we need a file path here.
const moduleDir = path.dirname(fileURLToPath(moduleUrl));
const candidates = [
path.resolve(moduleDir, "../../public/locales"),
path.resolve(cwd, "apps/web/public/locales"),
path.resolve(cwd, "public/locales"),
];
return candidates.find((candidate) => exists(candidate)) ?? candidates[0];
}
export const serverLocalePath = resolveServerLocalePath();
export const i18nCookie = createCookie("_i18n", {
...resolveSessionCookiePolicy({
nodeEnv: webConfig.nodeEnv,
routing: webConfig.routing,
maxAge: 60 * 60 * 24 * 30,
}),
secrets: webConfig.sessionSecret ? [webConfig.sessionSecret] : [],
});
export const remixI18Next = new RemixI18Next({
detection: {
// The cookie used to store user's language preference.
cookie: i18nCookie,
// The supported languages.
supportedLanguages: i18nConfig.supportedLngs,
// The fallback language.
fallbackLanguage: i18nConfig.fallbackLng,
},
// The i18next configuration used when translating
// Server-Side only messages.
i18next: {
...i18nConfig,
backend: {
localePath: serverLocalePath,
loadPath: `${serverLocalePath}/{{lng}}/{{ns}}.json`,
},
},
// The plugins you want RemixI18next to use for `i18n.getFixedT` inside loaders and actions.
// E.g. The Backend plugin for loading translations from the file system.
plugins: [Backend],
});
export async function getTranslations(request: Request, namespace = "translations") {
// Load every namespace so callers can resolve cross-namespace keys like
// `t("auth:errors.invalidEmail")` while `namespace` stays the default for
// un-prefixed keys. See #72 i18n namespace split.
const namespaces: [string, ...string[]] = [namespace, ...i18nNamespaces.filter((ns) => ns !== namespace)];
const t = await remixI18Next.getFixedT(request, namespaces);
let translations: any = {};
const locale = await remixI18Next.getLocale(request);
return { t, translations, locale };
}