Files
claudemesh/packages/i18n/src/server/index.ts
Alejandro Gutiérrez d3163a5bff feat(db): mesh data model — meshes, members, invites, audit log
- pgSchema "mesh" with 4 tables isolating the peer mesh domain
- Enums: visibility, transport, tier, role
- audit_log is metadata-only (E2E encryption enforced at broker/client)
- Cascade on mesh delete, soft-delete via archivedAt/revokedAt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 21:19:32 +01:00

186 lines
4.5 KiB
TypeScript

import { match } from "@formatjs/intl-localematcher";
import dayjs from "dayjs";
import { createInstance } from "i18next";
import resourcesToBackend from "i18next-resources-to-backend";
import Negotiator from "negotiator";
import { initReactI18next } from "react-i18next/initReactI18next";
import * as z from "zod";
import { logger } from "@turbostarter/shared/logger";
import { config, getInitOptions } from "../config";
import { env } from "../env";
import { loadTranslation, makeZodI18nMap } from "../utils";
import type { i18n, Namespace, TFunction } from "i18next";
export const initializeServerI18n = async ({
locale,
defaultLocale,
ns,
}: {
locale?: string;
defaultLocale?: string;
ns?: Namespace;
}): Promise<i18n> => {
const i18n = createInstance();
const loadedNamespaces = new Set<string>();
await new Promise((resolve) => {
void i18n
.use(
resourcesToBackend(
async (
language: (typeof config.locales)[number],
namespace: (typeof config.namespaces)[number],
callback,
) => {
const data = await loadTranslation(language, namespace);
loadedNamespaces.add(namespace);
return callback(null, data);
},
),
)
.use({
type: "3rdParty",
init: async (i18next: typeof i18n) => {
let iterations = 0;
const maxIterations = 100;
while (i18next.isInitializing) {
iterations++;
if (iterations > maxIterations) {
logger.error(
`i18next is not initialized after ${maxIterations} iterations`,
);
break;
}
await new Promise((resolve) => setTimeout(resolve, 1));
}
initReactI18next.init(i18next);
resolve(i18next);
},
})
.init(getInitOptions({ locale, defaultLocale, ns }));
});
const namespaces = ns
? typeof ns === "string"
? [ns]
: ns
: config.namespaces;
// If all namespaces are already loaded, return the i18n instance
if (loadedNamespaces.size === namespaces.length) {
return i18n;
}
// Otherwise, wait for all namespaces to be loaded
const maxWaitTimeMs = 100;
const checkIntervalMs = 5;
async function waitForNamespaces() {
const startTime = Date.now();
while (Date.now() - startTime < maxWaitTimeMs) {
const allNamespacesLoaded = namespaces.every((ns) =>
loadedNamespaces.has(ns),
);
if (allNamespacesLoaded) {
return true;
}
await new Promise((resolve) => setTimeout(resolve, checkIntervalMs));
}
return false;
}
const success = await waitForNamespaces();
if (!success) {
logger.warn(
`Not all namespaces were loaded after ${maxWaitTimeMs}ms. Initialization may be incomplete.`,
);
}
return i18n;
};
export const getLocaleFromCookies = async () => {
try {
const { cookies } = await import("next/headers");
return (await cookies()).get(config.cookie)?.value;
} catch {
return undefined;
}
};
export const getLocaleFromRequest = (request?: Request) => {
if (!request) return env.DEFAULT_LOCALE ?? config.defaultLocale;
const localeCookie = request.headers
.get("cookie")
?.split(";")
.find((cookie) => cookie.trim().startsWith(`${config.cookie}=`))
?.split("=")[1]
?.trim()
.replace(/[.,]/g, "");
if (localeCookie) {
return localeCookie;
}
const negotiatorHeaders: Record<string, string> = {};
request.headers.forEach((value: string, key: string) => {
negotiatorHeaders[key] = value;
});
const languages = new Negotiator({ headers: negotiatorHeaders }).languages();
try {
return match(
languages,
config.locales,
env.DEFAULT_LOCALE ?? config.defaultLocale,
);
} catch {
return env.DEFAULT_LOCALE ?? config.defaultLocale;
}
};
export const getTranslation = async <T extends Namespace>({
locale: passedLocale,
request,
ns,
}: { locale?: string; request?: Request; ns?: T } = {}) => {
const locale =
passedLocale ??
(request ? getLocaleFromRequest(request) : null) ??
(await getLocaleFromCookies()) ??
undefined;
const i18nextInstance = await initializeServerI18n({ locale, ns });
dayjs.locale(i18nextInstance.language);
const t = i18nextInstance.getFixedT<T>(
i18nextInstance.language,
ns,
) as TFunction<T>;
z.config({
localeError: makeZodI18nMap({ t: t as TFunction }),
});
return {
t,
i18n: i18nextInstance,
};
};