-
Notifications
You must be signed in to change notification settings - Fork 712
/
internationalization.ts
267 lines (254 loc) · 10.5 KB
/
internationalization.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
import { ok } from "assert";
import type { Application } from "../application";
import { DefaultMap, unique } from "../utils";
import {
translatable,
type BuiltinTranslatableStringArgs,
} from "./translatable";
import { readdirSync } from "fs";
import { join } from "path";
import { ReflectionKind } from "../models/reflections/kind";
/**
* ### What is translatable?
* TypeDoc includes a lot of literal strings. By convention, messages which are displayed
* to the user at the INFO level or above should be present in this object to be available
* for translation. Messages at the VERBOSE level need not be translated as they are primarily
* intended for debugging. ERROR/WARNING deprecation messages related to TypeDoc's API, or
* requesting users submit a bug report need not be translated.
*
* Errors thrown by TypeDoc are generally *not* considered translatable as they are not
* displayed to the user. An exception to this is errors thrown by the `validate` method
* on options, as option readers will use them to report errors to the user.
*
* ### Interface Keys
* This object uses a similar convention as TypeScript, where the specified key should
* indicate where placeholders are present by including a number in the name. This is
* so that translations can easily tell that they are including the appropriate placeholders.
* This will also be validated at runtime by the {@link Internationalization} class, but
* it's better to have that hint when editing as well.
*
* This interface defines the available translatable strings, and the number of placeholders
* that are required to use each string. Plugins may use declaration merging to add members
* to this interface to use TypeDoc's internationalization module.
*
* @example
* ```ts
* declare module "typedoc" {
* interface TranslatableStrings {
* // Define a translatable string with no arguments
* plugin_msg: [];
* // Define a translatable string requiring one argument
* plugin_msg_0: [string];
* }
* }
* ```
*/
export interface TranslatableStrings extends BuiltinTranslatableStringArgs {}
declare const TranslatedString: unique symbol;
export type TranslatedString = string & { [TranslatedString]: true };
/**
* Dynamic proxy type built from {@link TranslatableStrings}
*/
export type TranslationProxy = {
[K in keyof TranslatableStrings]: (
...args: TranslatableStrings[K]
) => TranslatedString;
};
// If we're running in ts-node, then we need the TS source rather than
// the compiled file.
const ext = process[Symbol.for("ts-node.register.instance") as never]
? "cts"
: "cjs";
/**
* Simple internationalization module which supports placeholders.
* See {@link TranslatableStrings} for a description of how this module works and how
* plugins should add translations.
*/
export class Internationalization {
private allTranslations = new DefaultMap<string, Map<string, string>>(
(lang) => {
// Make sure this isn't abused to load some random file by mistake
ok(
/^[A-Za-z-]+$/.test(lang),
"Locale names may only contain letters and dashes",
);
try {
return new Map(
// eslint-disable-next-line @typescript-eslint/no-var-requires
Object.entries(require(`./locales/${lang}.${ext}`)),
);
} catch {
return new Map();
}
},
);
/**
* Proxy object which supports dynamically translating
* all supported keys. This is generally used rather than the translate
* method so that renaming a key on the `translatable` object that contains
* all of the default translations will automatically update usage locations.
*/
proxy: TranslationProxy = new Proxy(this, {
get(internationalization, key) {
return (...args: string[]) =>
internationalization.translate(
key as never,
...(args as never),
);
},
}) as never as TranslationProxy;
/**
* If constructed without an application, will use the default language.
* Intended for use in unit tests only.
* @internal
*/
constructor(private application: Application | null) {}
/**
* Get the translation of the specified key, replacing placeholders
* with the arguments specified.
*/
translate<T extends keyof TranslatableStrings>(
key: T,
...args: TranslatableStrings[T]
): TranslatedString {
return (
this.allTranslations.get(this.application?.lang ?? "en").get(key) ??
translatable[key] ??
key
).replace(/\{(\d+)\}/g, (_, index) => {
return args[+index] ?? "(no placeholder)";
}) as TranslatedString;
}
kindSingularString(kind: ReflectionKind): TranslatedString {
switch (kind) {
case ReflectionKind.Project:
return this.proxy.kind_project();
case ReflectionKind.Module:
return this.proxy.kind_module();
case ReflectionKind.Namespace:
return this.proxy.kind_namespace();
case ReflectionKind.Enum:
return this.proxy.kind_enum();
case ReflectionKind.EnumMember:
return this.proxy.kind_enum_member();
case ReflectionKind.Variable:
return this.proxy.kind_variable();
case ReflectionKind.Function:
return this.proxy.kind_function();
case ReflectionKind.Class:
return this.proxy.kind_class();
case ReflectionKind.Interface:
return this.proxy.kind_interface();
case ReflectionKind.Constructor:
return this.proxy.kind_constructor();
case ReflectionKind.Property:
return this.proxy.kind_property();
case ReflectionKind.Method:
return this.proxy.kind_method();
case ReflectionKind.CallSignature:
return this.proxy.kind_call_signature();
case ReflectionKind.IndexSignature:
return this.proxy.kind_index_signature();
case ReflectionKind.ConstructorSignature:
return this.proxy.kind_constructor_signature();
case ReflectionKind.Parameter:
return this.proxy.kind_parameter();
case ReflectionKind.TypeLiteral:
return this.proxy.kind_type_literal();
case ReflectionKind.TypeParameter:
return this.proxy.kind_type_parameter();
case ReflectionKind.Accessor:
return this.proxy.kind_accessor();
case ReflectionKind.GetSignature:
return this.proxy.kind_get_signature();
case ReflectionKind.SetSignature:
return this.proxy.kind_set_signature();
case ReflectionKind.TypeAlias:
return this.proxy.kind_type_alias();
case ReflectionKind.Reference:
return this.proxy.kind_reference();
}
}
kindPluralString(kind: ReflectionKind): TranslatedString {
switch (kind) {
case ReflectionKind.Project:
return this.proxy.kind_plural_project();
case ReflectionKind.Module:
return this.proxy.kind_plural_module();
case ReflectionKind.Namespace:
return this.proxy.kind_plural_namespace();
case ReflectionKind.Enum:
return this.proxy.kind_plural_enum();
case ReflectionKind.EnumMember:
return this.proxy.kind_plural_enum_member();
case ReflectionKind.Variable:
return this.proxy.kind_plural_variable();
case ReflectionKind.Function:
return this.proxy.kind_plural_function();
case ReflectionKind.Class:
return this.proxy.kind_plural_class();
case ReflectionKind.Interface:
return this.proxy.kind_plural_interface();
case ReflectionKind.Constructor:
return this.proxy.kind_plural_constructor();
case ReflectionKind.Property:
return this.proxy.kind_plural_property();
case ReflectionKind.Method:
return this.proxy.kind_plural_method();
case ReflectionKind.CallSignature:
return this.proxy.kind_plural_call_signature();
case ReflectionKind.IndexSignature:
return this.proxy.kind_plural_index_signature();
case ReflectionKind.ConstructorSignature:
return this.proxy.kind_plural_constructor_signature();
case ReflectionKind.Parameter:
return this.proxy.kind_plural_parameter();
case ReflectionKind.TypeLiteral:
return this.proxy.kind_plural_type_literal();
case ReflectionKind.TypeParameter:
return this.proxy.kind_plural_type_parameter();
case ReflectionKind.Accessor:
return this.proxy.kind_plural_accessor();
case ReflectionKind.GetSignature:
return this.proxy.kind_plural_get_signature();
case ReflectionKind.SetSignature:
return this.proxy.kind_plural_set_signature();
case ReflectionKind.TypeAlias:
return this.proxy.kind_plural_type_alias();
case ReflectionKind.Reference:
return this.proxy.kind_plural_reference();
}
}
/**
* Add translations for a string which will be displayed to the user.
*/
addTranslations(
lang: string,
translations: Partial<Record<keyof TranslatableStrings, string>>,
override = false,
): void {
const target = this.allTranslations.get(lang);
for (const [key, val] of Object.entries(translations)) {
if (!target.has(key) || override) {
target.set(key, val);
}
}
}
/**
* Checks if we have any translations in the specified language.
*/
hasTranslations(lang: string): boolean {
return this.allTranslations.get(lang).size > 0;
}
/**
* Gets a list of all languages with at least one translation.
*/
getSupportedLanguages(): string[] {
return unique([
...readdirSync(join(__dirname, "locales")).map((x) =>
x.substring(0, x.indexOf(".")),
),
...this.allTranslations.keys(),
]).sort();
}
}