Adding a Localized Route
Opt a marketing page into localization with shared helpers and tooling.
StarterApp’s localization stack is opt-in. Localize one marketing route at a time by wiring dictionaries, registering the locale, and wrapping the page with the shared helper. The rest of the app stays English-only.
One Route at a Time
Start with a single page (for example /localized-page-example) before scaling out. The middleware allow list prevents accidental redirects on unfinished routes.
Prerequisites
- The locale definition is registered via
createLocalizedExampleAppI18nor a custom call tocreateAppI18n. - Your dictionaries are exposed as async loaders (they can live in a package or inside the app).
apps/marketing/i18n.tsre-exportsmarketingRoutesandmarketingDictionaries(already scaffolded by default).
Step-by-Step
1. Add dictionaries
Expose loaders that return plain objects. You can keep them in a package for reuse:
export const blogEn = { blog: { page: { title: "Blog" } } };
export const blogEs = { blog: { page: { title: "Blog" } } };
export const blogNamespaces = {
en: {
blog: async () => blogEn,
},
es: {
blog: async () => blogEs,
},
};2. Register the namespace
Extend the packaged helper (or create your own createAppI18n call):
import { createAppI18n } from "@starterapp/i18n-next";
import { blogNamespaces } from "@workspace/i18n-blog/messages";
export const {
dictionaries: marketingDictionaries,
routeRegistry: marketingRoutes,
localizedRoutes: marketingLocalizedRoutes,
} = createAppI18n({
appId: "marketing",
englishNamespaces: {
// spread existing namespaces if you reuse the packaged example
// ...localizedExampleEnglishNamespaces,
...blogNamespaces.en,
},
localeConfigs: [
{
locale: {
id: "es",
label: "Español",
dir: "ltr",
fallback: ["es", "en"],
enabled: true,
},
namespaces: {
// ...localizedExampleLocaleNamespaces.es,
...blogNamespaces.es,
},
},
],
localizedRoutes: ["/localized-page-example", "/blog"],
});The helper registers locale definitions and dictionary loaders at import time.
3. Allowlist the route
If you need to add routes after initialization, call the route registry:
export const marketingLocalizedRoutes = [
"/localized-page-example",
"/blog",
];Marketing middleware uses this registry to negotiate locales only for opted-in paths. When you seed the routes during createAppI18n, they appear here automatically.
4. Export the localized page
Create the localized route under [locale] and reuse the helper:
import { createMarketingLocalizedPage } from "~/i18n/server";
export default createMarketingLocalizedPage({
namespaces: ["blog"],
render: ({ locale, messages }) => {
const t = createTranslator({ locale, messages, namespace: "blog" });
return (
<Section>
<h1>{t("page.title")}</h1>
<p>{t("page.description")}</p>
</Section>
);
},
});Keep the non-localized entry as a thin re-export to serve English at /blog:
export { default } from "../[locale]/blog/page";Validation
pnpm test -- apps/marketing/__tests__/localized-example.integration.test.tsx— ensures middleware + rendering still pass.
Edge Caching
Localized pages remain dynamic because the middleware negotiates per request. Avoid revalidate > 0 on localized marketing pages unless you understand the caching trade-offs.
Next Steps
- Localize navigation links with
getLocaleAwareHreffrom@starterapp/i18n-next/navigation. - Add localized metadata using
generateLocalizedSitemapEntriesandgenerateHreflangLinks. - Track fallback telemetry through
recordDictionaryFallbackto monitor missing translations.