@adminforth/i18n 1.3.6 → 1.4.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/build.log +2 -2
- package/custom/LanguageEveryPageLoader.vue +4 -2
- package/custom/LanguageInUserMenu.vue +3 -2
- package/custom/LanguageUnderLogin.vue +3 -2
- package/custom/langCommon.ts +11 -2
- package/dist/custom/LanguageEveryPageLoader.vue +4 -2
- package/dist/custom/LanguageInUserMenu.vue +3 -2
- package/dist/custom/LanguageUnderLogin.vue +3 -2
- package/dist/custom/langCommon.ts +11 -2
- package/dist/index.js +48 -25
- package/index.ts +68 -47
- 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 31,
|
|
20
|
+
sent 32,912 bytes received 248 bytes 66,320.00 bytes/sec
|
|
21
|
+
total size is 31,979 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>
|
|
@@ -70,9 +70,10 @@ function doChangeLang(lang) {
|
|
|
70
70
|
|
|
71
71
|
const options = computed(() => {
|
|
72
72
|
return props.meta.supportedLanguages.map((lang) => {
|
|
73
|
+
const region = String(lang.code).split('-')[1]?.toUpperCase();
|
|
73
74
|
return {
|
|
74
75
|
value: lang.code,
|
|
75
|
-
label: lang.name,
|
|
76
|
+
label: region ? `${lang.name} (${region})` : lang.name,
|
|
76
77
|
};
|
|
77
78
|
});
|
|
78
79
|
});
|
|
@@ -87,6 +88,6 @@ const selectedOption = computed(() => {
|
|
|
87
88
|
|
|
88
89
|
|
|
89
90
|
onMounted(() => {
|
|
90
|
-
selectedLanguage.value = getLocalLang(props.meta.supportedLanguages);
|
|
91
|
+
selectedLanguage.value = getLocalLang(props.meta.supportedLanguages, props.meta.primaryLanguage);
|
|
91
92
|
});
|
|
92
93
|
</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>
|
|
@@ -70,9 +70,10 @@ function doChangeLang(lang) {
|
|
|
70
70
|
|
|
71
71
|
const options = computed(() => {
|
|
72
72
|
return props.meta.supportedLanguages.map((lang) => {
|
|
73
|
+
const region = String(lang.code).split('-')[1]?.toUpperCase();
|
|
73
74
|
return {
|
|
74
75
|
value: lang.code,
|
|
75
|
-
label: lang.name,
|
|
76
|
+
label: region ? `${lang.name} (${region})` : lang.name,
|
|
76
77
|
};
|
|
77
78
|
});
|
|
78
79
|
});
|
|
@@ -87,6 +88,6 @@ const selectedOption = computed(() => {
|
|
|
87
88
|
|
|
88
89
|
|
|
89
90
|
onMounted(() => {
|
|
90
|
-
selectedLanguage.value = getLocalLang(props.meta.supportedLanguages);
|
|
91
|
+
selectedLanguage.value = getLocalLang(props.meta.supportedLanguages, props.meta.primaryLanguage);
|
|
91
92
|
});
|
|
92
93
|
</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,13 +9,11 @@ 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';
|
|
15
16
|
import { AsyncQueue } from '@sapphire/async-queue';
|
|
16
|
-
console.log = (...args) => {
|
|
17
|
-
process.stdout.write(args.join(" ") + "\n");
|
|
18
|
-
};
|
|
19
17
|
const processFrontendMessagesQueue = new AsyncQueue();
|
|
20
18
|
const SLAVIC_PLURAL_EXAMPLES = {
|
|
21
19
|
uk: 'яблук | Яблуко | Яблука | Яблук', // zero | singular | 2-4 | 5+
|
|
@@ -40,9 +38,27 @@ const countryISO31661ByLangISO6391 = {
|
|
|
40
38
|
uk: 'ua', // Ukrainian → Ukraine
|
|
41
39
|
ur: 'pk', // Urdu → Pakistan
|
|
42
40
|
};
|
|
43
|
-
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
|
+
}
|
|
44
46
|
return countryISO31661ByLangISO6391[langCode] || langCode;
|
|
45
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
|
+
}
|
|
46
62
|
class CachingAdapterMemory {
|
|
47
63
|
constructor() {
|
|
48
64
|
this.cache = {};
|
|
@@ -87,6 +103,7 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
87
103
|
this.options = options;
|
|
88
104
|
this.cache = new CachingAdapterMemory();
|
|
89
105
|
this.trFieldNames = {};
|
|
106
|
+
this.primaryLanguage = options.primaryLanguage || 'en';
|
|
90
107
|
}
|
|
91
108
|
computeCompletedFieldValue(record) {
|
|
92
109
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -122,10 +139,10 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
122
139
|
return __awaiter(this, void 0, void 0, function* () {
|
|
123
140
|
var _a, _b;
|
|
124
141
|
_super.modifyResourceConfig.call(this, adminforth, resourceConfig);
|
|
125
|
-
//
|
|
142
|
+
// validate each supported language: ISO 639-1 or BCP-47 with region (e.g., en-GB)
|
|
126
143
|
this.options.supportedLanguages.forEach((lang) => {
|
|
127
|
-
if (!
|
|
128
|
-
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').`);
|
|
129
146
|
}
|
|
130
147
|
});
|
|
131
148
|
this.externalAppOnly = this.options.externalAppOnly === true;
|
|
@@ -242,10 +259,11 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
242
259
|
const compMeta = {
|
|
243
260
|
brandSlug: adminforth.config.customization.brandNameSlug,
|
|
244
261
|
pluginInstanceId: this.pluginInstanceId,
|
|
262
|
+
primaryLanguage: this.primaryLanguage,
|
|
245
263
|
supportedLanguages: this.options.supportedLanguages.map(lang => ({
|
|
246
264
|
code: lang,
|
|
247
265
|
// lang name on on language native name
|
|
248
|
-
name: iso6391.getNativeName(lang),
|
|
266
|
+
name: iso6391.getNativeName(getPrimaryLanguageCode(lang)),
|
|
249
267
|
}))
|
|
250
268
|
};
|
|
251
269
|
// add underLogin component
|
|
@@ -427,6 +445,7 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
427
445
|
}
|
|
428
446
|
translateToLang(langIsoCode_1, strings_1) {
|
|
429
447
|
return __awaiter(this, arguments, void 0, function* (langIsoCode, strings, plurals = false, translations, updateStrings = {}) {
|
|
448
|
+
var _a;
|
|
430
449
|
const maxKeysInOneReq = 10;
|
|
431
450
|
if (strings.length === 0) {
|
|
432
451
|
return [];
|
|
@@ -442,21 +461,23 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
442
461
|
return totalTranslated;
|
|
443
462
|
}
|
|
444
463
|
const lang = langIsoCode;
|
|
445
|
-
const
|
|
446
|
-
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()) || '';
|
|
447
468
|
const prompt = `
|
|
448
|
-
I need to translate strings in JSON to ${langName} language (ISO 639-1 code ${lang}) from English for my web app.
|
|
449
|
-
${
|
|
450
|
-
|
|
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:
|
|
451
473
|
|
|
452
|
-
\`\`\`json
|
|
453
|
-
${JSON.stringify(strings.reduce((acc, s) => {
|
|
474
|
+
\`\`\`json
|
|
475
|
+
${JSON.stringify(strings.reduce((acc, s) => {
|
|
454
476
|
acc[s.en_string] = '';
|
|
455
477
|
return acc;
|
|
456
478
|
}, {}), null, 2)}
|
|
457
|
-
\`\`\`
|
|
458
|
-
`;
|
|
459
|
-
process.env.HEAVY_DEBUG && console.log(`🪲🔪LLM prompt >> ${prompt.length}, <<${prompt} :\n\n`, JSON.stringify(prompt));
|
|
479
|
+
\`\`\`
|
|
480
|
+
`;
|
|
460
481
|
// call OpenAI
|
|
461
482
|
const resp = yield this.options.completeAdapter.complete(prompt, [], prompt.length * 2);
|
|
462
483
|
process.env.HEAVY_DEBUG && console.log(`🪲🔪LLM resp >> ${prompt.length}, <<${resp.content.length} :\n\n`, JSON.stringify(resp));
|
|
@@ -660,11 +681,12 @@ ${JSON.stringify(strings.reduce((acc, s) => {
|
|
|
660
681
|
throw new Error(`Category 'frontend' is reserved for frontend messages, use any other category for backend messages`);
|
|
661
682
|
}
|
|
662
683
|
// console.log('🪲tr', msg, category, lang);
|
|
663
|
-
// if lang is not supported ,
|
|
684
|
+
// if lang is not supported, fallback to primaryLanguage, then to english
|
|
664
685
|
if (!this.options.supportedLanguages.includes(lang)) {
|
|
665
|
-
lang =
|
|
666
|
-
|
|
667
|
-
|
|
686
|
+
lang = this.primaryLanguage; // fallback to primary language first
|
|
687
|
+
if (!this.options.supportedLanguages.includes(lang)) {
|
|
688
|
+
lang = 'en'; // final fallback to english
|
|
689
|
+
}
|
|
668
690
|
}
|
|
669
691
|
let result;
|
|
670
692
|
// try to get translation from cache
|
|
@@ -756,7 +778,8 @@ ${JSON.stringify(strings.reduce((acc, s) => {
|
|
|
756
778
|
const translations = {};
|
|
757
779
|
const allTranslations = yield resource.list([Filters.EQ(this.options.categoryFieldName, category)]);
|
|
758
780
|
for (const tr of allTranslations) {
|
|
759
|
-
|
|
781
|
+
const translatedValue = tr[this.trFieldNames[lang]];
|
|
782
|
+
translations[tr[this.enFieldName]] = translatedValue || tr[this.enFieldName];
|
|
760
783
|
}
|
|
761
784
|
yield this.cache.set(cacheKey, translations);
|
|
762
785
|
return translations;
|
|
@@ -767,8 +790,8 @@ ${JSON.stringify(strings.reduce((acc, s) => {
|
|
|
767
790
|
return this.options.supportedLanguages.map((lang) => {
|
|
768
791
|
return {
|
|
769
792
|
code: lang,
|
|
770
|
-
nameOnNative: iso6391.getNativeName(lang),
|
|
771
|
-
nameEnglish: iso6391.getName(lang),
|
|
793
|
+
nameOnNative: iso6391.getNativeName(getPrimaryLanguageCode(lang)),
|
|
794
|
+
nameEnglish: iso6391.getName(getPrimaryLanguageCode(lang)),
|
|
772
795
|
emojiFlag: getCountryCodeFromLangCode(lang).toUpperCase().replace(/./g, char => String.fromCodePoint(char.charCodeAt(0) + 127397)),
|
|
773
796
|
};
|
|
774
797
|
});
|
package/index.ts
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
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';
|
|
8
9
|
import { AsyncQueue } from '@sapphire/async-queue';
|
|
9
10
|
|
|
10
11
|
|
|
11
|
-
console.log = (...args) => {
|
|
12
|
-
process.stdout.write(args.join(" ") + "\n");
|
|
13
|
-
};
|
|
14
|
-
|
|
15
12
|
const processFrontendMessagesQueue = new AsyncQueue();
|
|
16
13
|
|
|
17
14
|
const SLAVIC_PLURAL_EXAMPLES = {
|
|
@@ -39,10 +36,30 @@ const countryISO31661ByLangISO6391 = {
|
|
|
39
36
|
ur: 'pk', // Urdu → Pakistan
|
|
40
37
|
};
|
|
41
38
|
|
|
42
|
-
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
|
+
}
|
|
43
44
|
return countryISO31661ByLangISO6391[langCode] || langCode;
|
|
44
45
|
}
|
|
45
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
|
+
|
|
46
63
|
|
|
47
64
|
interface ICachingAdapter {
|
|
48
65
|
get(key: string): Promise<any>;
|
|
@@ -88,7 +105,7 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
88
105
|
passwordField: AdminForthResourceColumn;
|
|
89
106
|
authResource: AdminForthResource;
|
|
90
107
|
emailConfirmedField?: AdminForthResourceColumn;
|
|
91
|
-
trFieldNames: Partial<Record<
|
|
108
|
+
trFieldNames: Partial<Record<SupportedLanguage, string>>;
|
|
92
109
|
enFieldName: string;
|
|
93
110
|
cache: ICachingAdapter;
|
|
94
111
|
primaryKeyFieldName: string;
|
|
@@ -100,16 +117,18 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
100
117
|
|
|
101
118
|
// sorted by name list of all supported languages, without en e.g. 'al|ro|uk'
|
|
102
119
|
fullCompleatedFieldValue: string;
|
|
120
|
+
primaryLanguage: SupportedLanguage;
|
|
103
121
|
|
|
104
122
|
constructor(options: PluginOptions) {
|
|
105
123
|
super(options, import.meta.url);
|
|
106
124
|
this.options = options;
|
|
107
125
|
this.cache = new CachingAdapterMemory();
|
|
108
126
|
this.trFieldNames = {};
|
|
127
|
+
this.primaryLanguage = options.primaryLanguage || 'en';
|
|
109
128
|
}
|
|
110
129
|
|
|
111
130
|
async computeCompletedFieldValue(record: any) {
|
|
112
|
-
return this.options.supportedLanguages.reduce((acc: string, lang:
|
|
131
|
+
return this.options.supportedLanguages.reduce((acc: string, lang: SupportedLanguage): string => {
|
|
113
132
|
if (lang === 'en') {
|
|
114
133
|
return acc;
|
|
115
134
|
}
|
|
@@ -138,10 +157,10 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
138
157
|
async modifyResourceConfig(adminforth: IAdminForth, resourceConfig: AdminForthResource) {
|
|
139
158
|
super.modifyResourceConfig(adminforth, resourceConfig);
|
|
140
159
|
|
|
141
|
-
//
|
|
160
|
+
// validate each supported language: ISO 639-1 or BCP-47 with region (e.g., en-GB)
|
|
142
161
|
this.options.supportedLanguages.forEach((lang) => {
|
|
143
|
-
if (!
|
|
144
|
-
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').`);
|
|
145
164
|
}
|
|
146
165
|
});
|
|
147
166
|
|
|
@@ -195,7 +214,7 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
195
214
|
|
|
196
215
|
this.enFieldName = this.trFieldNames['en'] || 'en_string';
|
|
197
216
|
|
|
198
|
-
this.fullCompleatedFieldValue = this.options.supportedLanguages.reduce((acc: string, lang:
|
|
217
|
+
this.fullCompleatedFieldValue = this.options.supportedLanguages.reduce((acc: string, lang: SupportedLanguage) => {
|
|
199
218
|
if (lang === 'en') {
|
|
200
219
|
return acc;
|
|
201
220
|
}
|
|
@@ -273,11 +292,12 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
273
292
|
const compMeta = {
|
|
274
293
|
brandSlug: adminforth.config.customization.brandNameSlug,
|
|
275
294
|
pluginInstanceId: this.pluginInstanceId,
|
|
295
|
+
primaryLanguage: this.primaryLanguage,
|
|
276
296
|
supportedLanguages: this.options.supportedLanguages.map(lang => (
|
|
277
297
|
{
|
|
278
298
|
code: lang,
|
|
279
299
|
// lang name on on language native name
|
|
280
|
-
name: iso6391.getNativeName(lang),
|
|
300
|
+
name: iso6391.getNativeName(getPrimaryLanguageCode(lang)),
|
|
281
301
|
}
|
|
282
302
|
))
|
|
283
303
|
};
|
|
@@ -321,7 +341,7 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
321
341
|
resourceConfig.hooks.edit.afterSave.push(async ({ updates, oldRecord }: { updates: any, oldRecord?: any }): Promise<{ ok: boolean, error?: string }> => {
|
|
322
342
|
if (oldRecord) {
|
|
323
343
|
// find lang which changed
|
|
324
|
-
let langsChanged:
|
|
344
|
+
let langsChanged: SupportedLanguage[] = [];
|
|
325
345
|
for (const lang of this.options.supportedLanguages) {
|
|
326
346
|
if (lang === 'en') {
|
|
327
347
|
continue;
|
|
@@ -477,7 +497,7 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
477
497
|
}
|
|
478
498
|
|
|
479
499
|
async translateToLang (
|
|
480
|
-
langIsoCode:
|
|
500
|
+
langIsoCode: SupportedLanguage,
|
|
481
501
|
strings: { en_string: string, category: string }[],
|
|
482
502
|
plurals=false,
|
|
483
503
|
translations: any[],
|
|
@@ -499,25 +519,25 @@ export default class I18nPlugin extends AdminForthPlugin {
|
|
|
499
519
|
return totalTranslated;
|
|
500
520
|
}
|
|
501
521
|
const lang = langIsoCode;
|
|
502
|
-
const
|
|
503
|
-
const
|
|
504
|
-
|
|
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() || '';
|
|
505
526
|
const prompt = `
|
|
506
|
-
I need to translate strings in JSON to ${langName} language (ISO 639-1 code ${lang}) from English for my web app.
|
|
507
|
-
${
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
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
|
+
`;
|
|
521
541
|
|
|
522
542
|
// call OpenAI
|
|
523
543
|
const resp = await this.options.completeAdapter.complete(
|
|
@@ -588,7 +608,7 @@ JSON.stringify(strings.reduce((acc: object, s: { en_string: string }): object =>
|
|
|
588
608
|
|
|
589
609
|
const needToTranslateByLang : Partial<
|
|
590
610
|
Record<
|
|
591
|
-
|
|
611
|
+
SupportedLanguage,
|
|
592
612
|
{
|
|
593
613
|
en_string: string;
|
|
594
614
|
category: string;
|
|
@@ -631,7 +651,7 @@ JSON.stringify(strings.reduce((acc: object, s: { en_string: string }): object =>
|
|
|
631
651
|
|
|
632
652
|
await Promise.all(
|
|
633
653
|
Object.entries(needToTranslateByLang).map(
|
|
634
|
-
async ([lang, strings]: [
|
|
654
|
+
async ([lang, strings]: [SupportedLanguage, { en_string: string, category: string }[]]) => {
|
|
635
655
|
// first translate without plurals
|
|
636
656
|
const stringsWithoutPlurals = strings.filter(s => !s.en_string.includes('|'));
|
|
637
657
|
const noPluralKeys = await this.translateToLang(lang, stringsWithoutPlurals, false, translations, updateStrings);
|
|
@@ -785,13 +805,13 @@ JSON.stringify(strings.reduce((acc: object, s: { en_string: string }): object =>
|
|
|
785
805
|
}
|
|
786
806
|
// console.log('🪲tr', msg, category, lang);
|
|
787
807
|
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
// 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
|
|
794
813
|
}
|
|
814
|
+
}
|
|
795
815
|
|
|
796
816
|
let result;
|
|
797
817
|
// try to get translation from cache
|
|
@@ -889,23 +909,24 @@ JSON.stringify(strings.reduce((acc: object, s: { en_string: string }): object =>
|
|
|
889
909
|
const translations = {};
|
|
890
910
|
const allTranslations = await resource.list([Filters.EQ(this.options.categoryFieldName, category)]);
|
|
891
911
|
for (const tr of allTranslations) {
|
|
892
|
-
|
|
912
|
+
const translatedValue = tr[this.trFieldNames[lang]];
|
|
913
|
+
translations[tr[this.enFieldName]] = translatedValue || tr[this.enFieldName];
|
|
893
914
|
}
|
|
894
915
|
await this.cache.set(cacheKey, translations);
|
|
895
916
|
return translations;
|
|
896
917
|
}
|
|
897
918
|
|
|
898
919
|
async languagesList(): Promise<{
|
|
899
|
-
code:
|
|
920
|
+
code: SupportedLanguage;
|
|
900
921
|
nameOnNative: string;
|
|
901
922
|
nameEnglish: string;
|
|
902
923
|
emojiFlag: string;
|
|
903
924
|
}[]> {
|
|
904
925
|
return this.options.supportedLanguages.map((lang) => {
|
|
905
926
|
return {
|
|
906
|
-
code: lang
|
|
907
|
-
nameOnNative: iso6391.getNativeName(lang),
|
|
908
|
-
nameEnglish: iso6391.getName(lang),
|
|
927
|
+
code: lang,
|
|
928
|
+
nameOnNative: iso6391.getNativeName(getPrimaryLanguageCode(lang)),
|
|
929
|
+
nameEnglish: iso6391.getName(getPrimaryLanguageCode(lang)),
|
|
909
930
|
emojiFlag: getCountryCodeFromLangCode(lang).toUpperCase().replace(/./g, char => String.fromCodePoint(char.charCodeAt(0) + 127397)),
|
|
910
931
|
};
|
|
911
932
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adminforth/i18n",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
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
|
}
|