Implement domain-based locale routing
An agent-oriented specification for configuring locale ownership, domain redirects, locale-aware links, and multilingual CMS behavior in Creght.
Use this specification when an AI coding agent must configure one Creght site to serve different default languages on different domains. Treat the rules below as an implementation contract, not as optional UI guidance.
Required inputs
Collect these values before editing code:
- The site-wide base
defaultLocale. This is also the fallback layer for multilingual CMS fields. - The complete site-wide
localeslist. - Every hostname that is already bound to this same Creght site.
- The unprefixed default locale owned by each hostname.
- Any additional prefixed locales that each hostname must serve locally.
- Whether automatic locale detection should be enabled.
Hard requirement:
i18n.domainsdoes not bind DNS or attach domains to a site. Every configured hostname MUST already be bound in Settings → Domains. If it is not bound, the request never reaches this site's locale router.
Canonical configuration
Write i18n as a plain object in talizen.config.ts. Do not import a configuration helper.
export default {
i18n: {
defaultLocale: 'zh-CN',
locales: ['zh-CN', 'en'],
localeDetection: false,
domains: [
{
domain: 'www.example.cn',
defaultLocale: 'zh-CN',
locales: ['zh-CN', 'en'],
},
{
domain: 'www.example.com',
defaultLocale: 'en',
locales: ['en'],
},
],
},
}
Keep every domain locale inside the top-level locales array. Including a domain's own defaultLocale in domains[].locales is not required for routing, but SHOULD be done for an explicit, auditable configuration.
Routing decision algorithm
- Match the request hostname against
i18n.domains[].domain. - Use that entry's
defaultLocaleas the routing default for unprefixed paths on that hostname. - If the URL contains an explicit locale prefix, verify that locale is supported site-wide.
- If the current domain serves the requested locale—through its
defaultLocaleorlocales—serve it on the current domain. - Otherwise, find the configured domain whose
defaultLocaleowns the requested locale and issue a temporary 307 redirect to that domain. - If no configured domain owns the locale, keep the request on the current domain and serve it with its explicit locale prefix.
Do not confuse defaults: top-level
i18n.defaultLocaleremains the site's base content locale. A domain entry'sdefaultLocalecontrols only the unprefixed routing default for requests on that hostname.
Domain locale ownership
Single-language domain
Omit additional locales when the domain should serve only its own defaultLocale. Requests for a locale owned by another domain redirect there.
Multilingual domain
List every additional locally served locale in domains[].locales. These locales remain on this host and use explicit path prefixes.
Unprefixed path
example.fr/about resolves to French when that domain declares defaultLocale: 'fr'. Do not add a redundant /fr prefix.
Explicit locale path
example.cn/en/about stays on example.cn only when that domain also serves en; otherwise it can redirect to the domain that owns English.
Generate links with Talizen APIs
Internal links MUST use Talizen locale-aware navigation. Never concatenate /${locale} manually and never infer locale from window.location.pathname.
import { Link, localizedPath } from 'talizen'
// Preferred for rendered internal navigation.
<Link href='/about'>About</Link>
// Use when a component requires a raw URL string.
const href = localizedPath('/about', locale)
When the target locale is the current domain's unprefixed default, Link and localizedPath() correctly omit the locale prefix. A missing prefix in that case is expected behavior, not a bug.
Language switcher
A language switcher SHOULD preserve the current locale-less route instead of sending every user to the home page.
import { Link } from 'talizen'
import { getLocalePath } from 'talizen/router'
<Link href={getLocalePath()} locale='en'>English</Link>
Read locale and CMS content correctly
On the server, read the resolved locale from context.locale or getLocale(). In React components, use useLocale(). Do not parse the URL because the router can strip locale prefixes before page code runs.
import { getLocale } from 'talizen'
import { getContent } from 'talizen/cms'
export async function getServerSideProps() {
const { locale } = getLocale()
const page = await getContent('pages', 'about', {})
return { props: { locale, page } }
}
talizen/cms returns fields already decoded for the current locale. Read the normal fields directly. Do not manually merge _i18n inside page code.
Acceptance matrix
For the canonical configuration above, verify this exact behavior:
www.example.cn/about → zh-CN, 200, no locale prefix
www.example.cn/en/about → en, 200, remains on .cn
www.example.com/about → en, 200, no locale prefix
www.example.com/zh-CN/about → redirect or prefixed output according to domain ownership
If en is removed from www.example.cn.locales, then www.example.cn/en/about SHOULD return a 307 redirect to www.example.com/about.
Implementation checklist
- Confirm every domain is bound to the same site before changing
talizen.config.ts. - Confirm all domain defaults and additional domain locales exist in top-level
i18n.locales. - Keep the config as a plain object; do not introduce Next.js configuration helpers.
- Use Talizen
LinkorlocalizedPath()for internal links. - Use
getLocale(),context.locale, oruseLocale()instead of parsing URLs. - Test unprefixed and prefixed routes on every configured hostname.
- Verify redirect status, destination hostname, path preservation, query preservation, and absence of redirect loops.
- Verify CMS content resolves to the same locale selected by routing.
Forbidden implementations
- Do not add
react-router-dom, Next.js routing packages, or custom locale routers. - Do not hardcode
/en,/fr, or another prefix into shared navigation components. - Do not treat top-level
defaultLocaleas if it changes per domain. - Do not assume that adding
i18n.domainsalso binds or verifies a hostname. - Do not duplicate CMS entries per locale; use field-level translations in one stable content item.
- Do not convert expected 307 locale redirects into permanent redirects unless the product's routing contract explicitly changes.
