@adminforth/i18n 1.3.7 → 1.4.1
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/build.log +2 -2
- package/custom/LanguageEveryPageLoader.vue +4 -2
- package/custom/LanguageInUserMenu.vue +12 -10
- package/custom/LanguageUnderLogin.vue +3 -2
- package/custom/langCommon.ts +11 -2
- package/dist/custom/LanguageEveryPageLoader.vue +4 -2
- package/dist/custom/LanguageInUserMenu.vue +12 -10
- package/dist/custom/LanguageUnderLogin.vue +3 -2
- package/dist/custom/langCommon.ts +11 -2
- package/dist/index.js +48 -22
- package/index.ts +68 -43
- package/package.json +3 -1
- package/types.ts +16 -2
package/build.log
CHANGED
|
@@ -17,5 +17,5 @@ custom/package-lock.json
|
|
|
17
17
|
custom/package.json
|
|
18
18
|
custom/tsconfig.json
|
|
19
19
|
|
|
20
|
-
sent 32,
|
|
21
|
-
total size is
|
|
20
|
+
sent 32,964 bytes received 248 bytes 66,424.00 bytes/sec
|
|
21
|
+
total size is 32,031 speedup is 0.96
|
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
<script setup>
|
|
4
4
|
import { onMounted } from 'vue';
|
|
5
5
|
import { useI18n } from 'vue-i18n';
|
|
6
|
-
import { setLang, getLocalLang } from './langCommon';
|
|
6
|
+
import { setLang, getLocalLang, setLocalLang } from './langCommon';
|
|
7
7
|
|
|
8
8
|
const { setLocaleMessage, locale } = useI18n();
|
|
9
9
|
|
|
10
10
|
const props = defineProps(['meta']);
|
|
11
11
|
|
|
12
12
|
onMounted(() => {
|
|
13
|
-
|
|
13
|
+
const langIso = getLocalLang(props.meta.supportedLanguages, props.meta.primaryLanguage);
|
|
14
|
+
setLocalLang(langIso);
|
|
15
|
+
setLang({ setLocaleMessage, locale }, props.meta.pluginInstanceId, langIso);
|
|
14
16
|
});
|
|
15
17
|
</script>
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="min-w-40">
|
|
3
|
-
<div class="cursor-pointer flex items-center gap-1 block px-4 py-2 text-sm text-black
|
|
3
|
+
<div class="cursor-pointer flex items-center justify-between gap-1 block px-4 py-2 text-sm text-black
|
|
4
4
|
hover:bg-html dark:text-darkSidebarTextHover dark:hover:bg-darkSidebarItemHover dark:hover:text-darkSidebarTextActive
|
|
5
5
|
w-full select-none "
|
|
6
6
|
:class="{ 'bg-black bg-opacity-10 ': showDropdown }"
|
|
7
7
|
@click="showDropdown = !showDropdown"
|
|
8
8
|
>
|
|
9
|
-
<
|
|
10
|
-
<span class="
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
<div>
|
|
10
|
+
<span class="mr-1">
|
|
11
|
+
<span class="flag-icon"
|
|
12
|
+
:class="`flag-icon-${getCountryCodeFromLangCode(selectedOption.value)}`"
|
|
13
|
+
></span>
|
|
14
|
+
</span>
|
|
15
|
+
<span>{{ selectedOption.label }}</span>
|
|
16
|
+
</div>
|
|
16
17
|
<IconCaretDownSolid class="h-5 w-5 text-lightPrimary dark:text-gray-400 opacity-50 transition duration-150 ease-in"
|
|
17
18
|
:class="{ 'transform rotate-180': showDropdown }"
|
|
18
19
|
/>
|
|
@@ -70,9 +71,10 @@ function doChangeLang(lang) {
|
|
|
70
71
|
|
|
71
72
|
const options = computed(() => {
|
|
72
73
|
return props.meta.supportedLanguages.map((lang) => {
|
|
74
|
+
const region = String(lang.code).split('-')[1]?.toUpperCase();
|
|
73
75
|
return {
|
|
74
76
|
value: lang.code,
|
|
75
|
-
label: lang.name,
|
|
77
|
+
label: region ? `${lang.name} (${region})` : lang.name,
|
|
76
78
|
};
|
|
77
79
|
});
|
|
78
80
|
});
|
|
@@ -87,6 +89,6 @@ const selectedOption = computed(() => {
|
|
|
87
89
|
|
|
88
90
|
|
|
89
91
|
onMounted(() => {
|
|
90
|
-
selectedLanguage.value = getLocalLang(props.meta.supportedLanguages);
|
|
92
|
+
selectedLanguage.value = getLocalLang(props.meta.supportedLanguages, props.meta.primaryLanguage);
|
|
91
93
|
});
|
|
92
94
|
</script>
|
|
@@ -53,14 +53,15 @@ watch(() => selectedLanguage.value, async (newVal) => {
|
|
|
53
53
|
|
|
54
54
|
const options = computed(() => {
|
|
55
55
|
return props.meta.supportedLanguages.map((lang) => {
|
|
56
|
+
const region = String(lang.code).split('-')[1]?.toUpperCase();
|
|
56
57
|
return {
|
|
57
58
|
value: lang.code,
|
|
58
|
-
label: lang.name,
|
|
59
|
+
label: region ? `${lang.name} (${region})` : lang.name,
|
|
59
60
|
};
|
|
60
61
|
});
|
|
61
62
|
});
|
|
62
63
|
|
|
63
64
|
onMounted(() => {
|
|
64
|
-
selectedLanguage.value = getLocalLang(props.meta.supportedLanguages);
|
|
65
|
+
selectedLanguage.value = getLocalLang(props.meta.supportedLanguages, props.meta.primaryLanguage);
|
|
65
66
|
});
|
|
66
67
|
</script>
|
package/custom/langCommon.ts
CHANGED
|
@@ -4,6 +4,7 @@ import dayjsLocales from './dayjsLocales';
|
|
|
4
4
|
import datepickerLocales from './datepickerLocales';
|
|
5
5
|
import dayjs from 'dayjs';
|
|
6
6
|
import Datepicker from "flowbite-datepicker/Datepicker";
|
|
7
|
+
import type { SupportedLanguage } from '../types';
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
const messagesCache: Record<
|
|
@@ -78,13 +79,17 @@ const countryISO31661ByLangISO6391 = {
|
|
|
78
79
|
};
|
|
79
80
|
|
|
80
81
|
export function getCountryCodeFromLangCode(langCode) {
|
|
81
|
-
|
|
82
|
+
const [primary, region] = String(langCode).split('-');
|
|
83
|
+
if (region && /^[A-Za-z]{2}$/.test(region)) {
|
|
84
|
+
return region.toLowerCase();
|
|
85
|
+
}
|
|
86
|
+
return countryISO31661ByLangISO6391[primary] || primary;
|
|
82
87
|
}
|
|
83
88
|
|
|
84
89
|
|
|
85
90
|
const LS_LANG_KEY = `afLanguage`;
|
|
86
91
|
|
|
87
|
-
export function getLocalLang(supportedLanguages: {code}[]): string {
|
|
92
|
+
export function getLocalLang(supportedLanguages: {code}[], primaryLanguage?: SupportedLanguage): string {
|
|
88
93
|
let lsLang = localStorage.getItem(LS_LANG_KEY);
|
|
89
94
|
// if someone screwed up the local storage or we stopped language support, lets check if it is in supported languages
|
|
90
95
|
if (lsLang && !supportedLanguages.find((l) => l.code == lsLang)) {
|
|
@@ -99,6 +104,10 @@ export function getLocalLang(supportedLanguages: {code}[]): string {
|
|
|
99
104
|
if (foundLang) {
|
|
100
105
|
return foundLang.code;
|
|
101
106
|
}
|
|
107
|
+
if (primaryLanguage && supportedLanguages.find((l) => l.code == primaryLanguage)) {
|
|
108
|
+
return primaryLanguage;
|
|
109
|
+
}
|
|
110
|
+
|
|
102
111
|
return supportedLanguages[0].code;
|
|
103
112
|
}
|
|
104
113
|
|
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
<script setup>
|
|
4
4
|
import { onMounted } from 'vue';
|
|
5
5
|
import { useI18n } from 'vue-i18n';
|
|
6
|
-
import { setLang, getLocalLang } from './langCommon';
|
|
6
|
+
import { setLang, getLocalLang, setLocalLang } from './langCommon';
|
|
7
7
|
|
|
8
8
|
const { setLocaleMessage, locale } = useI18n();
|
|
9
9
|
|
|
10
10
|
const props = defineProps(['meta']);
|
|
11
11
|
|
|
12
12
|
onMounted(() => {
|
|
13
|
-
|
|
13
|
+
const langIso = getLocalLang(props.meta.supportedLanguages, props.meta.primaryLanguage);
|
|
14
|
+
setLocalLang(langIso);
|
|
15
|
+
setLang({ setLocaleMessage, locale }, props.meta.pluginInstanceId, langIso);
|
|
14
16
|
});
|
|
15
17
|
</script>
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="min-w-40">
|
|
3
|
-
<div class="cursor-pointer flex items-center gap-1 block px-4 py-2 text-sm text-black
|
|
3
|
+
<div class="cursor-pointer flex items-center justify-between gap-1 block px-4 py-2 text-sm text-black
|
|
4
4
|
hover:bg-html dark:text-darkSidebarTextHover dark:hover:bg-darkSidebarItemHover dark:hover:text-darkSidebarTextActive
|
|
5
5
|
w-full select-none "
|
|
6
6
|
:class="{ 'bg-black bg-opacity-10 ': showDropdown }"
|
|
7
7
|
@click="showDropdown = !showDropdown"
|
|
8
8
|
>
|
|
9
|
-
<
|
|
10
|
-
<span class="
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
<div>
|
|
10
|
+
<span class="mr-1">
|
|
11
|
+
<span class="flag-icon"
|
|
12
|
+
:class="`flag-icon-${getCountryCodeFromLangCode(selectedOption.value)}`"
|
|
13
|
+
></span>
|
|
14
|
+
</span>
|
|
15
|
+
<span>{{ selectedOption.label }}</span>
|
|
16
|
+
</div>
|
|
16
17
|
<IconCaretDownSolid class="h-5 w-5 text-lightPrimary dark:text-gray-400 opacity-50 transition duration-150 ease-in"
|
|
17
18
|
:class="{ 'transform rotate-180': showDropdown }"
|
|
18
19
|
/>
|
|
@@ -70,9 +71,10 @@ function doChangeLang(lang) {
|
|
|
70
71
|
|
|
71
72
|
const options = computed(() => {
|
|
72
73
|
return props.meta.supportedLanguages.map((lang) => {
|
|
74
|
+
const region = String(lang.code).split('-')[1]?.toUpperCase();
|
|
73
75
|
return {
|
|
74
76
|
value: lang.code,
|
|
75
|
-
label: lang.name,
|
|
77
|
+
label: region ? `${lang.name} (${region})` : lang.name,
|
|
76
78
|
};
|
|
77
79
|
});
|
|
78
80
|
});
|
|
@@ -87,6 +89,6 @@ const selectedOption = computed(() => {
|
|
|
87
89
|
|
|
88
90
|
|
|
89
91
|
onMounted(() => {
|
|
90
|
-
selectedLanguage.value = getLocalLang(props.meta.supportedLanguages);
|
|
92
|
+
selectedLanguage.value = getLocalLang(props.meta.supportedLanguages, props.meta.primaryLanguage);
|
|
91
93
|
});
|
|
92
94
|
</script>
|
|
@@ -53,14 +53,15 @@ watch(() => selectedLanguage.value, async (newVal) => {
|
|
|
53
53
|
|
|
54
54
|
const options = computed(() => {
|
|
55
55
|
return props.meta.supportedLanguages.map((lang) => {
|
|
56
|
+
const region = String(lang.code).split('-')[1]?.toUpperCase();
|
|
56
57
|
return {
|
|
57
58
|
value: lang.code,
|
|
58
|
-
label: lang.name,
|
|
59
|
+
label: region ? `${lang.name} (${region})` : lang.name,
|
|
59
60
|
};
|
|
60
61
|
});
|
|
61
62
|
});
|
|
62
63
|
|
|
63
64
|
onMounted(() => {
|
|
64
|
-
selectedLanguage.value = getLocalLang(props.meta.supportedLanguages);
|
|
65
|
+
selectedLanguage.value = getLocalLang(props.meta.supportedLanguages, props.meta.primaryLanguage);
|
|
65
66
|
});
|
|
66
67
|
</script>
|
|
@@ -4,6 +4,7 @@ import dayjsLocales from './dayjsLocales';
|
|
|
4
4
|
import datepickerLocales from './datepickerLocales';
|
|
5
5
|
import dayjs from 'dayjs';
|
|
6
6
|
import Datepicker from "flowbite-datepicker/Datepicker";
|
|
7
|
+
import type { SupportedLanguage } from '../types';
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
const messagesCache: Record<
|
|
@@ -78,13 +79,17 @@ const countryISO31661ByLangISO6391 = {
|
|
|
78
79
|
};
|
|
79
80
|
|
|
80
81
|
export function getCountryCodeFromLangCode(langCode) {
|
|
81
|
-
|
|
82
|
+
const [primary, region] = String(langCode).split('-');
|
|
83
|
+
if (region && /^[A-Za-z]{2}$/.test(region)) {
|
|
84
|
+
return region.toLowerCase();
|
|
85
|
+
}
|
|
86
|
+
return countryISO31661ByLangISO6391[primary] || primary;
|
|
82
87
|
}
|
|
83
88
|
|
|
84
89
|
|
|
85
90
|
const LS_LANG_KEY = `afLanguage`;
|
|
86
91
|
|
|
87
|
-
export function getLocalLang(supportedLanguages: {code}[]): string {
|
|
92
|
+
export function getLocalLang(supportedLanguages: {code}[], primaryLanguage?: SupportedLanguage): string {
|
|
88
93
|
let lsLang = localStorage.getItem(LS_LANG_KEY);
|
|
89
94
|
// if someone screwed up the local storage or we stopped language support, lets check if it is in supported languages
|
|
90
95
|
if (lsLang && !supportedLanguages.find((l) => l.code == lsLang)) {
|
|
@@ -99,6 +104,10 @@ export function getLocalLang(supportedLanguages: {code}[]): string {
|
|
|
99
104
|
if (foundLang) {
|
|
100
105
|
return foundLang.code;
|
|
101
106
|
}
|
|
107
|
+
if (primaryLanguage && supportedLanguages.find((l) => l.code == primaryLanguage)) {
|
|
108
|
+
return primaryLanguage;
|
|
109
|
+
}
|
|
110
|
+
|
|
102
111
|
return supportedLanguages[0].code;
|
|
103
112
|
}
|
|
104
113
|
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
9
9
|
};
|
|
10
10
|
import { AdminForthPlugin, Filters, suggestIfTypo, AdminForthDataTypes, RAMLock } from "adminforth";
|
|
11
11
|
import iso6391 from 'iso-639-1';
|
|
12
|
+
import { iso31661Alpha2ToAlpha3 } from 'iso-3166';
|
|
12
13
|
import path from 'path';
|
|
13
14
|
import fs from 'fs-extra';
|
|
14
15
|
import chokidar from 'chokidar';
|
|
@@ -37,9 +38,27 @@ const countryISO31661ByLangISO6391 = {
|
|
|
37
38
|
uk: 'ua', // Ukrainian → Ukraine
|
|
38
39
|
ur: 'pk', // Urdu → Pakistan
|
|
39
40
|
};
|
|
40
|
-
function getCountryCodeFromLangCode(
|
|
41
|
+
function getCountryCodeFromLangCode(lang) {
|
|
42
|
+
const [langCode, region] = String(lang).split('-');
|
|
43
|
+
if (region && /^[A-Z]{2}$/.test(region)) {
|
|
44
|
+
return region.toLowerCase();
|
|
45
|
+
}
|
|
41
46
|
return countryISO31661ByLangISO6391[langCode] || langCode;
|
|
42
47
|
}
|
|
48
|
+
function getPrimaryLanguageCode(langCode) {
|
|
49
|
+
return String(langCode).split('-')[0];
|
|
50
|
+
}
|
|
51
|
+
function isValidSupportedLanguageTag(langCode) {
|
|
52
|
+
const [primary, region] = String(langCode).split('-');
|
|
53
|
+
if (!iso6391.validate(primary)) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
if (!region) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
const regionUpper = region.toUpperCase();
|
|
60
|
+
return /^[A-Z]{2}$/.test(regionUpper) && (regionUpper in iso31661Alpha2ToAlpha3);
|
|
61
|
+
}
|
|
43
62
|
class CachingAdapterMemory {
|
|
44
63
|
constructor() {
|
|
45
64
|
this.cache = {};
|
|
@@ -84,6 +103,7 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
84
103
|
this.options = options;
|
|
85
104
|
this.cache = new CachingAdapterMemory();
|
|
86
105
|
this.trFieldNames = {};
|
|
106
|
+
this.primaryLanguage = options.primaryLanguage || 'en';
|
|
87
107
|
}
|
|
88
108
|
computeCompletedFieldValue(record) {
|
|
89
109
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -119,10 +139,10 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
119
139
|
return __awaiter(this, void 0, void 0, function* () {
|
|
120
140
|
var _a, _b;
|
|
121
141
|
_super.modifyResourceConfig.call(this, adminforth, resourceConfig);
|
|
122
|
-
//
|
|
142
|
+
// validate each supported language: ISO 639-1 or BCP-47 with region (e.g., en-GB)
|
|
123
143
|
this.options.supportedLanguages.forEach((lang) => {
|
|
124
|
-
if (!
|
|
125
|
-
throw new Error(`Invalid language code ${lang}
|
|
144
|
+
if (!isValidSupportedLanguageTag(lang)) {
|
|
145
|
+
throw new Error(`Invalid language code ${lang}. Use ISO 639-1 (e.g., 'en') or BCP-47 with region (e.g., 'en-GB').`);
|
|
126
146
|
}
|
|
127
147
|
});
|
|
128
148
|
this.externalAppOnly = this.options.externalAppOnly === true;
|
|
@@ -239,10 +259,11 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
239
259
|
const compMeta = {
|
|
240
260
|
brandSlug: adminforth.config.customization.brandNameSlug,
|
|
241
261
|
pluginInstanceId: this.pluginInstanceId,
|
|
262
|
+
primaryLanguage: this.primaryLanguage,
|
|
242
263
|
supportedLanguages: this.options.supportedLanguages.map(lang => ({
|
|
243
264
|
code: lang,
|
|
244
265
|
// lang name on on language native name
|
|
245
|
-
name: iso6391.getNativeName(lang),
|
|
266
|
+
name: iso6391.getNativeName(getPrimaryLanguageCode(lang)),
|
|
246
267
|
}))
|
|
247
268
|
};
|
|
248
269
|
// add underLogin component
|
|
@@ -424,6 +445,7 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
424
445
|
}
|
|
425
446
|
translateToLang(langIsoCode_1, strings_1) {
|
|
426
447
|
return __awaiter(this, arguments, void 0, function* (langIsoCode, strings, plurals = false, translations, updateStrings = {}) {
|
|
448
|
+
var _a;
|
|
427
449
|
const maxKeysInOneReq = 10;
|
|
428
450
|
if (strings.length === 0) {
|
|
429
451
|
return [];
|
|
@@ -439,21 +461,23 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
439
461
|
return totalTranslated;
|
|
440
462
|
}
|
|
441
463
|
const lang = langIsoCode;
|
|
442
|
-
const
|
|
443
|
-
const
|
|
464
|
+
const primaryLang = getPrimaryLanguageCode(lang);
|
|
465
|
+
const langName = iso6391.getName(primaryLang);
|
|
466
|
+
const requestSlavicPlurals = Object.keys(SLAVIC_PLURAL_EXAMPLES).includes(primaryLang) && plurals;
|
|
467
|
+
const region = ((_a = String(lang).split('-')[1]) === null || _a === void 0 ? void 0 : _a.toUpperCase()) || '';
|
|
444
468
|
const prompt = `
|
|
445
|
-
I need to translate strings in JSON to ${langName} language (ISO 639-1 code ${lang}) from English for my web app.
|
|
446
|
-
${
|
|
447
|
-
|
|
469
|
+
I need to translate strings in JSON to ${langName} language (ISO 639-1 code ${lang}) from English for my web app.
|
|
470
|
+
${region ? `Use the regional conventions for ${lang} (region ${region}), including spelling, punctuation, and formatting.` : ''}
|
|
471
|
+
${requestSlavicPlurals ? `You should provide 4 slavic forms (in format "zero count | singular count | 2-4 | 5+") e.g. "apple | apples" should become "${SLAVIC_PLURAL_EXAMPLES[lang]}"` : ''}
|
|
472
|
+
Keep keys, as is, write translation into values! If keys have variables (in curly brackets), then translated strings should have them as well (variables itself should not be translated). Here are the strings:
|
|
448
473
|
|
|
449
|
-
\`\`\`json
|
|
450
|
-
${JSON.stringify(strings.reduce((acc, s) => {
|
|
474
|
+
\`\`\`json
|
|
475
|
+
${JSON.stringify(strings.reduce((acc, s) => {
|
|
451
476
|
acc[s.en_string] = '';
|
|
452
477
|
return acc;
|
|
453
478
|
}, {}), null, 2)}
|
|
454
|
-
\`\`\`
|
|
455
|
-
`;
|
|
456
|
-
process.env.HEAVY_DEBUG && console.log(`🪲🔪LLM prompt >> ${prompt.length}, <<${prompt} :\n\n`, JSON.stringify(prompt));
|
|
479
|
+
\`\`\`
|
|
480
|
+
`;
|
|
457
481
|
// call OpenAI
|
|
458
482
|
const resp = yield this.options.completeAdapter.complete(prompt, [], prompt.length * 2);
|
|
459
483
|
process.env.HEAVY_DEBUG && console.log(`🪲🔪LLM resp >> ${prompt.length}, <<${resp.content.length} :\n\n`, JSON.stringify(resp));
|
|
@@ -657,11 +681,12 @@ ${JSON.stringify(strings.reduce((acc, s) => {
|
|
|
657
681
|
throw new Error(`Category 'frontend' is reserved for frontend messages, use any other category for backend messages`);
|
|
658
682
|
}
|
|
659
683
|
// console.log('🪲tr', msg, category, lang);
|
|
660
|
-
// if lang is not supported ,
|
|
684
|
+
// if lang is not supported, fallback to primaryLanguage, then to english
|
|
661
685
|
if (!this.options.supportedLanguages.includes(lang)) {
|
|
662
|
-
lang =
|
|
663
|
-
|
|
664
|
-
|
|
686
|
+
lang = this.primaryLanguage; // fallback to primary language first
|
|
687
|
+
if (!this.options.supportedLanguages.includes(lang)) {
|
|
688
|
+
lang = 'en'; // final fallback to english
|
|
689
|
+
}
|
|
665
690
|
}
|
|
666
691
|
let result;
|
|
667
692
|
// try to get translation from cache
|
|
@@ -753,7 +778,8 @@ ${JSON.stringify(strings.reduce((acc, s) => {
|
|
|
753
778
|
const translations = {};
|
|
754
779
|
const allTranslations = yield resource.list([Filters.EQ(this.options.categoryFieldName, category)]);
|
|
755
780
|
for (const tr of allTranslations) {
|
|
756
|
-
|
|
781
|
+
const translatedValue = tr[this.trFieldNames[lang]];
|
|
782
|
+
translations[tr[this.enFieldName]] = translatedValue || tr[this.enFieldName];
|
|
757
783
|
}
|
|
758
784
|
yield this.cache.set(cacheKey, translations);
|
|
759
785
|
return translations;
|
|
@@ -764,8 +790,8 @@ ${JSON.stringify(strings.reduce((acc, s) => {
|
|
|
764
790
|
return this.options.supportedLanguages.map((lang) => {
|
|
765
791
|
return {
|
|
766
792
|
code: lang,
|
|
767
|
-
nameOnNative: iso6391.getNativeName(lang),
|
|
768
|
-
nameEnglish: iso6391.getName(lang),
|
|
793
|
+
nameOnNative: iso6391.getNativeName(getPrimaryLanguageCode(lang)),
|
|
794
|
+
nameEnglish: iso6391.getName(getPrimaryLanguageCode(lang)),
|
|
769
795
|
emojiFlag: getCountryCodeFromLangCode(lang).toUpperCase().replace(/./g, char => String.fromCodePoint(char.charCodeAt(0) + 127397)),
|
|
770
796
|
};
|
|
771
797
|
});
|
package/index.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import AdminForth, { AdminForthPlugin, Filters, suggestIfTypo, AdminForthDataTypes, RAMLock } from "adminforth";
|
|
2
2
|
import type { IAdminForth, IHttpServer, AdminForthComponentDeclaration, AdminForthResourceColumn, AdminForthResource, BeforeLoginConfirmationFunction, AdminForthConfigMenuItem } from "adminforth";
|
|
3
|
-
import type { PluginOptions } from './types.js';
|
|
4
|
-
import iso6391
|
|
3
|
+
import type { PluginOptions, SupportedLanguage } from './types.js';
|
|
4
|
+
import iso6391 from 'iso-639-1';
|
|
5
|
+
import { iso31661Alpha2ToAlpha3 } from 'iso-3166';
|
|
5
6
|
import path from 'path';
|
|
6
7
|
import fs from 'fs-extra';
|
|
7
8
|
import chokidar from 'chokidar';
|
|
@@ -35,10 +36,30 @@ const countryISO31661ByLangISO6391 = {
|
|
|
35
36
|
ur: 'pk', // Urdu → Pakistan
|
|
36
37
|
};
|
|
37
38
|
|
|
38
|
-
function getCountryCodeFromLangCode(
|
|
39
|
+
function getCountryCodeFromLangCode(lang: SupportedLanguage) {
|
|
40
|
+
const [langCode, region] = String(lang).split('-');
|
|
41
|
+
if (region && /^[A-Z]{2}$/.test(region)) {
|
|
42
|
+
return region.toLowerCase();
|
|
43
|
+
}
|
|
39
44
|
return countryISO31661ByLangISO6391[langCode] || langCode;
|
|
40
45
|
}
|
|
41
46
|
|
|
47
|
+
function getPrimaryLanguageCode(langCode: SupportedLanguage): string {
|
|
48
|
+
return String(langCode).split('-')[0];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isValidSupportedLanguageTag(langCode: SupportedLanguage): boolean {
|
|
52
|
+
const [primary, region] = String(langCode).split('-');
|
|
53
|
+
if (!iso6391.validate(primary as any)) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
if (!region) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
const regionUpper = region.toUpperCase();
|
|
60
|
+
return /^[A-Z]{2}$/.test(regionUpper) && (regionUpper in iso31661Alpha2ToAlpha3);
|
|
61
|
+
}
|
|
62
|
+
|
|
42
63
|
|
|
43
64
|
interface ICachingAdapter {
|
|
44
65
|
get(key: string): Promise<any>;
|
|
@@ -84,7 +105,7 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
84
105
|
passwordField: AdminForthResourceColumn;
|
|
85
106
|
authResource: AdminForthResource;
|
|
86
107
|
emailConfirmedField?: AdminForthResourceColumn;
|
|
87
|
-
trFieldNames: Partial<Record<
|
|
108
|
+
trFieldNames: Partial<Record<SupportedLanguage, string>>;
|
|
88
109
|
enFieldName: string;
|
|
89
110
|
cache: ICachingAdapter;
|
|
90
111
|
primaryKeyFieldName: string;
|
|
@@ -96,16 +117,18 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
96
117
|
|
|
97
118
|
// sorted by name list of all supported languages, without en e.g. 'al|ro|uk'
|
|
98
119
|
fullCompleatedFieldValue: string;
|
|
120
|
+
primaryLanguage: SupportedLanguage;
|
|
99
121
|
|
|
100
122
|
constructor(options: PluginOptions) {
|
|
101
123
|
super(options, import.meta.url);
|
|
102
124
|
this.options = options;
|
|
103
125
|
this.cache = new CachingAdapterMemory();
|
|
104
126
|
this.trFieldNames = {};
|
|
127
|
+
this.primaryLanguage = options.primaryLanguage || 'en';
|
|
105
128
|
}
|
|
106
129
|
|
|
107
130
|
async computeCompletedFieldValue(record: any) {
|
|
108
|
-
return this.options.supportedLanguages.reduce((acc: string, lang:
|
|
131
|
+
return this.options.supportedLanguages.reduce((acc: string, lang: SupportedLanguage): string => {
|
|
109
132
|
if (lang === 'en') {
|
|
110
133
|
return acc;
|
|
111
134
|
}
|
|
@@ -134,10 +157,10 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
134
157
|
async modifyResourceConfig(adminforth: IAdminForth, resourceConfig: AdminForthResource) {
|
|
135
158
|
super.modifyResourceConfig(adminforth, resourceConfig);
|
|
136
159
|
|
|
137
|
-
//
|
|
160
|
+
// validate each supported language: ISO 639-1 or BCP-47 with region (e.g., en-GB)
|
|
138
161
|
this.options.supportedLanguages.forEach((lang) => {
|
|
139
|
-
if (!
|
|
140
|
-
throw new Error(`Invalid language code ${lang}
|
|
162
|
+
if (!isValidSupportedLanguageTag(lang)) {
|
|
163
|
+
throw new Error(`Invalid language code ${lang}. Use ISO 639-1 (e.g., 'en') or BCP-47 with region (e.g., 'en-GB').`);
|
|
141
164
|
}
|
|
142
165
|
});
|
|
143
166
|
|
|
@@ -191,7 +214,7 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
191
214
|
|
|
192
215
|
this.enFieldName = this.trFieldNames['en'] || 'en_string';
|
|
193
216
|
|
|
194
|
-
this.fullCompleatedFieldValue = this.options.supportedLanguages.reduce((acc: string, lang:
|
|
217
|
+
this.fullCompleatedFieldValue = this.options.supportedLanguages.reduce((acc: string, lang: SupportedLanguage) => {
|
|
195
218
|
if (lang === 'en') {
|
|
196
219
|
return acc;
|
|
197
220
|
}
|
|
@@ -269,11 +292,12 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
269
292
|
const compMeta = {
|
|
270
293
|
brandSlug: adminforth.config.customization.brandNameSlug,
|
|
271
294
|
pluginInstanceId: this.pluginInstanceId,
|
|
295
|
+
primaryLanguage: this.primaryLanguage,
|
|
272
296
|
supportedLanguages: this.options.supportedLanguages.map(lang => (
|
|
273
297
|
{
|
|
274
298
|
code: lang,
|
|
275
299
|
// lang name on on language native name
|
|
276
|
-
name: iso6391.getNativeName(lang),
|
|
300
|
+
name: iso6391.getNativeName(getPrimaryLanguageCode(lang)),
|
|
277
301
|
}
|
|
278
302
|
))
|
|
279
303
|
};
|
|
@@ -317,7 +341,7 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
317
341
|
resourceConfig.hooks.edit.afterSave.push(async ({ updates, oldRecord }: { updates: any, oldRecord?: any }): Promise<{ ok: boolean, error?: string }> => {
|
|
318
342
|
if (oldRecord) {
|
|
319
343
|
// find lang which changed
|
|
320
|
-
let langsChanged:
|
|
344
|
+
let langsChanged: SupportedLanguage[] = [];
|
|
321
345
|
for (const lang of this.options.supportedLanguages) {
|
|
322
346
|
if (lang === 'en') {
|
|
323
347
|
continue;
|
|
@@ -473,7 +497,7 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
473
497
|
}
|
|
474
498
|
|
|
475
499
|
async translateToLang (
|
|
476
|
-
langIsoCode:
|
|
500
|
+
langIsoCode: SupportedLanguage,
|
|
477
501
|
strings: { en_string: string, category: string }[],
|
|
478
502
|
plurals=false,
|
|
479
503
|
translations: any[],
|
|
@@ -495,25 +519,25 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
495
519
|
return totalTranslated;
|
|
496
520
|
}
|
|
497
521
|
const lang = langIsoCode;
|
|
498
|
-
const
|
|
499
|
-
const
|
|
500
|
-
|
|
522
|
+
const primaryLang = getPrimaryLanguageCode(lang);
|
|
523
|
+
const langName = iso6391.getName(primaryLang);
|
|
524
|
+
const requestSlavicPlurals = Object.keys(SLAVIC_PLURAL_EXAMPLES).includes(primaryLang) && plurals;
|
|
525
|
+
const region = String(lang).split('-')[1]?.toUpperCase() || '';
|
|
501
526
|
const prompt = `
|
|
502
|
-
I need to translate strings in JSON to ${langName} language (ISO 639-1 code ${lang}) from English for my web app.
|
|
503
|
-
${
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
process.env.HEAVY_DEBUG && console.log(`🪲🔪LLM prompt >> ${prompt.length}, <<${prompt} :\n\n`, JSON.stringify(prompt));
|
|
527
|
+
I need to translate strings in JSON to ${langName} language (ISO 639-1 code ${lang}) from English for my web app.
|
|
528
|
+
${region ? `Use the regional conventions for ${lang} (region ${region}), including spelling, punctuation, and formatting.` : ''}
|
|
529
|
+
${requestSlavicPlurals ? `You should provide 4 slavic forms (in format "zero count | singular count | 2-4 | 5+") e.g. "apple | apples" should become "${SLAVIC_PLURAL_EXAMPLES[lang]}"` : ''}
|
|
530
|
+
Keep keys, as is, write translation into values! If keys have variables (in curly brackets), then translated strings should have them as well (variables itself should not be translated). Here are the strings:
|
|
531
|
+
|
|
532
|
+
\`\`\`json
|
|
533
|
+
${
|
|
534
|
+
JSON.stringify(strings.reduce((acc: object, s: { en_string: string }): object => {
|
|
535
|
+
acc[s.en_string] = '';
|
|
536
|
+
return acc;
|
|
537
|
+
}, {}), null, 2)
|
|
538
|
+
}
|
|
539
|
+
\`\`\`
|
|
540
|
+
`;
|
|
517
541
|
|
|
518
542
|
// call OpenAI
|
|
519
543
|
const resp = await this.options.completeAdapter.complete(
|
|
@@ -584,7 +608,7 @@ JSON.stringify(strings.reduce((acc: object, s: { en_string: string }): object =>
|
|
|
584
608
|
|
|
585
609
|
const needToTranslateByLang : Partial<
|
|
586
610
|
Record<
|
|
587
|
-
|
|
611
|
+
SupportedLanguage,
|
|
588
612
|
{
|
|
589
613
|
en_string: string;
|
|
590
614
|
category: string;
|
|
@@ -627,7 +651,7 @@ JSON.stringify(strings.reduce((acc: object, s: { en_string: string }): object =>
|
|
|
627
651
|
|
|
628
652
|
await Promise.all(
|
|
629
653
|
Object.entries(needToTranslateByLang).map(
|
|
630
|
-
async ([lang, strings]: [
|
|
654
|
+
async ([lang, strings]: [SupportedLanguage, { en_string: string, category: string }[]]) => {
|
|
631
655
|
// first translate without plurals
|
|
632
656
|
const stringsWithoutPlurals = strings.filter(s => !s.en_string.includes('|'));
|
|
633
657
|
const noPluralKeys = await this.translateToLang(lang, stringsWithoutPlurals, false, translations, updateStrings);
|
|
@@ -781,13 +805,13 @@ JSON.stringify(strings.reduce((acc: object, s: { en_string: string }): object =>
|
|
|
781
805
|
}
|
|
782
806
|
// console.log('🪲tr', msg, category, lang);
|
|
783
807
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
// throw new Error(`Language ${lang} is not entered to be supported by requested by browser in request headers accept-language`);
|
|
808
|
+
// if lang is not supported, fallback to primaryLanguage, then to english
|
|
809
|
+
if (!this.options.supportedLanguages.includes(lang as SupportedLanguage)) {
|
|
810
|
+
lang = this.primaryLanguage; // fallback to primary language first
|
|
811
|
+
if (!this.options.supportedLanguages.includes(lang as SupportedLanguage)) {
|
|
812
|
+
lang = 'en'; // final fallback to english
|
|
790
813
|
}
|
|
814
|
+
}
|
|
791
815
|
|
|
792
816
|
let result;
|
|
793
817
|
// try to get translation from cache
|
|
@@ -885,23 +909,24 @@ JSON.stringify(strings.reduce((acc: object, s: { en_string: string }): object =>
|
|
|
885
909
|
const translations = {};
|
|
886
910
|
const allTranslations = await resource.list([Filters.EQ(this.options.categoryFieldName, category)]);
|
|
887
911
|
for (const tr of allTranslations) {
|
|
888
|
-
|
|
912
|
+
const translatedValue = tr[this.trFieldNames[lang]];
|
|
913
|
+
translations[tr[this.enFieldName]] = translatedValue || tr[this.enFieldName];
|
|
889
914
|
}
|
|
890
915
|
await this.cache.set(cacheKey, translations);
|
|
891
916
|
return translations;
|
|
892
917
|
}
|
|
893
918
|
|
|
894
919
|
async languagesList(): Promise<{
|
|
895
|
-
code:
|
|
920
|
+
code: SupportedLanguage;
|
|
896
921
|
nameOnNative: string;
|
|
897
922
|
nameEnglish: string;
|
|
898
923
|
emojiFlag: string;
|
|
899
924
|
}[]> {
|
|
900
925
|
return this.options.supportedLanguages.map((lang) => {
|
|
901
926
|
return {
|
|
902
|
-
code: lang
|
|
903
|
-
nameOnNative: iso6391.getNativeName(lang),
|
|
904
|
-
nameEnglish: iso6391.getName(lang),
|
|
927
|
+
code: lang,
|
|
928
|
+
nameOnNative: iso6391.getNativeName(getPrimaryLanguageCode(lang)),
|
|
929
|
+
nameEnglish: iso6391.getName(getPrimaryLanguageCode(lang)),
|
|
905
930
|
emojiFlag: getCountryCodeFromLangCode(lang).toUpperCase().replace(/./g, char => String.fromCodePoint(char.charCodeAt(0) + 127397)),
|
|
906
931
|
};
|
|
907
932
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adminforth/i18n",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
"@aws-sdk/client-ses": "^3.654.0",
|
|
29
29
|
"@sapphire/async-queue": "^1.5.5",
|
|
30
30
|
"chokidar": "^4.0.1",
|
|
31
|
+
"fs-extra": "^11.3.2",
|
|
32
|
+
"iso-3166": "^4.3.0",
|
|
31
33
|
"iso-639-1": "^3.1.3"
|
|
32
34
|
},
|
|
33
35
|
"devDependencies": {
|
package/types.ts
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
import { CompletionAdapter, EmailAdapter } from 'adminforth';
|
|
2
2
|
import type { LanguageCode } from 'iso-639-1';
|
|
3
|
+
import { iso31661Alpha2ToAlpha3 } from 'iso-3166';
|
|
3
4
|
|
|
4
5
|
|
|
6
|
+
// BCP-47 support for types only: primary subtag is ISO 639-1, optional region
|
|
7
|
+
type Alpha2Code = keyof typeof iso31661Alpha2ToAlpha3;
|
|
8
|
+
type Bcp47LanguageTag = `${LanguageCode}-${Alpha2Code}`;
|
|
9
|
+
export type SupportedLanguage = LanguageCode | Bcp47LanguageTag;
|
|
10
|
+
|
|
5
11
|
export interface PluginOptions {
|
|
6
12
|
|
|
7
13
|
/* List of ISO 639-1 language codes which you want to tsupport*/
|
|
8
|
-
supportedLanguages:
|
|
14
|
+
supportedLanguages: SupportedLanguage[];
|
|
9
15
|
|
|
10
16
|
/**
|
|
11
17
|
* Each translation string will be stored in a separate field, you can remap it to existing columns using this option
|
|
12
18
|
* By default it will assume field are named like `${lang_code}_string` (e.g. 'en_string', 'uk_string', 'ja_string', 'fr_string')
|
|
13
19
|
*/
|
|
14
|
-
translationFieldNames: Partial<Record<
|
|
20
|
+
translationFieldNames: Partial<Record<SupportedLanguage, string>>;
|
|
15
21
|
|
|
16
22
|
/**
|
|
17
23
|
* Each string has a category, e.g. it might come from 'frontend' or some message from backend or column name on backend
|
|
@@ -49,4 +55,12 @@ export interface PluginOptions {
|
|
|
49
55
|
* it should be a JSON field (underlyng database type should be TEXT or JSON)
|
|
50
56
|
*/
|
|
51
57
|
reviewedCheckboxesFieldName?: string;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Primary language for the application. This is the default language shown to users.
|
|
61
|
+
* English is always used as the source language for translations, even if primaryLanguage is different.
|
|
62
|
+
* When a translation is missing for the primaryLanguage, English will be shown as fallback.
|
|
63
|
+
* Defaults to 'en' if not specified.
|
|
64
|
+
*/
|
|
65
|
+
primaryLanguage?: SupportedLanguage;
|
|
52
66
|
}
|