StarterApp Docs
Localization

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 createLocalizedExampleAppI18n or a custom call to createAppI18n.
  • Your dictionaries are exposed as async loaders (they can live in a package or inside the app).
  • apps/marketing/i18n.ts re-exports marketingRoutes and marketingDictionaries (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 getLocaleAwareHref from @starterapp/i18n-next/navigation.
  • Add localized metadata using generateLocalizedSitemapEntries and generateHreflangLinks.
  • Track fallback telemetry through recordDictionaryFallback to monitor missing translations.