@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/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-${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/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 AdminForthPlugin {
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
- computeCompletedFieldValue(record: any): Promise<string>;
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';