返回 CodeWhale
config.ts
根目录 / web / lib / i18n / config.ts
1 /**
2 * Locale configuration for codewhale.net.
3 *
4 * This is the single canonical website locale registry — the locale
5 * switcher, Next.js route generation, middleware detection, and content
6 * locale registry all derive from `ALL_LOCALES`. Sitemap and hreflang output
7 * then narrow that routed set to the locales with a genuine page-body
8 * translation. The cross-surface matrix (TUI packs, READMEs, website) lives
9 * in docs/LOCALIZATION.md.
10 *
11 * Status semantics:
12 * - `shipped` — full website parity with English on first-class pages.
13 * - `partial` — routed and selectable, but intentionally incomplete:
14 * chrome (nav/footer/switcher), the home page, and metadata
15 * are localized via web/lib/i18n/dictionaries/<code>/ and
16 * everything else falls back to English. The switcher marks
17 * these with a visible partial badge. No route 404s and no
18 * untranslated string ever renders a dictionary key.
19 * - `planned` — tracked in docs/LOCALIZATION.md with a target issue; not
20 * routed.
21 * - `deferred` — acknowledged but not scheduled; not routed.
22 *
23 * When adding a locale:
24 * 1. Add/flip the entry in `ALL_LOCALES` below.
25 * 2. Scaffold dictionaries under web/lib/i18n/dictionaries/<code>/
26 * (chrome.ts + home.ts — see dictionaries/en/ for the reference shape).
27 * 3. Run `npm run check:locales` (dictionary key parity) and `npm test`.
28 * 4. Update docs/LOCALIZATION.md.
29 */
30
31 /** Status of a locale relative to the website. */
32 export type LocaleStatus = "shipped" | "partial" | "planned" | "deferred";
33
34 /** Direction of the locale's script — used for the html `dir` attribute. */
35 export type LocaleDirection = "ltr" | "rtl";
36
37 export interface LocaleEntry {
38 /** ISO 639-1 or IETF BCP 47 language tag used in routes. */
39 code: string;
40 /** Display label (native script). */
41 label: string;
42 /** Status relative to the website. */
43 status: LocaleStatus;
44 /** Text direction for the html `dir` attribute. Defaults to "ltr". */
45 dir?: LocaleDirection;
46 /** One-line scope note shown to maintainers (not rendered). */
47 note?: string;
48 }
49
50 /** Text direction of a tracked locale ("ltr" for untracked codes). */
51 export function localeDirection(locale: string): LocaleDirection {
52 return ALL_LOCALES.find((l) => l.code === locale)?.dir ?? "ltr";
53 }
54
55 /**
56 * All locales the project tracks, ordered by priority.
57 *
58 * SHIPPED and PARTIAL locales are included in `locales` (the constrained
59 * set used by Next.js route generation and middleware). PLANNED and
60 * DEFERRED locales are listed here for the matrix but not routed.
61 */
62 export const ALL_LOCALES: LocaleEntry[] = [
63 { code: "en", label: "English", status: "shipped" },
64 {
65 code: "zh",
66 label: "中文",
67 status: "shipped",
68 // Shipped on the strength of translated first-class page BODIES
69 // (install, FAQ, community, contribute, models, runtime) — a chrome
70 // dictionary alone never earns `shipped`. Chrome + home moved to
71 // dictionaries/zh/ in #4934; the remaining page bodies are still
72 // inline `{ en, zh }` content modules awaiting the same move.
73 note: "#4934 — chrome + home dictionary-backed; first-class page bodies translated inline",
74 },
75 {
76 code: "ja",
77 label: "日本語",
78 status: "partial",
79 note: "#3091 — chrome + home localized; page bodies fall back to English",
80 },
81 {
82 code: "vi",
83 label: "Tiếng Việt",
84 status: "partial",
85 note: "#3091 — chrome + home localized; page bodies fall back to English",
86 },
87 {
88 code: "ko",
89 label: "한국어",
90 status: "partial",
91 note: "#3093 — chrome + home localized; page bodies fall back to English",
92 },
93 {
94 code: "ru",
95 label: "Русский",
96 status: "partial",
97 note: "#3092 — chrome + home localized; page bodies fall back to English",
98 },
99 {
100 code: "uk",
101 label: "Українська",
102 status: "partial",
103 note: "#4791 — shipped alongside Russian; same partial scope",
104 },
105 {
106 code: "es",
107 label: "Español",
108 status: "partial",
109 note: "#3093 — chrome + home localized; page bodies fall back to English",
110 },
111 {
112 code: "pt-BR",
113 label: "Português (BR)",
114 status: "partial",
115 note: "#3093 — chrome + home localized; page bodies fall back to English",
116 },
117 {
118 code: "fr",
119 label: "Français",
120 status: "partial",
121 note: "#4788 — TUI pack shipped in v0.9.2; chrome + home + docs-guide localized; page bodies fall back to English",
122 },
123 {
124 code: "de",
125 label: "Deutsch",
126 status: "partial",
127 note: "#4788 — TUI pack shipped in v0.9.2; chrome + home + docs-guide localized; page bodies fall back to English",
128 },
129 {
130 code: "ca",
131 label: "Català",
132 status: "partial",
133 note: "#4749/#4788 — TUI pack shipped in v0.9.2; chrome + home + docs-guide localized; page bodies fall back to English",
134 },
135 {
136 code: "id",
137 label: "Bahasa Indonesia",
138 status: "partial",
139 note: "#4789 — chrome + home localized; page bodies fall back to English",
140 },
141 {
142 code: "hi",
143 label: "हिन्दी",
144 status: "partial",
145 note: "#4790 — TUI pack shipped in v0.9.2; chrome + home + docs-guide localized; page bodies fall back to English",
146 },
147 {
148 code: "tr",
149 label: "Türkçe",
150 status: "partial",
151 note: "major-language wave — chrome + home + docs-guide localized; page bodies fall back to English",
152 },
153 {
154 code: "it",
155 label: "Italiano",
156 status: "partial",
157 note: "major-language wave — chrome + home + docs-guide localized; page bodies fall back to English",
158 },
159 {
160 code: "pl",
161 label: "Polski",
162 status: "partial",
163 note: "major-language wave — chrome + home + docs-guide localized; page bodies fall back to English",
164 },
165 {
166 code: "ar",
167 label: "العربية",
168 status: "partial",
169 dir: "rtl",
170 note: "RTL — html dir derived from this entry; chrome + home + docs-guide localized, page bodies fall back to English",
171 },
172 ];
173
174 /**
175 * Active website locales (used by Next.js route generation, middleware, and
176 * the switcher). Both `shipped` and `partial` locales route; `partial` locales
177 * carry a visible partial-pack status in the switcher. Metadata and sitemap
178 * generation narrow this routed set per page-body translation coverage.
179 */
180 export const locales = ALL_LOCALES.filter(
181 (l) => l.status === "shipped" || l.status === "partial",
182 ).map((l) => l.code) as readonly string[];
183
184 export type Locale = (typeof locales)[number];
185 export const defaultLocale: Locale = "en";
186
187 /** Locales whose packs are intentionally incomplete (English fallback). */
188 export const partialLocales = ALL_LOCALES.filter((l) => l.status === "partial").map(
189 (l) => l.code,
190 ) as readonly string[];
191
192 export function isPartialLocale(x: string): boolean {
193 return partialLocales.includes(x);
194 }
195
196 /** Set to "1" once the Gitee mirror at gitee.com/Hmbown/... exists. */
197 export const GITEE_ENABLED = process.env.NEXT_PUBLIC_GITEE_ENABLED === "1";
198
199 export function isValidLocale(x: string): x is Locale {
200 return (locales as readonly string[]).includes(x);
201 }
202
203 /** Check if a locale code is tracked (shipped, partial, planned, or deferred). */
204 export function isTrackedLocale(x: string): boolean {
205 return ALL_LOCALES.some((l) => l.code === x);
206 }
207
207 lines TYPESCRIPT