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