Internationalization (i18n)
Multi-language support built on @nuxtjs/i18n, with built-in Simplified Chinese, Traditional Chinese, and English. Translation files use a flat structure that is easy to read and AI-friendly.
Enable i18n
// nuxt.config.ts
swiftConfig: {
i18n: {
enabled: true,
defaultLocale: 'zh-CN',
locales: ['zh-CN', 'zh-TW', 'en'],
}
}
swiftConfig.i18n.locales does exactly one thing: it maps language codes into @nuxtjs/i18n's { code, language } shape so route prefixes (/en/, /zh-TW/) get generated. It does not register translation files — there is no file field in it.
Translation files are wired up by the top-level i18n block in nuxt.config.ts (sw config set framework.i18n.enabled true uncomments this block in the starter for you):
// nuxt.config.ts — top-level i18n, a sibling of swiftConfig
i18n: {
langDir: 'locales', // relative to the i18n/ directory
locales: [
{ code: 'zh-CN', language: 'zh-CN', name: '简体中文', file: 'zh-CN.json' },
{ code: 'zh-TW', language: 'zh-TW', name: '繁體中文', file: 'zh-TW.json' },
{ code: 'en', language: 'en', name: 'English', file: 'en.json' },
],
}
The top-level i18n.locales overrides the framework default, so keep the language codes in both places in sync. Setting only swiftConfig.i18n.locales and skipping the top-level file entries yields working route prefixes but no translations at all — $t() just echoes the key back. Strategy, browser detection, bundling and the rest come from framework defaults; you don't need to restate them.
Built-in framework copy and localization
Framework components ship their own copy too — the paginator's "N items", the feed's
"Skip in Ns", the modal's "Close / Cancel / Confirm", plus ~33 aria-labels.
These follow your active language automatically, with zero changes on your side.
- With i18n enabled: follows the user's currently selected language (not the build-time
defaultLocale) — flipSwLangSwitchand framework copy changes right along with it - With i18n disabled: always Chinese, zero overhead
zh-CN/zh-TW/enare built in; any other language (sayja) falls back to Chinese rather than rendering blanks- Language codes are normalized by primary subtag:
zh-Hant/zh-HK/zh-MOmap to Traditional,en-US/en-GBmap to English
<!-- Nothing to do on your side: switch to en and the paginator is already English -->
<SwPagination v-model:page="page" :total="total" show-total />
<!-- zh-CN → 「共 100 条」/ zh-TW → 「共 100 筆」/ en → "100 items" -->
Components that already expose a slot / prop escape hatch are unaffected — SwModal's
cancelText / confirmText, SwGallery's #empty / #finished and friends still win when
you pass them; the built-in dictionary only kicks in when you don't.
Translation File Structure
Translation files live in i18n/locales/ using a flat JSON format:
// i18n/locales/zh-CN.json
{
"首页": "首页",
"快速上手": "快速上手",
"立即开始": "立即开始",
"首页SEO描述": "Swift Web 框架的官方文档站,涵盖组件库和开发规范"
}
// i18n/locales/en.json
{
"首页": "Home",
"快速上手": "Quick Start",
"立即开始": "Get Started",
"首页SEO描述": "Official docs for Swift Web framework, covering components and standards"
}
Usage in Pages
<script setup lang="ts">
const { t } = useI18n()
useSeo({
title: () => t('首页'),
description: () => t('首页SEO描述'),
})
</script>
<template>
<h1>{{ t('快速上手') }}</h1>
<button>{{ t('立即开始') }}</button>
</template>
Language Switching
<template>
<!-- Built-in switcher: renders only when i18n is enabled with locales ≥ 2 (self-gating,
zero footprint otherwise); customizable via tokens (--sw-lang-switch-*),
named icon slots, or the default scoped slot -->
<SwLangSwitch />
</template>
Hand-roll only when you need a fully custom UI (note: use <option :selected> instead of <select :value> — the latter is invalid HTML and picks the wrong option on SSR-rendered loads):
<script setup lang="ts">
const { locale, setLocale, locales } = useI18n()
</script>
<template>
<select @change="setLocale(($event.target as HTMLSelectElement).value)">
<option v-for="loc in locales" :key="loc.code" :value="loc.code" :selected="loc.code === locale">
{{ loc.name }}
</option>
</select>
</template>
Translation Key Conventions
- Short strings (≤50 characters): use the Chinese original as the key, e.g.
t('立即开始') - Long strings (>50 characters): use a short Chinese description as the key with the full text in the JSON value, e.g.
t('首页SEO描述') - Do not use long sentences as keys — it makes translation files hard to read
URL Localization
@nuxtjs/i18n uses a path-prefix strategy by default:
- zh-CN (default locale):
example.com/(no prefix) - zh-TW:
example.com/zh-TW/ - en:
example.com/en/
The canonical tag is injected by useSeo(); for multilingual sites (locales ≥ 2, prefix strategy) the sitemap automatically splits per locale and emits hreflang cross-links (zero configuration, see SEO).
Browser Language Detection Does Not Work on Cacheable Pages
@nuxtjs/i18n's detectBrowserLanguage (redirecting visitors to their locale prefix based on Accept-Language / cookie) is structurally inert on ISR / cacheable pages: nitro's cache wrapper only passes the cache.varies whitelist of request headers into the render (host / x-forwarded-proto), so Accept-Language and Cookie never reach the detection logic. This is not a configuration problem, and adding them to varies is not a way out — varies feeds the cache key, and splitting by cookie value drives the hit rate to zero.
Actual behavior and trade-offs:
- Landing directly on a cacheable page (including the home page) never triggers a locale redirect; every visitor gets the default-locale cached copy. This is deterministic — which also means a detection redirect can never be broadcast from the cache, so there is no correctness risk.
- Visitors switch languages via SwLangSwitch; the path prefix (
/en/...) is the durable locale carrier. - The framework strips any Set-Cookie written during the render of a cacheable page (including i18n's detection-marker cookie): a cacheable response carrying Set-Cookie would be baked into the shared cache copy and replayed, and makes nginx / CDNs without
proxy_ignore_headersrefuse to cache the page at all (see the deployment guide troubleshooting table). In dev mode the strip logs a console warning naming the stripped cookie; write per-user cookies on the client, or make the route non-cacheable.
html lang Follows the Active Locale
On multilingual sites (locales ≥ 2), a built-in runtime plugin takes over <html lang> with the active locale (using the locale's language field, falling back to code):
- Visiting
/en/xxx→<html lang="en">, correct from the first SSR frame - Switching languages in-site (
SwLangSwitch) →langupdates reactively - Single-locale sites / i18n disabled keep the build-time static value (
i18n.defaultLocale) — zero behavior change
Zero configuration for appliers. Do not double-write useHead({ htmlAttrs: { lang } }) on the business side — htmlAttrs ownership belongs to the framework, and double-writing causes conflicting overrides.