@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 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 48,185 bytes received 305 bytes 96,980.00 bytes/sec
24
- total size is 47,033 speedup is 0.97
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-${getCountryCodeFromLangCode(lang)}`"
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-${getCountryCodeFromLangCode(selectedOption.value)}`"
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-${getCountryCodeFromLangCode(option.value)}`"
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, getCountryCodeFromLangCode, getLocalLang, setLocalLang } from './langCommon';
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-${getCountryCodeFromLangCode(option.value)}`"
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-${getCountryCodeFromLangCode(option.value)}`"
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, getCountryCodeFromLangCode, getLocalLang } from './langCommon';
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-${getCountryCodeFromLangCode(task.state?.lang)}`"
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/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
+ }