@to-kn/koa-locales 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +201 -0
- package/dist/esm/index.js +271 -0
- package/dist/esm/src/index.js +320 -0
- package/dist/esm/test/index.test.js +580 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +250 -0
- package/dist/types/index.d.ts +28 -0
- package/dist/types/src/index.d.ts +27 -0
- package/dist/types/test/index.test.d.ts +1 -0
- package/package.json +79 -0
@@ -0,0 +1,320 @@
|
|
1
|
+
import Debug from "debug";
|
2
|
+
import fs from "fs";
|
3
|
+
import { ms } from "humanize-ms";
|
4
|
+
import ini from "ini";
|
5
|
+
import yaml from "js-yaml";
|
6
|
+
import { createRequire } from "module";
|
7
|
+
import assign from "object-assign";
|
8
|
+
import path from "path";
|
9
|
+
import util from "util";
|
10
|
+
|
11
|
+
const DEFAULT_OPTIONS = {
|
12
|
+
defaultLocale: "en-US",
|
13
|
+
queryField: "locale",
|
14
|
+
cookieField: "locale",
|
15
|
+
localeAlias: {},
|
16
|
+
writeCookie: true,
|
17
|
+
cookieMaxAge: "1y",
|
18
|
+
dir: undefined,
|
19
|
+
dirs: [path.join(process.cwd(), "locales")],
|
20
|
+
functionName: "__",
|
21
|
+
};
|
22
|
+
function locales(app, options = {}) {
|
23
|
+
options = assign({}, DEFAULT_OPTIONS, options);
|
24
|
+
const defaultLocale = formatLocale(options.defaultLocale || "en-US");
|
25
|
+
const queryField = options.queryField || "locale";
|
26
|
+
const cookieField = options.cookieField || "locale";
|
27
|
+
const cookieDomain = options.cookieDomain;
|
28
|
+
const localeAlias = options.localeAlias || {};
|
29
|
+
const writeCookie = options.writeCookie !== false;
|
30
|
+
const cookieMaxAge = ms(options.cookieMaxAge || "1y");
|
31
|
+
const localeDir = options.dir;
|
32
|
+
const localeDirs = options.dirs || [path.join(process.cwd(), "locales")];
|
33
|
+
const functionName = options.functionName || "__";
|
34
|
+
const resources = {};
|
35
|
+
/**
|
36
|
+
* @Deprecated Use options.dirs instead.
|
37
|
+
*/
|
38
|
+
if (localeDir && !localeDirs.includes(localeDir)) {
|
39
|
+
localeDirs.push(localeDir);
|
40
|
+
}
|
41
|
+
appendDebugLog("Starting resource loading");
|
42
|
+
throw new Error("Resource loader invoked");
|
43
|
+
// Loop through all directories, merging resources for the same locale
|
44
|
+
// Later directories override earlier ones
|
45
|
+
for (let i = 0; i < localeDirs.length; i++) {
|
46
|
+
const dir = localeDirs[i];
|
47
|
+
if (!fs.existsSync(dir)) {
|
48
|
+
continue;
|
49
|
+
}
|
50
|
+
const names = fs.readdirSync(dir);
|
51
|
+
for (let j = 0; j < names.length; j++) {
|
52
|
+
const name = names[j];
|
53
|
+
const filepath = path.join(dir, name);
|
54
|
+
// support en_US.js => en-US.js
|
55
|
+
const locale = formatLocale(name.split(".")[0]);
|
56
|
+
let resource = {};
|
57
|
+
if (name.endsWith(".js")) {
|
58
|
+
const require = createRequire(import.meta.url);
|
59
|
+
const mod = require(filepath);
|
60
|
+
resource = flattening(mod.default || mod);
|
61
|
+
appendDebugLog(
|
62
|
+
`Loaded JS resource for locale '${locale}' from: ${filepath}`,
|
63
|
+
resource,
|
64
|
+
);
|
65
|
+
} else if (name.endsWith(".json")) {
|
66
|
+
// @ts-ignore
|
67
|
+
resource = flattening(require(filepath));
|
68
|
+
appendDebugLog(
|
69
|
+
`Loaded JSON resource for locale '${locale}' from: ${filepath}`,
|
70
|
+
resource,
|
71
|
+
);
|
72
|
+
} else if (name.endsWith(".properties")) {
|
73
|
+
resource = ini.parse(fs.readFileSync(filepath, "utf8"));
|
74
|
+
appendDebugLog(
|
75
|
+
`Loaded PROPERTIES resource for locale '${locale}' from: ${filepath}`,
|
76
|
+
resource,
|
77
|
+
);
|
78
|
+
} else if (name.endsWith(".yml") || name.endsWith(".yaml")) {
|
79
|
+
resource = flattening(yaml.load(fs.readFileSync(filepath, "utf8")));
|
80
|
+
appendDebugLog(
|
81
|
+
`Loaded YAML resource for locale '${locale}' from: ${filepath}`,
|
82
|
+
resource,
|
83
|
+
);
|
84
|
+
}
|
85
|
+
// Always merge, but let later dirs override earlier ones
|
86
|
+
resources[locale] = { ...resources[locale], ...resource };
|
87
|
+
appendDebugLog(
|
88
|
+
`Merged resource for locale '${locale}'`,
|
89
|
+
resources[locale],
|
90
|
+
);
|
91
|
+
}
|
92
|
+
}
|
93
|
+
appendDebugLog("Finished resource loading");
|
94
|
+
const debug = Debug("koa-locales");
|
95
|
+
const debugSilly = Debug("koa-locales:silly");
|
96
|
+
debug(
|
97
|
+
"Init locales with %j, got %j resources",
|
98
|
+
options,
|
99
|
+
Object.keys(resources),
|
100
|
+
);
|
101
|
+
if (typeof app[functionName] !== "undefined") {
|
102
|
+
console.warn(
|
103
|
+
'[koa-locales] will override exists "%s" function on app',
|
104
|
+
functionName,
|
105
|
+
);
|
106
|
+
}
|
107
|
+
function gettext(locale, key, value) {
|
108
|
+
if (arguments.length === 0 || arguments.length === 1) {
|
109
|
+
// __()
|
110
|
+
// --('en')
|
111
|
+
return "";
|
112
|
+
}
|
113
|
+
const resource = resources[locale] || {};
|
114
|
+
let text = resource[key];
|
115
|
+
if (text === undefined) {
|
116
|
+
text = key;
|
117
|
+
}
|
118
|
+
debugSilly("%s: %j => %j", locale, key, text);
|
119
|
+
if (!text) {
|
120
|
+
return "";
|
121
|
+
}
|
122
|
+
if (arguments.length === 2) {
|
123
|
+
// __(locale, key)
|
124
|
+
return text;
|
125
|
+
}
|
126
|
+
if (arguments.length === 3) {
|
127
|
+
if (isObject(value)) {
|
128
|
+
// __(locale, key, object)
|
129
|
+
// __('zh', '{a} {b} {b} {a}', {a: 'foo', b: 'bar'})
|
130
|
+
// =>
|
131
|
+
// foo bar bar foo
|
132
|
+
return formatWithObject(text, value);
|
133
|
+
}
|
134
|
+
if (Array.isArray(value)) {
|
135
|
+
// __(locale, key, array)
|
136
|
+
// __('zh', '{0} {1} {1} {0}', ['foo', 'bar'])
|
137
|
+
// =>
|
138
|
+
// foo bar bar foo
|
139
|
+
return formatWithArray(text, value);
|
140
|
+
}
|
141
|
+
// __(locale, key, value)
|
142
|
+
return util.format(text, value);
|
143
|
+
}
|
144
|
+
// __(locale, key, value1, ...)
|
145
|
+
const args = new Array(arguments.length - 1);
|
146
|
+
args[0] = text;
|
147
|
+
for (let i = 2; i < arguments.length; i++) {
|
148
|
+
args[i - 1] = arguments[i];
|
149
|
+
}
|
150
|
+
return util.format.apply(util, args);
|
151
|
+
}
|
152
|
+
app[functionName] = gettext;
|
153
|
+
app.context[functionName] = function (key, value) {
|
154
|
+
if (arguments.length === 0) {
|
155
|
+
// __()
|
156
|
+
return "";
|
157
|
+
}
|
158
|
+
const locale = this.__getLocale();
|
159
|
+
if (arguments.length === 1) {
|
160
|
+
return gettext(locale, key);
|
161
|
+
}
|
162
|
+
if (arguments.length === 2) {
|
163
|
+
return gettext(locale, key, value);
|
164
|
+
}
|
165
|
+
const args = new Array(arguments.length + 1);
|
166
|
+
args[0] = locale;
|
167
|
+
for (let i = 0; i < arguments.length; i++) {
|
168
|
+
args[i + 1] = arguments[i];
|
169
|
+
}
|
170
|
+
// @ts-expect-error: dynamic argument forwarding
|
171
|
+
return gettext(...args);
|
172
|
+
};
|
173
|
+
app.context.__getLocale = function () {
|
174
|
+
if (this.__locale) {
|
175
|
+
return this.__locale;
|
176
|
+
}
|
177
|
+
const cookieLocale = this.cookies.get(cookieField, { signed: false });
|
178
|
+
// 1. Query
|
179
|
+
let locale = this.query[queryField];
|
180
|
+
let localeOrigin = "query";
|
181
|
+
// 2. Cookie
|
182
|
+
if (!locale) {
|
183
|
+
locale = cookieLocale;
|
184
|
+
localeOrigin = "cookie";
|
185
|
+
}
|
186
|
+
// 3. Header
|
187
|
+
if (!locale) {
|
188
|
+
let languages = this.acceptsLanguages();
|
189
|
+
if (languages) {
|
190
|
+
if (Array.isArray(languages)) {
|
191
|
+
if (languages[0] === "*") {
|
192
|
+
languages = languages.slice(1);
|
193
|
+
}
|
194
|
+
if (languages.length > 0) {
|
195
|
+
for (let i = 0; i < languages.length; i++) {
|
196
|
+
const lang = formatLocale(languages[i]);
|
197
|
+
if (resources[lang] || localeAlias[lang]) {
|
198
|
+
locale = lang;
|
199
|
+
localeOrigin = "header";
|
200
|
+
break;
|
201
|
+
}
|
202
|
+
}
|
203
|
+
}
|
204
|
+
} else {
|
205
|
+
locale = languages;
|
206
|
+
localeOrigin = "header";
|
207
|
+
}
|
208
|
+
}
|
209
|
+
if (!locale) {
|
210
|
+
locale = defaultLocale;
|
211
|
+
localeOrigin = "default";
|
212
|
+
}
|
213
|
+
}
|
214
|
+
if (locale && locale in localeAlias) {
|
215
|
+
const originalLocale = locale;
|
216
|
+
locale = localeAlias[locale];
|
217
|
+
debugSilly(
|
218
|
+
"Used alias, received %s but using %s",
|
219
|
+
originalLocale,
|
220
|
+
locale,
|
221
|
+
);
|
222
|
+
}
|
223
|
+
locale = formatLocale(locale || defaultLocale);
|
224
|
+
if (!resources[locale]) {
|
225
|
+
debugSilly(
|
226
|
+
"Locale %s is not supported. Using default (%s)",
|
227
|
+
locale,
|
228
|
+
defaultLocale,
|
229
|
+
);
|
230
|
+
locale = defaultLocale;
|
231
|
+
}
|
232
|
+
if (writeCookie && cookieLocale !== locale && !this.headerSent) {
|
233
|
+
updateCookie(this, locale);
|
234
|
+
}
|
235
|
+
debug("Locale: %s from %s", locale, localeOrigin);
|
236
|
+
debugSilly("Locale: %s from %s", locale, localeOrigin);
|
237
|
+
this.__locale = locale;
|
238
|
+
this.__localeOrigin = localeOrigin;
|
239
|
+
return locale;
|
240
|
+
};
|
241
|
+
app.context.__getLocaleOrigin = function () {
|
242
|
+
if (this.__localeOrigin) return this.__localeOrigin;
|
243
|
+
this.__getLocale();
|
244
|
+
return this.__localeOrigin;
|
245
|
+
};
|
246
|
+
app.context.__setLocale = function (locale) {
|
247
|
+
this.__locale = locale;
|
248
|
+
this.__localeOrigin = "set";
|
249
|
+
updateCookie(this, locale);
|
250
|
+
};
|
251
|
+
function updateCookie(ctx, locale) {
|
252
|
+
const cookieOptions = {
|
253
|
+
httpOnly: false,
|
254
|
+
maxAge: cookieMaxAge,
|
255
|
+
signed: false,
|
256
|
+
domain: cookieDomain,
|
257
|
+
overwrite: true,
|
258
|
+
};
|
259
|
+
ctx.cookies.set(cookieField, locale, cookieOptions);
|
260
|
+
debugSilly("Saved cookie with locale %s", locale);
|
261
|
+
}
|
262
|
+
}
|
263
|
+
function isObject(obj) {
|
264
|
+
return Object.prototype.toString.call(obj) === "[object Object]";
|
265
|
+
}
|
266
|
+
const ARRAY_INDEX_RE = /\{(\d+)\}/g;
|
267
|
+
function formatWithArray(text, values) {
|
268
|
+
return text.replace(ARRAY_INDEX_RE, (orignal, matched) => {
|
269
|
+
const index = parseInt(matched);
|
270
|
+
if (index < values.length) {
|
271
|
+
return values[index];
|
272
|
+
}
|
273
|
+
// not match index, return orignal text
|
274
|
+
return orignal;
|
275
|
+
});
|
276
|
+
}
|
277
|
+
const Object_INDEX_RE = /\{(.+?)\}/g;
|
278
|
+
function formatWithObject(text, values) {
|
279
|
+
return text.replace(Object_INDEX_RE, (orignal, matched) => {
|
280
|
+
const value = values[matched];
|
281
|
+
if (value) {
|
282
|
+
return value;
|
283
|
+
}
|
284
|
+
// not match index, return orignal text
|
285
|
+
return orignal;
|
286
|
+
});
|
287
|
+
}
|
288
|
+
function formatLocale(locale) {
|
289
|
+
if (!locale) return "";
|
290
|
+
return locale.replace(/_/g, "-").toLowerCase();
|
291
|
+
}
|
292
|
+
function flattening(data) {
|
293
|
+
const result = {};
|
294
|
+
function deepFlat(data, keys) {
|
295
|
+
Object.keys(data).forEach((key) => {
|
296
|
+
const value = data[key];
|
297
|
+
const k = keys ? keys + "." + key : key;
|
298
|
+
if (isObject(value)) {
|
299
|
+
deepFlat(value, k);
|
300
|
+
} else {
|
301
|
+
result[k] = String(value);
|
302
|
+
}
|
303
|
+
});
|
304
|
+
}
|
305
|
+
deepFlat(data, "");
|
306
|
+
return result;
|
307
|
+
}
|
308
|
+
function appendDebugLog(message, obj) {
|
309
|
+
const logPath = path.resolve(process.cwd(), "resource-debug.log");
|
310
|
+
let line = `[DEBUG] ${message}`;
|
311
|
+
if (obj !== undefined) {
|
312
|
+
try {
|
313
|
+
line += " " + JSON.stringify(obj);
|
314
|
+
} catch {
|
315
|
+
line += " " + String(obj);
|
316
|
+
}
|
317
|
+
}
|
318
|
+
fs.appendFileSync(logPath, line + "\n");
|
319
|
+
}
|
320
|
+
export default locales;
|