feat(i18n): add mobile localization release gates
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
import { apiV1Error, apiV1Success } from "@/server/api/v1/responses";
|
||||
import { negotiateLocale } from "@/server/api/v1/localization";
|
||||
import { getCatalogVersion, getResolvedCatalog } from "@/server/i18n/catalog";
|
||||
import { getPublicLocalizationMetadata } from "@/server/i18n/runtime";
|
||||
import { I18N_NAMESPACES, type I18nNamespace } from "@/lib/i18n";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const namespaceSet = new Set<string>(I18N_NAMESPACES);
|
||||
|
||||
export function GET(request: Request) {
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
const metadata = getPublicLocalizationMetadata();
|
||||
const resolved = negotiateLocale({
|
||||
metadata,
|
||||
requestedLocale: url.searchParams.get("locale"),
|
||||
acceptLanguage: request.headers.get("accept-language"),
|
||||
});
|
||||
const namespaces = parseNamespaces(url.searchParams.get("namespaces"));
|
||||
const catalog = getResolvedCatalog(resolved.locale, namespaces, metadata.catalogVersion);
|
||||
|
||||
return apiV1Success({
|
||||
locale: catalog.locale,
|
||||
requestedLocale: resolved.requestedLocale,
|
||||
defaultLocale: resolved.defaultLocale,
|
||||
source: resolved.source,
|
||||
fallbackChain: catalog.fallbackChain,
|
||||
catalogVersion: getCatalogVersion(),
|
||||
namespaces: catalog.namespaces,
|
||||
messages: catalog.messages,
|
||||
}, {
|
||||
headers: {
|
||||
"Cache-Control": "public, max-age=60, stale-while-revalidate=300",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return apiV1Error(error);
|
||||
}
|
||||
}
|
||||
|
||||
function parseNamespaces(value: string | null): I18nNamespace[] {
|
||||
if (!value) return [...I18N_NAMESPACES];
|
||||
const namespaces = value
|
||||
.split(",")
|
||||
.map((item) => item.trim())
|
||||
.filter((item): item is I18nNamespace => namespaceSet.has(item));
|
||||
return namespaces.length ? namespaces : [...I18N_NAMESPACES];
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { cookies } from "next/headers";
|
||||
import { z } from "zod";
|
||||
import { apiV1Error, apiV1Success } from "@/server/api/v1/responses";
|
||||
import { domainActorFromSession } from "@/server/auth/domain-actor";
|
||||
import { getSessionContextFromHeaders } from "@/server/auth/session";
|
||||
import { getServerConfig } from "@/server/config";
|
||||
import { DomainError } from "@/server/domain/errors";
|
||||
import {
|
||||
getUserPreferences,
|
||||
updateColorModePreference,
|
||||
updateLanguagePreference,
|
||||
} from "@/server/settings/preferences";
|
||||
import {
|
||||
COLOR_MODE_COOKIE,
|
||||
COLOR_MODE_COOKIE_MAX_AGE,
|
||||
isColorMode,
|
||||
} from "@/lib/color-mode";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const inputSchema = z.object({
|
||||
colorMode: z.string().optional(),
|
||||
language: z.string().trim().min(2).max(12).optional(),
|
||||
});
|
||||
|
||||
export async function PATCH(request: Request) {
|
||||
try {
|
||||
const context = await getSessionContextFromHeaders(new Headers(request.headers));
|
||||
if (!context) {
|
||||
throw new DomainError("UNAUTHENTICATED", "Authentication required.", {
|
||||
messageKey: "api.errors.unauthenticated",
|
||||
});
|
||||
}
|
||||
const parsed = inputSchema.safeParse(await request.json());
|
||||
if (!parsed.success) {
|
||||
throw new DomainError("VALIDATION_ERROR", "Invalid preference payload.", {
|
||||
messageKey: "validation.required",
|
||||
});
|
||||
}
|
||||
|
||||
const actor = domainActorFromSession(context);
|
||||
let preferences = getUserPreferences(actor);
|
||||
if (parsed.data.language) {
|
||||
preferences = updateLanguagePreference(actor, { language: parsed.data.language });
|
||||
}
|
||||
if (parsed.data.colorMode) {
|
||||
if (!isColorMode(parsed.data.colorMode)) {
|
||||
throw new DomainError("VALIDATION_ERROR", "Invalid color mode.");
|
||||
}
|
||||
preferences = updateColorModePreference(actor, { colorMode: parsed.data.colorMode });
|
||||
const config = getServerConfig();
|
||||
(await cookies()).set(COLOR_MODE_COOKIE, preferences.colorMode, {
|
||||
httpOnly: false,
|
||||
maxAge: COLOR_MODE_COOKIE_MAX_AGE,
|
||||
path: "/",
|
||||
sameSite: "lax",
|
||||
secure: config.secureCookies,
|
||||
});
|
||||
}
|
||||
|
||||
return apiV1Success({ preferences });
|
||||
} catch (error) {
|
||||
return apiV1Error(error);
|
||||
}
|
||||
}
|
||||
@@ -52,11 +52,11 @@ export async function GET(request: Request) {
|
||||
},
|
||||
preferences,
|
||||
localization: {
|
||||
language: preferences.language,
|
||||
portalLocale,
|
||||
userPreferenceLocale: preferences.language,
|
||||
clientDefaultLocale: portalLocale,
|
||||
resolvedLocale: resolvedLocale.locale,
|
||||
requestedLocale: resolvedLocale.requestedLocale,
|
||||
defaultLocale: resolvedLocale.defaultLocale,
|
||||
instanceDefaultLocale: resolvedLocale.defaultLocale,
|
||||
source: resolvedLocale.source,
|
||||
fallbackChain: resolvedLocale.fallbackChain,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user