|
1 | 1 | import { ok } from "assert"; |
2 | | -import type { Application } from "../application.js"; |
3 | | -import { DefaultMap, setTranslations, type TranslatedString, unique } from "#utils"; |
| 2 | +import { addTranslations, DefaultMap, setTranslations, type TranslatedString } from "#utils"; |
4 | 3 | import { readdirSync } from "fs"; |
5 | 4 | import { join } from "path"; |
6 | | -import translatable from "./locales/en.cjs"; |
7 | 5 | import { type BuiltinTranslatableStringArgs } from "./translatable.js"; |
8 | 6 | import { createRequire } from "node:module"; |
9 | 7 | import { fileURLToPath } from "node:url"; |
@@ -54,85 +52,70 @@ export type TranslationProxy = { |
54 | 52 | ) => TranslatedString; |
55 | 53 | }; |
56 | 54 |
|
| 55 | +const req = createRequire(fileURLToPath(import.meta.url)); |
| 56 | + |
57 | 57 | /** |
58 | | - * Simple internationalization module which supports placeholders. |
59 | | - * See {@link TranslatableStrings} for a description of how this module works and how |
60 | | - * plugins should add translations. |
| 58 | + * Load TypeDoc's translations for a specified language |
61 | 59 | */ |
62 | | -export class Internationalization { |
63 | | - private allTranslations = new DefaultMap<string, Map<string, string>>( |
64 | | - (lang) => { |
65 | | - const req = createRequire(fileURLToPath(import.meta.url)); |
66 | | - // Make sure this isn't abused to load some random file by mistake |
67 | | - ok( |
68 | | - /^[A-Za-z-]+$/.test(lang), |
69 | | - "Locale names may only contain letters and dashes", |
70 | | - ); |
71 | | - try { |
72 | | - return new Map(Object.entries(req(`./locales/${lang}.cjs`))); |
73 | | - } catch { |
74 | | - return new Map(); |
75 | | - } |
76 | | - }, |
| 60 | +export function loadTranslations(lang: string): Record<string, string> { |
| 61 | + // Make sure this isn't abused to load some random file by mistake |
| 62 | + ok( |
| 63 | + /^[A-Za-z-]+$/.test(lang), |
| 64 | + "Locale names may only contain letters and dashes", |
77 | 65 | ); |
| 66 | + try { |
| 67 | + return req(`./locales/${lang}.cjs`); |
| 68 | + } catch { |
| 69 | + return {}; |
| 70 | + } |
| 71 | +} |
78 | 72 |
|
79 | | - /** |
80 | | - * If constructed without an application, will use the default language. |
81 | | - * Intended for use in unit tests only. |
82 | | - * @internal |
83 | | - */ |
84 | | - constructor(private application: Application | null) { |
85 | | - // TODO: Get rid of this extra proxy |
86 | | - setTranslations( |
87 | | - new Proxy(this, { |
88 | | - get(i, p) { |
89 | | - const t = i.allTranslations.get(i.application?.lang ?? "en") ?? |
90 | | - translatable; |
91 | | - return t instanceof Map ? t.get(p as string) : t[p]; |
92 | | - }, |
93 | | - has(i, p) { |
94 | | - const t = i.allTranslations.get(i.application?.lang ?? "en") ?? |
95 | | - translatable; |
96 | | - return t instanceof Map ? t.has(p as string) : Object.prototype.hasOwnProperty.call(t, p); |
97 | | - }, |
98 | | - }) as never, |
99 | | - ); |
| 73 | +/** |
| 74 | + * Get languages which TypeDoc includes translations for |
| 75 | + */ |
| 76 | +export function getNativelySupportedLanguages(): string[] { |
| 77 | + return readdirSync(join(fileURLToPath(import.meta.url), "../locales")) |
| 78 | + .map((x) => x.substring(0, x.indexOf("."))); |
| 79 | +} |
| 80 | + |
| 81 | +/** |
| 82 | + * Responsible for maintaining loaded internationalized strings. |
| 83 | + */ |
| 84 | +export class Internationalization { |
| 85 | + private locales = new DefaultMap<string, Record<string, string>>(() => ({})); |
| 86 | + private loadedLocale!: string; |
| 87 | + |
| 88 | + constructor() { |
| 89 | + this.setLocale("en"); |
100 | 90 | } |
101 | 91 |
|
102 | | - /** |
103 | | - * Add translations for a string which will be displayed to the user. |
104 | | - */ |
105 | | - addTranslations( |
106 | | - lang: string, |
107 | | - translations: Partial<Record<keyof TranslatableStrings, string>>, |
108 | | - override = false, |
109 | | - ): void { |
110 | | - const target = this.allTranslations.get(lang); |
111 | | - for (const [key, val] of Object.entries(translations)) { |
112 | | - if (!target.has(key) || override) { |
113 | | - target.set(key, val); |
114 | | - } |
| 92 | + setLocale(locale: string): void { |
| 93 | + if (this.loadedLocale !== locale) { |
| 94 | + const defaultTranslations = loadTranslations(locale); |
| 95 | + const overrides = this.locales.get(locale); |
| 96 | + setTranslations({ ...defaultTranslations, ...overrides }); |
| 97 | + this.loadedLocale = locale; |
115 | 98 | } |
116 | 99 | } |
117 | 100 |
|
118 | | - /** |
119 | | - * Checks if we have any translations in the specified language. |
120 | | - */ |
121 | | - hasTranslations(lang: string): boolean { |
122 | | - return this.allTranslations.get(lang).size > 0; |
| 101 | + addTranslations(locale: string, translations: Record<string, string>): void { |
| 102 | + Object.assign(this.locales.get(locale), translations); |
| 103 | + if (locale === this.loadedLocale) { |
| 104 | + addTranslations(translations); |
| 105 | + } |
| 106 | + } |
| 107 | + |
| 108 | + hasTranslations(locale: string) { |
| 109 | + return this.getSupportedLanguages().includes(locale); |
123 | 110 | } |
124 | 111 |
|
125 | | - /** |
126 | | - * Gets a list of all languages with at least one translation. |
127 | | - */ |
128 | 112 | getSupportedLanguages(): string[] { |
129 | | - return unique([ |
130 | | - ...readdirSync( |
131 | | - join(fileURLToPath(import.meta.url), "../locales"), |
132 | | - ).map((x) => x.substring(0, x.indexOf("."))), |
133 | | - ...this.allTranslations.keys(), |
134 | | - ]) |
135 | | - .filter((lang) => this.hasTranslations(lang)) |
136 | | - .sort(); |
| 113 | + const supported = new Set(getNativelySupportedLanguages()); |
| 114 | + for (const [locale, translations] of this.locales) { |
| 115 | + if (Object.entries(translations).length) { |
| 116 | + supported.add(locale); |
| 117 | + } |
| 118 | + } |
| 119 | + return Array.from(supported); |
137 | 120 | } |
138 | 121 | } |
0 commit comments