@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/dist/base.js
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { AdminForthPlugin } from "adminforth";
|
|
11
|
+
import iso6391 from 'iso-639-1';
|
|
12
|
+
import getFlagEmoji from 'country-flag-svg';
|
|
13
|
+
import { parse } from 'bcp-47';
|
|
14
|
+
const REGION_CODE_RE = /^[A-Z]{2}$/;
|
|
15
|
+
// keys of all translations are original English strings
|
|
16
|
+
export const SOURCE_LANGUAGE = 'en';
|
|
17
|
+
export const PLURAL_SEPARATOR = '|';
|
|
18
|
+
export const SLAVIC_PLURAL_FORMS = 4;
|
|
19
|
+
export const SLAVIC_PLURAL_FORMS_HINT = 'zero | singular | 2-4 | 5+';
|
|
20
|
+
// forms in SLAVIC_PLURAL_FORMS_HINT order
|
|
21
|
+
export const SLAVIC_PLURAL_EXAMPLES = {
|
|
22
|
+
uk: 'яблук | яблуко | яблука | яблук',
|
|
23
|
+
bg: 'ябълки | ябълка | ябълки | ябълки',
|
|
24
|
+
cs: 'jablek | jablko | jablka | jablek',
|
|
25
|
+
hr: 'jabuka | jabuka | jabuke | jabuka',
|
|
26
|
+
mk: 'јаболка | јаболко | јаболка | јаболка',
|
|
27
|
+
pl: 'jabłek | jabłko | jabłka | jabłek',
|
|
28
|
+
sk: 'jabĺk | jablko | jablká | jabĺk',
|
|
29
|
+
sl: 'jabolk | jabolko | jabolka | jabolk',
|
|
30
|
+
sr: 'јабука | јабука | јабуке | јабука',
|
|
31
|
+
be: 'яблыкаў | яблык | яблыкі | яблыкаў',
|
|
32
|
+
ru: 'яблок | яблоко | яблока | яблок',
|
|
33
|
+
};
|
|
34
|
+
const countryISO31661ByLangISO6391 = {
|
|
35
|
+
en: 'us', // English → United States
|
|
36
|
+
zh: 'cn', // Chinese → China
|
|
37
|
+
hi: 'in', // Hindi → India
|
|
38
|
+
ar: 'sa', // Arabic → Saudi Arabia
|
|
39
|
+
ko: 'kr', // Korean → South Korea
|
|
40
|
+
ja: 'jp', // Japanese → Japan
|
|
41
|
+
uk: 'ua', // Ukrainian → Ukraine
|
|
42
|
+
ur: 'pk', // Urdu → Pakistan
|
|
43
|
+
sr: 'rs', // Serbian → Serbia
|
|
44
|
+
da: 'dk', // Danish → Denmark
|
|
45
|
+
sv: 'se', // Swedish → Sweden
|
|
46
|
+
he: 'il', // Hebrew → Israel
|
|
47
|
+
el: 'gr', // Greek → Greece
|
|
48
|
+
cs: 'cz', // Czech → Czech Republic
|
|
49
|
+
vi: 'vn', // Vietnamese → Vietnam
|
|
50
|
+
aa: 'et', // Afar → Ethiopia
|
|
51
|
+
ab: 'ge', // Abkhaz → Georgia
|
|
52
|
+
ae: 'ir', // Avestan → Iran
|
|
53
|
+
af: 'za', // Afrikaans → South Africa
|
|
54
|
+
ak: 'gh', // Akan → Ghana
|
|
55
|
+
am: 'et', // Amharic → Ethiopia
|
|
56
|
+
an: 'es', // Aragonese → Spain
|
|
57
|
+
as: 'in', // Assamese → India
|
|
58
|
+
av: 'ru', // Avaric → Russia
|
|
59
|
+
ay: 'bo', // Aymara → Bolivia
|
|
60
|
+
ba: 'ru', // Bashkir → Russia
|
|
61
|
+
be: 'by', // Belarusian → Belarus
|
|
62
|
+
bi: 'vu', // Bislama → Vanuatu
|
|
63
|
+
bm: 'ml', // Bambara → Mali
|
|
64
|
+
bn: 'bd', // Bengali → Bangladesh
|
|
65
|
+
bo: 'cn', // Tibetan → China
|
|
66
|
+
br: 'fr', // Breton → France
|
|
67
|
+
bs: 'ba', // Bosnian → Bosnia & Herzegovina
|
|
68
|
+
ca: 'es', // Catalan → Spain
|
|
69
|
+
ce: 'ru', // Chechen → Russia
|
|
70
|
+
ch: 'gu', // Chamorro → Guam
|
|
71
|
+
co: 'fr', // Corsican → France
|
|
72
|
+
cr: 'ca', // Cree → Canada
|
|
73
|
+
cu: 'ru', // Old Church Slavonic → Russia
|
|
74
|
+
cv: 'ru', // Chuvash → Russia
|
|
75
|
+
cy: 'gb', // Welsh → United Kingdom
|
|
76
|
+
dv: 'mv', // Divehi → Maldives
|
|
77
|
+
dz: 'bt', // Dzongkha → Bhutan
|
|
78
|
+
ee: 'gh', // Ewe → Ghana
|
|
79
|
+
et: 'ee', // Estonian → Estonia
|
|
80
|
+
eu: 'es', // Basque → Spain
|
|
81
|
+
fa: 'ir', // Persian → Iran
|
|
82
|
+
ff: 'sn', // Fula → Senegal
|
|
83
|
+
fy: 'nl', // Western Frisian → Netherlands
|
|
84
|
+
ga: 'ie', // Irish → Ireland
|
|
85
|
+
gd: 'gb', // Scottish Gaelic → United Kingdom
|
|
86
|
+
gl: 'es', // Galician → Spain
|
|
87
|
+
gn: 'py', // Guaraní → Paraguay
|
|
88
|
+
gu: 'in', // Gujarati → India
|
|
89
|
+
gv: 'im', // Manx → Isle of Man
|
|
90
|
+
ha: 'ng', // Hausa → Nigeria
|
|
91
|
+
ho: 'pg', // Hiri Motu → Papua New Guinea
|
|
92
|
+
hy: 'am', // Armenian → Armenia
|
|
93
|
+
hz: 'na', // Herero → Namibia
|
|
94
|
+
ie: 'ee', // Interlingue → Estonia
|
|
95
|
+
ig: 'ng', // Igbo → Nigeria
|
|
96
|
+
ii: 'cn', // Nuosu → China
|
|
97
|
+
ik: 'us', // Inupiaq → United States
|
|
98
|
+
iu: 'ca', // Inuktitut → Canada
|
|
99
|
+
jv: 'id', // Javanese → Indonesia
|
|
100
|
+
ka: 'ge', // Georgian → Georgia
|
|
101
|
+
kg: 'cd', // Kongo → Congo - Kinshasa
|
|
102
|
+
ki: 'ke', // Kikuyu → Kenya
|
|
103
|
+
kj: 'na', // Kwanyama → Namibia
|
|
104
|
+
kk: 'kz', // Kazakh → Kazakhstan
|
|
105
|
+
kl: 'gl', // Kalaallisut → Greenland
|
|
106
|
+
km: 'kh', // Khmer → Cambodia
|
|
107
|
+
kn: 'in', // Kannada → India
|
|
108
|
+
kr: 'ng', // Kanuri → Nigeria
|
|
109
|
+
ks: 'in', // Kashmiri → India
|
|
110
|
+
ku: 'tr', // Kurdish → Türkiye
|
|
111
|
+
kv: 'ru', // Komi → Russia
|
|
112
|
+
kw: 'gb', // Cornish → United Kingdom
|
|
113
|
+
ky: 'kg', // Kyrgyz → Kyrgyzstan
|
|
114
|
+
la: 'va', // Latin → Vatican City
|
|
115
|
+
lb: 'lu', // Luxembourgish → Luxembourg
|
|
116
|
+
lg: 'ug', // Ganda → Uganda
|
|
117
|
+
li: 'nl', // Limburgish → Netherlands
|
|
118
|
+
ln: 'cd', // Lingala → Congo - Kinshasa
|
|
119
|
+
lo: 'la', // Lao → Laos
|
|
120
|
+
lu: 'cd', // Luba-Katanga → Congo - Kinshasa
|
|
121
|
+
mi: 'nz', // Māori → New Zealand
|
|
122
|
+
ml: 'in', // Malayalam → India
|
|
123
|
+
mr: 'in', // Marathi → India
|
|
124
|
+
ms: 'my', // Malay → Malaysia
|
|
125
|
+
my: 'mm', // Burmese → Myanmar (Burma)
|
|
126
|
+
na: 'nr', // Nauru → Nauru
|
|
127
|
+
nb: 'no', // Norwegian Bokmål → Norway
|
|
128
|
+
nd: 'zw', // Northern Ndebele → Zimbabwe
|
|
129
|
+
ne: 'np', // Nepali → Nepal
|
|
130
|
+
ng: 'na', // Ndonga → Namibia
|
|
131
|
+
nn: 'no', // Norwegian Nynorsk → Norway
|
|
132
|
+
nr: 'za', // Southern Ndebele → South Africa
|
|
133
|
+
nv: 'us', // Navajo → United States
|
|
134
|
+
ny: 'mw', // Chichewa → Malawi
|
|
135
|
+
oc: 'fr', // Occitan → France
|
|
136
|
+
oj: 'ca', // Ojibwe → Canada
|
|
137
|
+
om: 'et', // Oromo → Ethiopia
|
|
138
|
+
or: 'in', // Oriya → India
|
|
139
|
+
os: 'ge', // Ossetian → Georgia
|
|
140
|
+
pa: 'in', // Panjabi → India
|
|
141
|
+
pi: 'gb', // Pāli → United Kingdom
|
|
142
|
+
ps: 'af', // Pashto → Afghanistan
|
|
143
|
+
qu: 'pe', // Quechua → Peru
|
|
144
|
+
rm: 'ch', // Romansh → Switzerland
|
|
145
|
+
rn: 'bi', // Kirundi → Burundi
|
|
146
|
+
sa: 'in', // Sanskrit → India
|
|
147
|
+
sc: 'it', // Sardinian → Italy
|
|
148
|
+
sd: 'pk', // Sindhi → Pakistan
|
|
149
|
+
se: 'no', // Northern Sami → Norway
|
|
150
|
+
sg: 'cf', // Sango → Central African Republic
|
|
151
|
+
si: 'lk', // Sinhala → Sri Lanka
|
|
152
|
+
sl: 'si', // Slovenian → Slovenia
|
|
153
|
+
sm: 'ws', // Samoan → Samoa
|
|
154
|
+
sn: 'zw', // Shona → Zimbabwe
|
|
155
|
+
sq: 'al', // Albanian → Albania
|
|
156
|
+
ss: 'za', // Swati → South Africa
|
|
157
|
+
st: 'za', // Southern Sotho → South Africa
|
|
158
|
+
su: 'id', // Sundanese → Indonesia
|
|
159
|
+
sw: 'tz', // Swahili → Tanzania
|
|
160
|
+
ta: 'in', // Tamil → India
|
|
161
|
+
te: 'in', // Telugu → India
|
|
162
|
+
tg: 'tj', // Tajik → Tajikistan
|
|
163
|
+
ti: 'et', // Tigrinya → Ethiopia
|
|
164
|
+
tk: 'tm', // Turkmen → Turkmenistan
|
|
165
|
+
tl: 'ph', // Tagalog → Philippines
|
|
166
|
+
tn: 'za', // Tswana → South Africa
|
|
167
|
+
ts: 'za', // Tsonga → South Africa
|
|
168
|
+
tt: 'ru', // Tatar → Russia
|
|
169
|
+
tw: 'gh', // Twi → Ghana
|
|
170
|
+
ty: 'pf', // Tahitian → French Polynesia
|
|
171
|
+
ug: 'cn', // Uyghur → China
|
|
172
|
+
ve: 'za', // Venda → South Africa
|
|
173
|
+
wa: 'be', // Walloon → Belgium
|
|
174
|
+
wo: 'sn', // Wolof → Senegal
|
|
175
|
+
xh: 'za', // Xhosa → South Africa
|
|
176
|
+
yi: 'ua', // Yiddish → Ukraine
|
|
177
|
+
yo: 'ng', // Yoruba → Nigeria
|
|
178
|
+
za: 'cn', // Zhuang → China
|
|
179
|
+
zu: 'za', // Zulu → South Africa
|
|
180
|
+
};
|
|
181
|
+
export function getCountryCodeFromLangCode(lang) {
|
|
182
|
+
const [langCode, region] = String(lang).split('-');
|
|
183
|
+
if (region && REGION_CODE_RE.test(region)) {
|
|
184
|
+
return region.toLowerCase();
|
|
185
|
+
}
|
|
186
|
+
return countryISO31661ByLangISO6391[langCode] || langCode;
|
|
187
|
+
}
|
|
188
|
+
export function getPrimaryLanguageCode(langCode) {
|
|
189
|
+
return String(langCode).split('-')[0];
|
|
190
|
+
}
|
|
191
|
+
// languages which need 4 plural forms "zero | singular | 2-4 | 5+" instead of English ones
|
|
192
|
+
export function requiresSlavicPlurals(lang) {
|
|
193
|
+
return getPrimaryLanguageCode(lang) in SLAVIC_PLURAL_EXAMPLES;
|
|
194
|
+
}
|
|
195
|
+
export function isValidSupportedLanguageTag(langCode) {
|
|
196
|
+
try {
|
|
197
|
+
const schema = parse(String(langCode), {
|
|
198
|
+
normalize: true,
|
|
199
|
+
warning: (reason, code, offset) => {
|
|
200
|
+
console.warn(`Warning in validating language tag ${langCode}: reason=${reason}, code=${code}, offset=${offset}`);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
const isValid = schema.language.length === 2;
|
|
204
|
+
return isValid;
|
|
205
|
+
}
|
|
206
|
+
catch (e) {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Storage-independent part of i18n plugins: language validation, language selector components,
|
|
212
|
+
* frontend messages endpoint and formatting of translated strings (params and pluralization).
|
|
213
|
+
* Subclasses provide translations storage by implementing getCategoryTranslations.
|
|
214
|
+
* Custom components are resolved from this package's custom folder, so subclasses from other packages reuse them.
|
|
215
|
+
*/
|
|
216
|
+
export class I18nBase extends AdminForthPlugin {
|
|
217
|
+
constructor(options) {
|
|
218
|
+
super(options, import.meta.url);
|
|
219
|
+
this.options = options;
|
|
220
|
+
this.primaryLanguage = options.primaryLanguage || SOURCE_LANGUAGE;
|
|
221
|
+
this.validateSupportedLanguages();
|
|
222
|
+
}
|
|
223
|
+
// app has one translations storage, so plugin instance id does not depend on options
|
|
224
|
+
instanceUniqueRepresentation() {
|
|
225
|
+
return 'single';
|
|
226
|
+
}
|
|
227
|
+
// languages translations are written for: strings in source language are the keys themselves
|
|
228
|
+
targetLanguages() {
|
|
229
|
+
return this.options.supportedLanguages.filter((lang) => lang !== SOURCE_LANGUAGE);
|
|
230
|
+
}
|
|
231
|
+
// empty translation means "not translated yet", original string is shown instead
|
|
232
|
+
fallbackToOriginal(messages) {
|
|
233
|
+
return Object.fromEntries(Object.entries(messages).map(([key, value]) => [key, value || key]));
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Translates backend strings (menu, resource labels, errors) with adminforth.tr,
|
|
237
|
+
* subclass provides only translation lookup in its storage for already resolved language
|
|
238
|
+
*/
|
|
239
|
+
installBackendTranslator(adminforth, lookup) {
|
|
240
|
+
adminforth.tr = (msg, category, lang, params, pluralizationNumber) => __awaiter(this, void 0, void 0, function* () {
|
|
241
|
+
if (!msg) {
|
|
242
|
+
return msg;
|
|
243
|
+
}
|
|
244
|
+
const resolvedLang = this.resolveLanguage(lang);
|
|
245
|
+
const translation = yield lookup(msg, category, resolvedLang);
|
|
246
|
+
return this.formatTranslation(msg, translation || msg, resolvedLang, params, pluralizationNumber);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
// validate each supported language: ISO 639-1 or BCP-47 with region (e.g., en-GB)
|
|
250
|
+
validateSupportedLanguages() {
|
|
251
|
+
this.options.supportedLanguages.forEach((lang) => {
|
|
252
|
+
if (!isValidSupportedLanguageTag(lang)) {
|
|
253
|
+
throw new Error(`Invalid language code ${lang}. Use ISO 639-1 (e.g., 'en') or BCP-47 with region (e.g., 'en-GB').`);
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
injectLanguageSelectors(adminforth) {
|
|
258
|
+
const compMeta = {
|
|
259
|
+
brandSlug: adminforth.config.customization.brandNameSlug,
|
|
260
|
+
pluginInstanceId: this.pluginInstanceId,
|
|
261
|
+
primaryLanguage: this.primaryLanguage,
|
|
262
|
+
afOrder: this.options.loginPageLanguageSelectorOrder || 0,
|
|
263
|
+
supportedLanguages: this.options.supportedLanguages.map(lang => ({
|
|
264
|
+
code: lang,
|
|
265
|
+
// lang name on on language native name
|
|
266
|
+
name: iso6391.getNativeName(getPrimaryLanguageCode(lang)),
|
|
267
|
+
countryCode: getCountryCodeFromLangCode(lang),
|
|
268
|
+
}))
|
|
269
|
+
};
|
|
270
|
+
adminforth.config.customization.loginPageInjections.underLoginButton.push({
|
|
271
|
+
file: this.componentPath('LanguageUnderLogin.vue'),
|
|
272
|
+
meta: compMeta
|
|
273
|
+
});
|
|
274
|
+
(adminforth.config.customization.globalInjections.userMenu).push({
|
|
275
|
+
file: this.componentPath('LanguageInUserMenu.vue'),
|
|
276
|
+
meta: compMeta
|
|
277
|
+
});
|
|
278
|
+
adminforth.config.customization.globalInjections.everyPageBottom.push({
|
|
279
|
+
file: this.componentPath('LanguageEveryPageLoader.vue'),
|
|
280
|
+
meta: compMeta
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
// if lang is not supported, fallback to primaryLanguage, then to english
|
|
284
|
+
resolveLanguage(lang) {
|
|
285
|
+
if (!this.options.supportedLanguages.includes(lang)) {
|
|
286
|
+
lang = this.primaryLanguage; // fallback to primary language first
|
|
287
|
+
if (!this.options.supportedLanguages.includes(lang)) {
|
|
288
|
+
lang = SOURCE_LANGUAGE; // final fallback to english
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return lang;
|
|
292
|
+
}
|
|
293
|
+
formatTranslation(msg, translation, lang, params, pluralizationNumber) {
|
|
294
|
+
let result = translation;
|
|
295
|
+
// if msg has '|' in it, then we need to aplly pluralization
|
|
296
|
+
if (msg.includes(PLURAL_SEPARATOR) && pluralizationNumber !== undefined) {
|
|
297
|
+
result = this.applyPluralization(result, pluralizationNumber, lang);
|
|
298
|
+
}
|
|
299
|
+
if (params) {
|
|
300
|
+
for (const [key, value] of Object.entries(params)) {
|
|
301
|
+
if (result) {
|
|
302
|
+
result = result.replace(`{${key}}`, value);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return result;
|
|
307
|
+
}
|
|
308
|
+
applyPluralization(str, number, lang) {
|
|
309
|
+
const choices = str.split(PLURAL_SEPARATOR);
|
|
310
|
+
const choicesLength = choices.length;
|
|
311
|
+
if (choicesLength > 2) {
|
|
312
|
+
// this is slavic pluralization
|
|
313
|
+
return choices[this.slavicPluralRule(number, choicesLength)];
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
return number === 1 ? choices[0] : choices[1];
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
// taken from here https://vue-i18n.intlify.dev/guide/essentials/pluralization.html#custom-pluralization
|
|
320
|
+
slavicPluralRule(choice, choicesLength) {
|
|
321
|
+
if (choice === 0) {
|
|
322
|
+
return 0;
|
|
323
|
+
}
|
|
324
|
+
const teen = choice > 10 && choice < 20;
|
|
325
|
+
const endsWithOne = choice % 10 === 1;
|
|
326
|
+
if (!teen && endsWithOne) {
|
|
327
|
+
return 1;
|
|
328
|
+
}
|
|
329
|
+
if (!teen && choice % 10 >= 2 && choice % 10 <= 4) {
|
|
330
|
+
return 2;
|
|
331
|
+
}
|
|
332
|
+
return choicesLength < 4 ? 2 : 3;
|
|
333
|
+
}
|
|
334
|
+
languagesList() {
|
|
335
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
336
|
+
return Promise.all(this.options.supportedLanguages.map((lang) => __awaiter(this, void 0, void 0, function* () {
|
|
337
|
+
const flagEmoji = typeof getFlagEmoji === 'function'
|
|
338
|
+
? getFlagEmoji
|
|
339
|
+
: getFlagEmoji.default;
|
|
340
|
+
return {
|
|
341
|
+
code: lang,
|
|
342
|
+
nameOnNative: iso6391.getNativeName(getPrimaryLanguageCode(lang)),
|
|
343
|
+
nameEnglish: iso6391.getName(getPrimaryLanguageCode(lang)),
|
|
344
|
+
emojiFlag: getCountryCodeFromLangCode(lang).toUpperCase().replace(/./g, char => String.fromCodePoint(char.charCodeAt(0) + 127397)),
|
|
345
|
+
svgFlagB64: yield flagEmoji(getCountryCodeFromLangCode(lang)),
|
|
346
|
+
};
|
|
347
|
+
})));
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
setupEndpoints(server) {
|
|
351
|
+
server.endpoint({
|
|
352
|
+
method: 'GET',
|
|
353
|
+
path: `/plugin/${this.pluginInstanceId}/frontend_messages`,
|
|
354
|
+
noAuth: true,
|
|
355
|
+
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ query }) {
|
|
356
|
+
const lang = query.lang;
|
|
357
|
+
// form map of translations
|
|
358
|
+
const translations = yield this.getCategoryTranslations('frontend', lang);
|
|
359
|
+
return translations;
|
|
360
|
+
})
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
<div class="group hover:bg-gray-100 dark:hover:bg-gray-600 px-2 py-2 flex items-center cursor-pointer" v-for="(index, lang) in checkedLanguages" :key="lang" @click="toggleLanguage(lang)">
|
|
42
42
|
<Checkbox v-model="checkedLanguages[lang]" />
|
|
43
43
|
<span class="flag-icon mr-2"
|
|
44
|
-
:class="`flag-icon-${
|
|
44
|
+
:class="`flag-icon-${props.meta.countryCodes[lang]}`"
|
|
45
45
|
></span>
|
|
46
46
|
<span class="group-hover:text-gray-900 text-gray-600 dark:text-white dark:group-hover:text-gray-200">{{ ISO6391.getName(lang.slice(0,2)) }} ({{ lang }})</span>
|
|
47
47
|
</div>
|
|
@@ -58,7 +58,6 @@
|
|
|
58
58
|
import { computed, onMounted, ref, onUnmounted } from 'vue';
|
|
59
59
|
import { callAdminForthApi } from '@/utils';
|
|
60
60
|
import { useAdminforth } from '@/adminforth';
|
|
61
|
-
import { getCountryCodeFromLangCode } from './langCommon';
|
|
62
61
|
import { getName, overwrite } from 'country-list';
|
|
63
62
|
import ISO6391 from 'iso-639-1';
|
|
64
63
|
import 'flag-icon-css/css/flag-icons.min.css';
|
|
@@ -82,6 +81,7 @@
|
|
|
82
81
|
adminUser: Record<string, any>;
|
|
83
82
|
meta: {
|
|
84
83
|
supportedLanguages: string[];
|
|
84
|
+
countryCodes: Record<string, string>;
|
|
85
85
|
pluginInstanceId: string;
|
|
86
86
|
};
|
|
87
87
|
clearCheckboxes: () => void;
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
<div>
|
|
11
11
|
<span class="mr-1">
|
|
12
12
|
<span class="flag-icon"
|
|
13
|
-
:class="`flag-icon-${
|
|
13
|
+
:class="`flag-icon-${selectedOption.countryCode}`"
|
|
14
14
|
></span>
|
|
15
15
|
</span>
|
|
16
16
|
<span>{{ selectedOption.label }}</span>
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
>
|
|
33
33
|
<span class="mr-1">
|
|
34
34
|
<span class="flag-icon"
|
|
35
|
-
:class="`flag-icon-${
|
|
35
|
+
:class="`flag-icon-${option.countryCode}`"
|
|
36
36
|
></span>
|
|
37
37
|
</span>
|
|
38
38
|
<span>{{ option.label }}</span>
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
import 'flag-icon-css/css/flag-icons.min.css';
|
|
49
49
|
import { IconCaretDownSolid } from '@iconify-prerendered/vue-flowbite';
|
|
50
50
|
|
|
51
|
-
import { setLang,
|
|
51
|
+
import { setLang, getLocalLang, setLocalLang, getLanguageOptions } from './langCommon';
|
|
52
52
|
|
|
53
53
|
import { computed, ref, onMounted, watch } from 'vue';
|
|
54
54
|
import { useI18n } from 'vue-i18n';
|
|
@@ -68,15 +68,7 @@ function doChangeLang(lang) {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
|
|
71
|
-
const options = computed(() =>
|
|
72
|
-
return props.meta.supportedLanguages.map((lang) => {
|
|
73
|
-
const region = String(lang.code).split('-')[1]?.toUpperCase();
|
|
74
|
-
return {
|
|
75
|
-
value: lang.code,
|
|
76
|
-
label: region ? `${lang.name} (${region})` : lang.name,
|
|
77
|
-
};
|
|
78
|
-
});
|
|
79
|
-
});
|
|
71
|
+
const options = computed(() => getLanguageOptions(props.meta.supportedLanguages));
|
|
80
72
|
|
|
81
73
|
const selectedOption = computed(() => {
|
|
82
74
|
const val = options.value.find((option) => option.value === selectedLanguage.value);
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
<template #item="{ option }">
|
|
10
10
|
<span class="mr-1">
|
|
11
11
|
<span class="flag-icon"
|
|
12
|
-
:class="`flag-icon-${
|
|
12
|
+
:class="`flag-icon-${option.countryCode}`"
|
|
13
13
|
></span>
|
|
14
14
|
|
|
15
15
|
</span>
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
<template #selected-item="{option}">
|
|
20
20
|
<span class="mr-1">
|
|
21
21
|
<span class="flag-icon"
|
|
22
|
-
:class="`flag-icon-${
|
|
22
|
+
:class="`flag-icon-${option.countryCode}`"
|
|
23
23
|
></span>
|
|
24
24
|
</span>
|
|
25
25
|
<span>{{ option.label }}</span>
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
<script setup>
|
|
32
32
|
import Select from '@/afcl/Select.vue';
|
|
33
33
|
import 'flag-icon-css/css/flag-icons.min.css';
|
|
34
|
-
import { setLang,
|
|
34
|
+
import { setLang, getLocalLang, getLanguageOptions } from './langCommon';
|
|
35
35
|
import { useCoreStore } from '@/stores/core';
|
|
36
36
|
|
|
37
37
|
import { computed, ref, onMounted, watch } from 'vue';
|
|
@@ -52,15 +52,7 @@ watch(() => selectedLanguage.value, async (newVal) => {
|
|
|
52
52
|
});
|
|
53
53
|
|
|
54
54
|
|
|
55
|
-
const options = computed(() =>
|
|
56
|
-
return props.meta.supportedLanguages.map((lang) => {
|
|
57
|
-
const region = String(lang.code).split('-')[1]?.toUpperCase();
|
|
58
|
-
return {
|
|
59
|
-
value: lang.code,
|
|
60
|
-
label: region ? `${lang.name} (${region})` : lang.name,
|
|
61
|
-
};
|
|
62
|
-
});
|
|
63
|
-
});
|
|
55
|
+
const options = computed(() => getLanguageOptions(props.meta.supportedLanguages));
|
|
64
56
|
|
|
65
57
|
onMounted(() => {
|
|
66
58
|
selectedLanguage.value = getLocalLang(props.meta.supportedLanguages, props.meta.primaryLanguage);
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
</span>
|
|
23
23
|
<div class="flex items-center gap-2 shrink-0">
|
|
24
24
|
<span class="flag-icon"
|
|
25
|
-
:class="`flag-icon-${
|
|
25
|
+
:class="`flag-icon-${task.state?.countryCode}`"
|
|
26
26
|
></span>
|
|
27
27
|
<component
|
|
28
28
|
:is="getCustomComponent({file: '@@/plugins/BackgroundJobsPlugin/StateToIcon.vue'})"
|
|
@@ -61,7 +61,6 @@
|
|
|
61
61
|
import { useI18n } from 'vue-i18n'
|
|
62
62
|
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
|
63
63
|
import websocket from '@/websocket';
|
|
64
|
-
import { getCountryCodeFromLangCode } from './langCommon';
|
|
65
64
|
import { getCustomComponent } from '@/utils';
|
|
66
65
|
import { Button, Spinner } from '@/afcl';
|
|
67
66
|
import { IconAngleLeftOutline, IconAngleRightOutline } from '@iconify-prerendered/vue-flowbite';
|
|
@@ -106,7 +105,7 @@ async function loadPage(targetPage: number) {
|
|
|
106
105
|
try {
|
|
107
106
|
for (let retry = 0; retry < retries; retry++) {
|
|
108
107
|
try {
|
|
109
|
-
const res = await props.getJobTasks(PAGE_SIZE, targetPage * PAGE_SIZE, ['lang', 'error', 'taskName', 'failedToTranslate']);
|
|
108
|
+
const res = await props.getJobTasks(PAGE_SIZE, targetPage * PAGE_SIZE, ['lang', 'countryCode', 'error', 'taskName', 'failedToTranslate']);
|
|
110
109
|
if (res && res.length > 0) {
|
|
111
110
|
translationTasks.value = res;
|
|
112
111
|
page.value = targetPage;
|
|
@@ -65,32 +65,20 @@ export async function setLang({ setLocaleMessage, locale }: any, pluginInstanceI
|
|
|
65
65
|
setLocalLang(langIso);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
// only remap the country code for the languages where language code is different from the country code
|
|
69
|
-
// don't include es: es, fr: fr, etc, only include the ones where language code is different from the country code
|
|
70
|
-
const countryISO31661ByLangISO6391 = {
|
|
71
|
-
en: 'us', // English → United States
|
|
72
|
-
zh: 'cn', // Chinese → China
|
|
73
|
-
hi: 'in', // Hindi → India
|
|
74
|
-
ar: 'sa', // Arabic → Saudi Arabia
|
|
75
|
-
ko: 'kr', // Korean → South Korea
|
|
76
|
-
ja: 'jp', // Japanese → Japan
|
|
77
|
-
uk: 'ua', // Ukrainian → Ukraine
|
|
78
|
-
ur: 'pk', // Urdu → Pakistan
|
|
79
|
-
sr: 'rs', // Serbian → Serbia
|
|
80
|
-
da: 'dk' // Danish → Denmark
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
export function getCountryCodeFromLangCode(langCode) {
|
|
84
|
-
const [primary, region] = String(langCode).split('-');
|
|
85
|
-
if (region && /^[A-Za-z]{2}$/.test(region)) {
|
|
86
|
-
return region.toLowerCase();
|
|
87
|
-
}
|
|
88
|
-
return countryISO31661ByLangISO6391[primary] || primary;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
|
|
92
68
|
const LS_LANG_KEY = `afLanguage`;
|
|
93
69
|
|
|
70
|
+
// options for language selectors, region of BCP-47 tag is added to native language name, e.g. "Português (BR)"
|
|
71
|
+
export function getLanguageOptions(supportedLanguages: { code: string, name: string, countryCode: string }[]) {
|
|
72
|
+
return supportedLanguages.map((lang) => {
|
|
73
|
+
const region = String(lang.code).split('-')[1]?.toUpperCase();
|
|
74
|
+
return {
|
|
75
|
+
value: lang.code,
|
|
76
|
+
countryCode: lang.countryCode,
|
|
77
|
+
label: region ? `${lang.name} (${region})` : lang.name,
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
94
82
|
export function getLocalLang(supportedLanguages: {code}[], primaryLanguage?: SupportedLanguage): string {
|
|
95
83
|
let lsLang = localStorage.getItem(LS_LANG_KEY);
|
|
96
84
|
// if someone screwed up the local storage or we stopped language support, lets check if it is in supported languages
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { AdminForthPlugin } from "adminforth";
|
|
2
1
|
import type { IAdminForth, IHttpServer, AdminForthResourceColumn, AdminForthResource, AdminUser } from "adminforth";
|
|
3
2
|
import type { PluginOptions, SupportedLanguage } from './types.js';
|
|
3
|
+
import { I18nBase } from './base.js';
|
|
4
4
|
interface IFailedTranslation {
|
|
5
5
|
lang: SupportedLanguage;
|
|
6
6
|
en_string: string;
|
|
@@ -11,8 +11,7 @@ interface ICachingAdapter {
|
|
|
11
11
|
set(key: string, value: any): Promise<void>;
|
|
12
12
|
clear(key: string): Promise<void>;
|
|
13
13
|
}
|
|
14
|
-
export default class I18nPlugin extends
|
|
15
|
-
options: PluginOptions;
|
|
14
|
+
export default class I18nPlugin extends I18nBase<PluginOptions> {
|
|
16
15
|
emailField: AdminForthResourceColumn;
|
|
17
16
|
passwordField: AdminForthResourceColumn;
|
|
18
17
|
authResource: AdminForthResource;
|
|
@@ -22,12 +21,14 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
22
21
|
cache: ICachingAdapter;
|
|
23
22
|
primaryKeyFieldName: string;
|
|
24
23
|
menuItemWithBadgeId: string;
|
|
25
|
-
adminforth: IAdminForth;
|
|
26
24
|
externalAppOnly: boolean;
|
|
27
25
|
fullCompleatedFieldValue: string;
|
|
28
|
-
primaryLanguage: SupportedLanguage;
|
|
29
26
|
constructor(options: PluginOptions);
|
|
30
|
-
|
|
27
|
+
completedLangsValue(isTranslated: (lang: SupportedLanguage) => boolean): string;
|
|
28
|
+
computeCompletedFieldValue(record: any): string;
|
|
29
|
+
categoryCacheKey(category: string, lang: string): string;
|
|
30
|
+
messageCacheKey(category: string, lang: string, enString: string): string;
|
|
31
|
+
clearTranslationCache(category: string, lang: string, enString: string): void;
|
|
31
32
|
updateUntranslatedMenuBadge(): Promise<void>;
|
|
32
33
|
modifyResourceConfig(adminforth: IAdminForth, resourceConfig: AdminForthResource): Promise<void>;
|
|
33
34
|
generateAndSaveBunch(prompt: string, strings: {
|
|
@@ -63,21 +64,12 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
63
64
|
processExtractedMessages(adminforth: IAdminForth, filePath: string): Promise<void>;
|
|
64
65
|
tryProcessAndWatch(adminforth: IAdminForth): Promise<void>;
|
|
65
66
|
validateConfigAfterDiscover(adminforth: IAdminForth, resourceConfig: AdminForthResource): void;
|
|
66
|
-
applyPluralization(str: string, number: number, lang: string): string;
|
|
67
|
-
slavicPluralRule(choice: any, choicesLength: any): 3 | 0 | 2 | 1;
|
|
68
|
-
instanceUniqueRepresentation(pluginOptions: any): string;
|
|
69
67
|
getCategoryTranslations(category: string, lang: string): Promise<Record<string, string>>;
|
|
70
|
-
languagesList(): Promise<{
|
|
71
|
-
code: SupportedLanguage;
|
|
72
|
-
nameOnNative: string;
|
|
73
|
-
nameEnglish: string;
|
|
74
|
-
emojiFlag: string;
|
|
75
|
-
svgFlagB64: string;
|
|
76
|
-
}[]>;
|
|
77
68
|
feedCategoryTranslations(messages: {
|
|
78
69
|
en_string: string;
|
|
79
70
|
source: string;
|
|
80
71
|
}[], category: string): Promise<void>;
|
|
81
72
|
setupEndpoints(server: IHttpServer): void;
|
|
82
73
|
}
|
|
83
|
-
export {};
|
|
74
|
+
export { I18nBase, getCountryCodeFromLangCode, getPrimaryLanguageCode, isValidSupportedLanguageTag, requiresSlavicPlurals, SOURCE_LANGUAGE, PLURAL_SEPARATOR, SLAVIC_PLURAL_FORMS, SLAVIC_PLURAL_FORMS_HINT } from './base.js';
|
|
75
|
+
export type { I18nBaseOptions, SupportedLanguage } from './types.js';
|