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