81 lines
2.9 KiB
TypeScript
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 };
|
|
}
|