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>
This commit is contained in:
185
packages/i18n/src/server/index.ts
Normal file
185
packages/i18n/src/server/index.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
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,
|
||||
};
|
||||
};
|
||||
26
packages/i18n/src/server/with-i18n.tsx
Normal file
26
packages/i18n/src/server/with-i18n.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import dayjs from "dayjs";
|
||||
import * as z from "zod";
|
||||
|
||||
import { env } from "../env";
|
||||
import { makeZodI18nMap } from "../utils";
|
||||
|
||||
import { getLocaleFromCookies, initializeServerI18n } from ".";
|
||||
|
||||
type LayoutOrPageComponent<Params> = React.ComponentType<Params>;
|
||||
|
||||
export function withI18n<Params extends object>(
|
||||
Component: LayoutOrPageComponent<Params>,
|
||||
) {
|
||||
return async function I18nServerComponentWrapper(params: Params) {
|
||||
const i18n = await initializeServerI18n({
|
||||
locale: await getLocaleFromCookies(),
|
||||
defaultLocale: env.DEFAULT_LOCALE,
|
||||
});
|
||||
dayjs.locale(i18n.language);
|
||||
z.config({
|
||||
localeError: makeZodI18nMap({ t: i18n.t }),
|
||||
});
|
||||
|
||||
return <Component {...params} />;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user