Localization
Request-scoped locale resolution and Effect HttpApi middleware for Paraglide applications.
bunx --bun shadcn@latest add https://krakstack.net/r/localization.jsonOverview
localization provides request-scoped locale context for Effect HttpApi applications using Paraglide. It resolves the locale from the locale URL query parameter, the locale request header, the locale cookie, or Accept-Language, in that order, then provides that context to downstream handlers through LocaleMiddleware. All explicit locale values are validated with Effect Schema and cookies are parsed with Effect's HTTP cookie utilities. The localize helper selects a record translation using the current locale and fallback settings.
Included Files
lib/localization.ts-LocaleContext,LocaleMiddleware,LocaleMiddlewareLive,LocalizedInputType, andlocalize
Requirements
The receiving project must use Paraglide and expose its generated runtime at @/paraglide/runtime. Supported locales are derived from the receiving project's Paraglide configuration.
API Setup
Register the live middleware layer in the application HTTP API layer:
import { Layer } from "effect";
import { LocaleMiddlewareLive } from "@/lib/localization";
const apiLayer = Layer.mergeAll(/* application layers */).pipe(
Layer.provide(LocaleMiddlewareLive),
);Apply LocaleMiddleware to an API group and read LocaleContext in endpoint handlers:
import { Effect } from "effect";
import { HttpApiGroup } from "effect/unstable/httpapi";
import { LocaleContext, LocaleMiddleware } from "@/lib/localization";
const RecordsGroup = HttpApiGroup.make("records").middleware(LocaleMiddleware);
const program = Effect.gen(function* () {
const { locale, fallbackLocale } = yield* LocaleContext;
// Use locale and fallbackLocale to resolve localized data.
});Translation Resolution
localize expects a record with a translations array whose items have a locale field. It returns the matching translation merged with the base record, falls back to fallbackLocale or the first translation, and respects fallbackLocale: "none" when a translation must exactly match.
import { localize, type LocalizedInputType } from "@/lib/localization";
const context: LocalizedInputType = { locale: "fr", fallbackLocale: "en" };
const record = localize(context, {
id: "welcome",
translations: [
{ locale: "en", name: "Welcome" },
{ locale: "fr", name: "Bienvenue" },
],
});