@adminforth/i18n 2.3.2 → 2.5.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/base.ts +402 -0
- package/build.log +2 -2
- package/custom/BulkActionButton.vue +2 -2
- package/custom/LanguageInUserMenu.vue +4 -12
- package/custom/LanguageUnderLogin.vue +4 -12
- package/custom/TranslationJobViewComponent.vue +2 -3
- package/custom/langCommon.ts +12 -24
- package/dist/base.d.ts +59 -0
- package/dist/base.js +363 -0
- package/dist/custom/BulkActionButton.vue +2 -2
- package/dist/custom/LanguageInUserMenu.vue +4 -12
- package/dist/custom/LanguageUnderLogin.vue +4 -12
- package/dist/custom/TranslationJobViewComponent.vue +2 -3
- package/dist/custom/langCommon.ts +12 -24
- package/dist/index.d.ts +9 -17
- package/dist/index.js +58 -263
- package/dist/types.d.ts +17 -12
- package/index.ts +67 -294
- package/package.json +1 -1
- package/types.ts +20 -14
package/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import AdminForth, {
|
|
1
|
+
import AdminForth, { Filters, suggestIfTypo, AdminForthDataTypes, RAMLock, filtersTools, AdminForthFilterOperators } from "adminforth";
|
|
2
2
|
import type { IAdminForth, IHttpServer, AdminForthComponentDeclaration, AdminForthResourceColumn, AdminForthResource, BeforeLoginConfirmationFunction, AdminForthConfigMenuItem, AdminUser } from "adminforth";
|
|
3
3
|
import type { PluginOptions, SupportedLanguage } from './types.js';
|
|
4
|
+
import { I18nBase, getCountryCodeFromLangCode, getPrimaryLanguageCode, requiresSlavicPlurals, SLAVIC_PLURAL_EXAMPLES, SLAVIC_PLURAL_FORMS, SLAVIC_PLURAL_FORMS_HINT, SOURCE_LANGUAGE } from './base.js';
|
|
4
5
|
import { z } from "zod";
|
|
5
6
|
import iso6391 from 'iso-639-1';
|
|
6
7
|
|
|
@@ -25,7 +26,6 @@ import path from 'path';
|
|
|
25
26
|
import fs from 'fs-extra';
|
|
26
27
|
import chokidar from 'chokidar';
|
|
27
28
|
import { AsyncQueue } from '@sapphire/async-queue';
|
|
28
|
-
import getFlagEmoji from 'country-flag-svg';
|
|
29
29
|
import { parse } from 'bcp-47';
|
|
30
30
|
import pLimit from 'p-limit';
|
|
31
31
|
import { randomUUID } from 'crypto';
|
|
@@ -33,62 +33,6 @@ import { afLogger } from "adminforth";
|
|
|
33
33
|
|
|
34
34
|
const processFrontendMessagesQueue = new AsyncQueue();
|
|
35
35
|
|
|
36
|
-
const SLAVIC_PLURAL_EXAMPLES = {
|
|
37
|
-
uk: 'яблук | Яблуко | Яблука | Яблук', // zero | singular | 2-4 | 5+
|
|
38
|
-
bg: 'ябълки | ябълка | ябълки | ябълки', // zero | singular | 2-4 | 5+
|
|
39
|
-
cs: 'jablek | jablko | jablka | jablek', // zero | singular | 2-4 | 5+
|
|
40
|
-
hr: 'jabuka | jabuka | jabuke | jabuka', // zero | singular | 2-4 | 5+
|
|
41
|
-
mk: 'јаболка | јаболко | јаболка | јаболка', // zero | singular | 2-4 | 5+
|
|
42
|
-
pl: 'jabłek | jabłko | jabłka | jabłek', // zero | singular | 2-4 | 5+
|
|
43
|
-
sk: 'jabĺk | jablko | jablká | jabĺk', // zero | singular | 2-4 | 5+
|
|
44
|
-
sl: 'jabolk | jabolko | jabolka | jabolk', // zero | singular | 2-4 | 5+
|
|
45
|
-
sr: 'јабука | јабука | јабуке | јабука', // zero | singular | 2-4 | 5+
|
|
46
|
-
be: 'яблыкаў | яблык | яблыкі | яблыкаў', // zero | singular | 2-4 | 5+
|
|
47
|
-
ru: 'яблок | яблоко | яблока | яблок', // zero | singular | 2-4 | 5+
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const countryISO31661ByLangISO6391 = {
|
|
51
|
-
en: 'us', // English → United States
|
|
52
|
-
zh: 'cn', // Chinese → China
|
|
53
|
-
hi: 'in', // Hindi → India
|
|
54
|
-
ar: 'sa', // Arabic → Saudi Arabia
|
|
55
|
-
ko: 'kr', // Korean → South Korea
|
|
56
|
-
ja: 'jp', // Japanese → Japan
|
|
57
|
-
uk: 'ua', // Ukrainian → Ukraine
|
|
58
|
-
ur: 'pk', // Urdu → Pakistan
|
|
59
|
-
sr: 'rs', // Serbian → Serbia
|
|
60
|
-
da: 'dk' // Danish → Denmark
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
function getCountryCodeFromLangCode(lang: SupportedLanguage) {
|
|
64
|
-
const [langCode, region] = String(lang).split('-');
|
|
65
|
-
if (region && /^[A-Z]{2}$/.test(region)) {
|
|
66
|
-
return region.toLowerCase();
|
|
67
|
-
}
|
|
68
|
-
return countryISO31661ByLangISO6391[langCode] || langCode;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function getPrimaryLanguageCode(langCode: SupportedLanguage): string {
|
|
72
|
-
return String(langCode).split('-')[0];
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function isValidSupportedLanguageTag(langCode: SupportedLanguage): boolean {
|
|
76
|
-
try {
|
|
77
|
-
const schema = parse(String(langCode),
|
|
78
|
-
{
|
|
79
|
-
normalize: true,
|
|
80
|
-
warning: (reason, code, offset) => {
|
|
81
|
-
console.warn(`Warning in validating language tag ${langCode}: reason=${reason}, code=${code}, offset=${offset}`);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
);
|
|
85
|
-
const isValid = schema.language.length === 2;
|
|
86
|
-
return isValid;
|
|
87
|
-
} catch (e) {
|
|
88
|
-
return false;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
36
|
interface IFailedTranslation {
|
|
93
37
|
lang: SupportedLanguage;
|
|
94
38
|
en_string: string;
|
|
@@ -134,8 +78,7 @@ class AiTranslateError extends Error {
|
|
|
134
78
|
this.name = 'AiTranslateError';
|
|
135
79
|
}
|
|
136
80
|
}
|
|
137
|
-
export default class I18nPlugin extends
|
|
138
|
-
options: PluginOptions;
|
|
81
|
+
export default class I18nPlugin extends I18nBase<PluginOptions> {
|
|
139
82
|
emailField: AdminForthResourceColumn;
|
|
140
83
|
passwordField: AdminForthResourceColumn;
|
|
141
84
|
authResource: AdminForthResource;
|
|
@@ -146,35 +89,39 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
146
89
|
primaryKeyFieldName: string;
|
|
147
90
|
menuItemWithBadgeId: string;
|
|
148
91
|
|
|
149
|
-
adminforth: IAdminForth;
|
|
150
|
-
|
|
151
92
|
externalAppOnly: boolean;
|
|
152
93
|
|
|
153
94
|
// sorted by name list of all supported languages, without en e.g. 'al|ro|uk'
|
|
154
95
|
fullCompleatedFieldValue: string;
|
|
155
|
-
primaryLanguage: SupportedLanguage;
|
|
156
96
|
|
|
157
97
|
constructor(options: PluginOptions) {
|
|
158
|
-
super(options
|
|
159
|
-
this.options = options;
|
|
98
|
+
super(options);
|
|
160
99
|
this.cache = new CachingAdapterMemory();
|
|
161
100
|
this.trFieldNames = {};
|
|
162
|
-
this.primaryLanguage = options.primaryLanguage || 'en';
|
|
163
101
|
}
|
|
164
102
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
103
|
+
// translated languages joined by '|', e.g. 'uk|ja', stored in completedFieldName to filter not fully translated strings
|
|
104
|
+
completedLangsValue(isTranslated: (lang: SupportedLanguage) => boolean): string {
|
|
105
|
+
return this.targetLanguages().filter(isTranslated).join('|');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
computeCompletedFieldValue(record: any): string {
|
|
109
|
+
return this.completedLangsValue((lang) => Boolean(record[this.trFieldNames[lang]]));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// all translations of category for frontend_messages
|
|
113
|
+
categoryCacheKey(category: string, lang: string): string {
|
|
114
|
+
return `${this.resourceConfig.resourceId}:${category}:${lang}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// one string translated by backend tr
|
|
118
|
+
messageCacheKey(category: string, lang: string, enString: string): string {
|
|
119
|
+
return `${this.categoryCacheKey(category, lang)}:${enString}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
clearTranslationCache(category: string, lang: string, enString: string) {
|
|
123
|
+
this.cache.clear(this.categoryCacheKey(category, lang));
|
|
124
|
+
this.cache.clear(this.messageCacheKey(category, lang, enString));
|
|
178
125
|
}
|
|
179
126
|
|
|
180
127
|
async updateUntranslatedMenuBadge() {
|
|
@@ -201,13 +148,6 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
201
148
|
}
|
|
202
149
|
);
|
|
203
150
|
|
|
204
|
-
// validate each supported language: ISO 639-1 or BCP-47 with region (e.g., en-GB)
|
|
205
|
-
this.options.supportedLanguages.forEach((lang) => {
|
|
206
|
-
if (!isValidSupportedLanguageTag(lang)) {
|
|
207
|
-
throw new Error(`Invalid language code ${lang}. Use ISO 639-1 (e.g., 'en') or BCP-47 with region (e.g., 'en-GB').`);
|
|
208
|
-
}
|
|
209
|
-
});
|
|
210
|
-
|
|
211
151
|
if (this.options.translateLangAsBCP47Code) {
|
|
212
152
|
for (const [lang, bcp47] of Object.entries(this.options.translateLangAsBCP47Code)) {
|
|
213
153
|
if (!this.options.supportedLanguages.includes(lang as SupportedLanguage)) {
|
|
@@ -226,10 +166,7 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
226
166
|
}
|
|
227
167
|
|
|
228
168
|
// parse trFieldNames
|
|
229
|
-
for (const lang of this.
|
|
230
|
-
if (lang === 'en') {
|
|
231
|
-
continue;
|
|
232
|
-
}
|
|
169
|
+
for (const lang of this.targetLanguages()) {
|
|
233
170
|
if (this.options.translationFieldNames?.[lang]) {
|
|
234
171
|
this.trFieldNames[lang] = this.options.translationFieldNames[lang];
|
|
235
172
|
} else {
|
|
@@ -264,18 +201,9 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
264
201
|
};
|
|
265
202
|
}
|
|
266
203
|
|
|
267
|
-
this.enFieldName = this.trFieldNames[
|
|
204
|
+
this.enFieldName = this.trFieldNames[SOURCE_LANGUAGE] || `${SOURCE_LANGUAGE}_string`;
|
|
268
205
|
|
|
269
|
-
this.fullCompleatedFieldValue = this.
|
|
270
|
-
if (lang === 'en') {
|
|
271
|
-
return acc;
|
|
272
|
-
}
|
|
273
|
-
if (acc !== '') {
|
|
274
|
-
acc += '|';
|
|
275
|
-
}
|
|
276
|
-
acc += lang as string;
|
|
277
|
-
return acc;
|
|
278
|
-
}, '');
|
|
206
|
+
this.fullCompleatedFieldValue = this.completedLangsValue(() => true);
|
|
279
207
|
|
|
280
208
|
// if not enFieldName column is not found, throw error
|
|
281
209
|
const enColumn = resourceConfig.columns.find(c => c.name === this.enFieldName);
|
|
@@ -341,35 +269,9 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
341
269
|
});
|
|
342
270
|
}
|
|
343
271
|
|
|
344
|
-
const compMeta = {
|
|
345
|
-
brandSlug: adminforth.config.customization.brandNameSlug,
|
|
346
|
-
pluginInstanceId: this.pluginInstanceId,
|
|
347
|
-
primaryLanguage: this.primaryLanguage,
|
|
348
|
-
afOrder: this.options.loginPageLanguageSelectorOrder || 0,
|
|
349
|
-
supportedLanguages: this.options.supportedLanguages.map(lang => (
|
|
350
|
-
{
|
|
351
|
-
code: lang,
|
|
352
|
-
// lang name on on language native name
|
|
353
|
-
name: iso6391.getNativeName(getPrimaryLanguageCode(lang)),
|
|
354
|
-
}
|
|
355
|
-
))
|
|
356
|
-
};
|
|
357
272
|
// add underLogin component
|
|
358
273
|
if (!this.externalAppOnly) {
|
|
359
|
-
(adminforth
|
|
360
|
-
file: this.componentPath('LanguageUnderLogin.vue'),
|
|
361
|
-
meta: compMeta
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
(adminforth.config.customization.globalInjections.userMenu).push({
|
|
365
|
-
file: this.componentPath('LanguageInUserMenu.vue'),
|
|
366
|
-
meta: compMeta
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
adminforth.config.customization.globalInjections.everyPageBottom.push({
|
|
370
|
-
file: this.componentPath('LanguageEveryPageLoader.vue'),
|
|
371
|
-
meta: compMeta
|
|
372
|
-
});
|
|
274
|
+
this.injectLanguageSelectors(adminforth);
|
|
373
275
|
}
|
|
374
276
|
|
|
375
277
|
// disable create allowedActions for translations
|
|
@@ -377,10 +279,7 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
377
279
|
|
|
378
280
|
// add hook to validate user did not screw up with template params
|
|
379
281
|
resourceConfig.hooks.edit.beforeSave.push(async ({ updates, oldRecord }: { updates: any, oldRecord?: any }): Promise<{ ok: boolean, error?: string }> => {
|
|
380
|
-
for (const lang of this.
|
|
381
|
-
if (lang === 'en') {
|
|
382
|
-
continue;
|
|
383
|
-
}
|
|
282
|
+
for (const lang of this.targetLanguages()) {
|
|
384
283
|
if (updates[this.trFieldNames[lang]]) { // if user set '', it will have '' in updates, then it is fine, we shoudl nto check it
|
|
385
284
|
if (!ensureTemplateHasAllParams(oldRecord[this.enFieldName], updates[this.trFieldNames[lang]])) {
|
|
386
285
|
return { ok: false, error: `Template params mismatch for ${updates[this.enFieldName]}. Template param names should be the same as in original string. E. g. 'Hello {name}', should be 'Hola {name}' and not 'Hola {nombre}'!` };
|
|
@@ -394,20 +293,11 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
394
293
|
resourceConfig.hooks.edit.afterSave.push(async ({ updates, oldRecord }: { updates: any, oldRecord?: any }): Promise<{ ok: boolean, error?: string }> => {
|
|
395
294
|
if (oldRecord) {
|
|
396
295
|
// find lang which changed
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
if (lang === 'en') {
|
|
400
|
-
continue;
|
|
401
|
-
}
|
|
402
|
-
if (updates[this.trFieldNames[lang]] !== undefined) {
|
|
403
|
-
langsChanged.push(lang);
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
296
|
+
const langsChanged = this.targetLanguages().filter((lang) => updates[this.trFieldNames[lang]] !== undefined);
|
|
297
|
+
|
|
407
298
|
// clear frontend cache for all langsChanged
|
|
408
299
|
for (const lang of langsChanged) {
|
|
409
|
-
this.
|
|
410
|
-
this.cache.clear(`${this.resourceConfig.resourceId}:${oldRecord[this.options.categoryFieldName]}:${lang}:${oldRecord[this.enFieldName]}`);
|
|
300
|
+
this.clearTranslationCache(oldRecord[this.options.categoryFieldName], lang, oldRecord[this.enFieldName]);
|
|
411
301
|
}
|
|
412
302
|
this.updateUntranslatedMenuBadge();
|
|
413
303
|
}
|
|
@@ -418,9 +308,7 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
418
308
|
// add hook on delete of any translation to reset cache
|
|
419
309
|
resourceConfig.hooks.delete.afterSave.push(async ({ record }: { record: any }): Promise<{ ok: boolean, error?: string }> => {
|
|
420
310
|
for (const lang of this.options.supportedLanguages) {
|
|
421
|
-
|
|
422
|
-
this.cache.clear(`${this.resourceConfig.resourceId}:${record[this.options.categoryFieldName]}:${lang}`);
|
|
423
|
-
this.cache.clear(`${this.resourceConfig.resourceId}:${record[this.options.categoryFieldName]}:${lang}:${record[this.enFieldName]}`);
|
|
311
|
+
this.clearTranslationCache(record[this.options.categoryFieldName], lang, record[this.enFieldName]);
|
|
424
312
|
}
|
|
425
313
|
this.updateUntranslatedMenuBadge();
|
|
426
314
|
return { ok: true };
|
|
@@ -489,6 +377,7 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
489
377
|
file: this.componentPath('BulkActionButton.vue'),
|
|
490
378
|
meta: {
|
|
491
379
|
supportedLanguages: this.options.supportedLanguages,
|
|
380
|
+
countryCodes: Object.fromEntries(this.options.supportedLanguages.map((lang) => [lang, getCountryCodeFromLangCode(lang)])),
|
|
492
381
|
pluginInstanceId: this.pluginInstanceId,
|
|
493
382
|
}
|
|
494
383
|
}
|
|
@@ -671,13 +560,8 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
671
560
|
|
|
672
561
|
|
|
673
562
|
for (const lang of langsInvolved) {
|
|
674
|
-
const categoriesInvolved = new Set();
|
|
675
563
|
for (const { enStr, category } of Object.values(updateStrings)) {
|
|
676
|
-
|
|
677
|
-
await this.cache.clear(`${this.resourceConfig.resourceId}:${category}:${lang}:${enStr}`);
|
|
678
|
-
}
|
|
679
|
-
for (const category of categoriesInvolved) {
|
|
680
|
-
await this.cache.clear(`${this.resourceConfig.resourceId}:${category}:${lang}`);
|
|
564
|
+
this.clearTranslationCache(category, lang, enStr);
|
|
681
565
|
}
|
|
682
566
|
}
|
|
683
567
|
|
|
@@ -746,14 +630,14 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
746
630
|
const lang = langIsoCode;
|
|
747
631
|
const primaryLang = getPrimaryLanguageCode(lang);
|
|
748
632
|
const langName = iso6391.getName(primaryLang);
|
|
749
|
-
const requestSlavicPlurals =
|
|
633
|
+
const requestSlavicPlurals = requiresSlavicPlurals(lang) && plurals;
|
|
750
634
|
const region = langSchema.region;
|
|
751
635
|
const basePrompt = `
|
|
752
636
|
I need to translate strings in JSON to ${langName} language ${replacedLanguageCodeForTranslations || lang.length > 2 ? `BCP-47 code ${langCode}` : `ISO 639-1 code ${langIsoCode}`} from English for my web app.
|
|
753
637
|
|
|
754
638
|
|
|
755
639
|
${region ? `Use the regional conventions for ${langCode} (region ${region}), including spelling, punctuation, and formatting.` : ''}
|
|
756
|
-
${requestSlavicPlurals ? `You should provide
|
|
640
|
+
${requestSlavicPlurals ? `You should provide ${SLAVIC_PLURAL_FORMS} slavic forms (in format "${SLAVIC_PLURAL_FORMS_HINT}") e.g. "apple | apples" should become "${SLAVIC_PLURAL_EXAMPLES[primaryLang]}"` : ''}
|
|
757
641
|
JSON keys are internal identifiers. Keep keys as is and write translations into values. If values have variables (in curly brackets), then translated strings should have them as well (variables itself should not be translated). Here are the strings:
|
|
758
642
|
\`\`\`json
|
|
759
643
|
{
|
|
@@ -832,6 +716,7 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
832
716
|
translations: translations.filter(t => stringBanchCopy.includes(t.en_string)),
|
|
833
717
|
updateStrings,
|
|
834
718
|
lang,
|
|
719
|
+
countryCode: getCountryCodeFromLangCode(lang as SupportedLanguage),
|
|
835
720
|
failedToTranslate,
|
|
836
721
|
needToTranslateByLang,
|
|
837
722
|
promptCost: basePromptTokenLength + banchTokens,
|
|
@@ -866,8 +751,8 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
866
751
|
const translations = await this.adminforth.resource(this.resourceConfig.resourceId).list(Filters.IN(this.primaryKeyFieldName, selectedIds));
|
|
867
752
|
const languagesToProcess = selectedLanguages || this.options.supportedLanguages;
|
|
868
753
|
for (const lang of languagesToProcess) {
|
|
869
|
-
if (lang ===
|
|
870
|
-
// all strings are in
|
|
754
|
+
if (lang === SOURCE_LANGUAGE) {
|
|
755
|
+
// all strings are in source language, no need to translate
|
|
871
756
|
continue;
|
|
872
757
|
}
|
|
873
758
|
for (const translation of translations) {
|
|
@@ -1049,10 +934,7 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
1049
934
|
this.options.completeAdapter.validate();
|
|
1050
935
|
}
|
|
1051
936
|
|
|
1052
|
-
for (const lang of this.
|
|
1053
|
-
if (lang === 'en') {
|
|
1054
|
-
continue;
|
|
1055
|
-
}
|
|
937
|
+
for (const lang of this.targetLanguages()) {
|
|
1056
938
|
const column = resourceConfig.columns.find(c => c.name === this.trFieldNames[lang]);
|
|
1057
939
|
if (!column) {
|
|
1058
940
|
throw new Error(`Field ${this.trFieldNames[lang]} not found for storing translation for language ${lang}
|
|
@@ -1092,151 +974,51 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
1092
974
|
if (!this.externalAppOnly) {
|
|
1093
975
|
this.tryProcessAndWatch(adminforth);
|
|
1094
976
|
|
|
1095
|
-
adminforth
|
|
1096
|
-
if (!msg) {
|
|
1097
|
-
return msg;
|
|
1098
|
-
}
|
|
1099
|
-
|
|
977
|
+
this.installBackendTranslator(adminforth, async (msg, category, lang) => {
|
|
1100
978
|
if (category === 'frontend') {
|
|
1101
979
|
throw new Error(`Category 'frontend' is reserved for frontend messages, use any other category for backend messages`);
|
|
1102
980
|
}
|
|
1103
|
-
// console.log('🪲tr', msg, category, lang);
|
|
1104
981
|
|
|
1105
|
-
// if lang is not supported, fallback to primaryLanguage, then to english
|
|
1106
|
-
if (!this.options.supportedLanguages.includes(lang as SupportedLanguage)) {
|
|
1107
|
-
lang = this.primaryLanguage; // fallback to primary language first
|
|
1108
|
-
if (!this.options.supportedLanguages.includes(lang as SupportedLanguage)) {
|
|
1109
|
-
lang = 'en'; // final fallback to english
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
let result;
|
|
1114
982
|
// try to get translation from cache
|
|
1115
|
-
const cacheKey =
|
|
983
|
+
const cacheKey = this.messageCacheKey(category, lang, msg);
|
|
1116
984
|
const cached = await this.cache.get(cacheKey);
|
|
1117
985
|
if (cached) {
|
|
1118
|
-
|
|
986
|
+
return cached;
|
|
1119
987
|
}
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
this.updateUntranslatedMenuBadge();
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
// do this check here, to faster register missing translations
|
|
1132
|
-
// also not cache it - no sense to cache english strings
|
|
1133
|
-
if (lang === 'en') {
|
|
1134
|
-
// set to cache to return faster next time
|
|
1135
|
-
result = msg;
|
|
1136
|
-
} else {
|
|
1137
|
-
result = translation?.[this.trFieldNames[lang]];
|
|
1138
|
-
if (!result) {
|
|
1139
|
-
// return english
|
|
1140
|
-
result = msg;
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1143
|
-
// cache so even if key does not exist, we will not hit database
|
|
1144
|
-
await this.cache.set(cacheKey, result);
|
|
1145
|
-
}
|
|
1146
|
-
// if msg has '|' in it, then we need to aplly pluralization
|
|
1147
|
-
if (msg.includes('|') && pluralizationNumber !== undefined) {
|
|
1148
|
-
result = this.applyPluralization(result, pluralizationNumber, lang);
|
|
988
|
+
const resource = adminforth.resource(resourceConfig.resourceId);
|
|
989
|
+
const translation = await resource.get([Filters.EQ(this.enFieldName, msg), Filters.EQ(this.options.categoryFieldName, category)]);
|
|
990
|
+
if (!translation) {
|
|
991
|
+
await resource.create({
|
|
992
|
+
[this.enFieldName]: msg,
|
|
993
|
+
[this.options.categoryFieldName]: category,
|
|
994
|
+
});
|
|
995
|
+
this.updateUntranslatedMenuBadge();
|
|
1149
996
|
}
|
|
1150
997
|
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
998
|
+
// do this check here, to faster register missing translations
|
|
999
|
+
const result = lang === SOURCE_LANGUAGE ? msg : translation?.[this.trFieldNames[lang]] || msg;
|
|
1000
|
+
// cache so even if key does not exist, we will not hit database
|
|
1001
|
+
await this.cache.set(cacheKey, result);
|
|
1158
1002
|
return result;
|
|
1159
|
-
}
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
applyPluralization(str: string, number: number, lang: string): string {
|
|
1164
|
-
const choices = str.split('|');
|
|
1165
|
-
const choicesLength = choices.length;
|
|
1166
|
-
|
|
1167
|
-
if (choicesLength > 2) {
|
|
1168
|
-
// this is slavic pluralization
|
|
1169
|
-
return choices[this.slavicPluralRule(number, choicesLength)];
|
|
1170
|
-
} else {
|
|
1171
|
-
return number === 1 ? choices[0] : choices[1];
|
|
1172
|
-
}
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
// taken from here https://vue-i18n.intlify.dev/guide/essentials/pluralization.html#custom-pluralization
|
|
1176
|
-
slavicPluralRule(choice, choicesLength) {
|
|
1177
|
-
if (choice === 0) {
|
|
1178
|
-
return 0
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
const teen = choice > 10 && choice < 20
|
|
1182
|
-
const endsWithOne = choice % 10 === 1
|
|
1183
|
-
|
|
1184
|
-
if (!teen && endsWithOne) {
|
|
1185
|
-
return 1
|
|
1186
|
-
}
|
|
1187
|
-
if (!teen && choice % 10 >= 2 && choice % 10 <= 4) {
|
|
1188
|
-
return 2
|
|
1003
|
+
});
|
|
1189
1004
|
}
|
|
1190
|
-
|
|
1191
|
-
return choicesLength < 4 ? 2 : 3
|
|
1192
|
-
}
|
|
1193
|
-
instanceUniqueRepresentation(pluginOptions: any) : string {
|
|
1194
|
-
// optional method to return unique string representation of plugin instance.
|
|
1195
|
-
// Needed if plugin can have multiple instances on one resource
|
|
1196
|
-
return `single`;
|
|
1197
1005
|
}
|
|
1198
1006
|
|
|
1199
1007
|
async getCategoryTranslations(category: string, lang: string): Promise<Record<string, string>> {
|
|
1200
1008
|
const resource = this.adminforth.resource(this.resourceConfig.resourceId);
|
|
1201
|
-
const cacheKey =
|
|
1009
|
+
const cacheKey = this.categoryCacheKey(category, lang);
|
|
1202
1010
|
const cached = await this.cache.get(cacheKey);
|
|
1203
1011
|
if (cached) {
|
|
1204
1012
|
return cached;
|
|
1205
1013
|
}
|
|
1206
|
-
const translations = {};
|
|
1207
1014
|
const allTranslations = await resource.list([Filters.EQ(this.options.categoryFieldName, category)]);
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
}
|
|
1015
|
+
const translations = this.fallbackToOriginal(Object.fromEntries(
|
|
1016
|
+
allTranslations.map((tr) => [tr[this.enFieldName], tr[this.trFieldNames[lang]]]),
|
|
1017
|
+
));
|
|
1212
1018
|
await this.cache.set(cacheKey, translations);
|
|
1213
1019
|
return translations;
|
|
1214
1020
|
}
|
|
1215
1021
|
|
|
1216
|
-
async languagesList(): Promise<{
|
|
1217
|
-
code: SupportedLanguage;
|
|
1218
|
-
nameOnNative: string;
|
|
1219
|
-
nameEnglish: string;
|
|
1220
|
-
emojiFlag: string;
|
|
1221
|
-
svgFlagB64: string;
|
|
1222
|
-
}[]> {
|
|
1223
|
-
return Promise.all(this.options.supportedLanguages.map(async (lang) => {
|
|
1224
|
-
const flagEmoji =
|
|
1225
|
-
typeof getFlagEmoji === 'function'
|
|
1226
|
-
? getFlagEmoji
|
|
1227
|
-
: getFlagEmoji.default;
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
return {
|
|
1231
|
-
code: lang,
|
|
1232
|
-
nameOnNative: iso6391.getNativeName(getPrimaryLanguageCode(lang)),
|
|
1233
|
-
nameEnglish: iso6391.getName(getPrimaryLanguageCode(lang)),
|
|
1234
|
-
emojiFlag: getCountryCodeFromLangCode(lang).toUpperCase().replace(/./g, char => String.fromCodePoint(char.charCodeAt(0) + 127397)),
|
|
1235
|
-
svgFlagB64: await flagEmoji(getCountryCodeFromLangCode(lang)),
|
|
1236
|
-
};
|
|
1237
|
-
}));
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
1022
|
async feedCategoryTranslations(messages: {
|
|
1241
1023
|
en_string: string;
|
|
1242
1024
|
source: string;
|
|
@@ -1279,19 +1061,7 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
1279
1061
|
|
|
1280
1062
|
|
|
1281
1063
|
setupEndpoints(server: IHttpServer) {
|
|
1282
|
-
|
|
1283
|
-
method: 'GET',
|
|
1284
|
-
path: `/plugin/${this.pluginInstanceId}/frontend_messages`,
|
|
1285
|
-
noAuth: true,
|
|
1286
|
-
handler: async ({ query }) => {
|
|
1287
|
-
const lang = query.lang;
|
|
1288
|
-
|
|
1289
|
-
// form map of translations
|
|
1290
|
-
const translations = await this.getCategoryTranslations('frontend', lang);
|
|
1291
|
-
return translations;
|
|
1292
|
-
|
|
1293
|
-
}
|
|
1294
|
-
});
|
|
1064
|
+
super.setupEndpoints(server);
|
|
1295
1065
|
|
|
1296
1066
|
const lock = new RAMLock();
|
|
1297
1067
|
|
|
@@ -1452,3 +1222,6 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
1452
1222
|
}
|
|
1453
1223
|
|
|
1454
1224
|
}
|
|
1225
|
+
|
|
1226
|
+
export { I18nBase, getCountryCodeFromLangCode, getPrimaryLanguageCode, isValidSupportedLanguageTag, requiresSlavicPlurals, SOURCE_LANGUAGE, PLURAL_SEPARATOR, SLAVIC_PLURAL_FORMS, SLAVIC_PLURAL_FORMS_HINT } from './base.js';
|
|
1227
|
+
export type { I18nBaseOptions, SupportedLanguage } from './types.js';
|
package/package.json
CHANGED
package/types.ts
CHANGED
|
@@ -8,11 +8,30 @@ type Alpha2Code = keyof typeof iso31661Alpha2ToAlpha3;
|
|
|
8
8
|
type Bcp47LanguageTag = `${LanguageCode}-${Alpha2Code}`;
|
|
9
9
|
export type SupportedLanguage = LanguageCode | Bcp47LanguageTag;
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Options shared by all i18n plugins built on top of I18nBase
|
|
13
|
+
*/
|
|
14
|
+
export interface I18nBaseOptions extends PluginsCommonOptions {
|
|
12
15
|
|
|
13
16
|
/* List of language codes which you want to support. Can be either short ISO 639-1 language codes or/and BCP47 tags */
|
|
14
17
|
supportedLanguages: SupportedLanguage[];
|
|
15
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Language selector on login page order
|
|
21
|
+
*/
|
|
22
|
+
loginPageLanguageSelectorOrder?: number;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Primary language for the application. This is the default language shown to users.
|
|
26
|
+
* English is always used as the source language for translations, even if primaryLanguage is different.
|
|
27
|
+
* When a translation is missing for the primaryLanguage, English will be shown as fallback.
|
|
28
|
+
* Defaults to 'en' if not specified.
|
|
29
|
+
*/
|
|
30
|
+
primaryLanguage?: SupportedLanguage;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface PluginOptions extends I18nBaseOptions {
|
|
34
|
+
|
|
16
35
|
/**
|
|
17
36
|
* Each translation string will be stored in a separate field, you can remap it to existing columns using this option
|
|
18
37
|
* By default it will assume field are named like `${lang_code}_string` (e.g. 'en_string', 'uk_string', 'ja_string', 'fr_string')
|
|
@@ -26,11 +45,6 @@ export interface PluginOptions extends PluginsCommonOptions {
|
|
|
26
45
|
*/
|
|
27
46
|
categoryFieldName: string;
|
|
28
47
|
|
|
29
|
-
/**
|
|
30
|
-
* Language selector on login page order
|
|
31
|
-
*/
|
|
32
|
-
loginPageLanguageSelectorOrder?: number;
|
|
33
|
-
|
|
34
48
|
/**
|
|
35
49
|
* Optional source field to store e.g. file name where it first was captured
|
|
36
50
|
*/
|
|
@@ -61,14 +75,6 @@ export interface PluginOptions extends PluginsCommonOptions {
|
|
|
61
75
|
*/
|
|
62
76
|
reviewedCheckboxesFieldName?: string;
|
|
63
77
|
|
|
64
|
-
/**
|
|
65
|
-
* Primary language for the application. This is the default language shown to users.
|
|
66
|
-
* English is always used as the source language for translations, even if primaryLanguage is different.
|
|
67
|
-
* When a translation is missing for the primaryLanguage, English will be shown as fallback.
|
|
68
|
-
* Defaults to 'en' if not specified.
|
|
69
|
-
*/
|
|
70
|
-
primaryLanguage?: SupportedLanguage;
|
|
71
|
-
|
|
72
78
|
/**
|
|
73
79
|
* Ask translator to treat some code from supportedLanguages as exact BCP47 tag. Read docs for details.
|
|
74
80
|
* key - one of the values form supportedLanguages, value -BCP47 tag
|