@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 CHANGED
@@ -17,5 +17,5 @@ custom/package-lock.json
17
17
  custom/package.json
18
18
  custom/tsconfig.json
19
19
 
20
- sent 32,187 bytes received 248 bytes 64,870.00 bytes/sec
21
- total size is 31,254 speedup is 0.96
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
- setLang({ setLocaleMessage, locale }, props.meta.pluginInstanceId, getLocalLang(props.meta.supportedLanguages));
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
- return countryISO31661ByLangISO6391[langCode] || langCode;
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
- setLang({ setLocaleMessage, locale }, props.meta.pluginInstanceId, getLocalLang(props.meta.supportedLanguages));
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
- return countryISO31661ByLangISO6391[langCode] || langCode;
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(langCode) {
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
- // check each supported language is valid ISO 639-1 code
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 (!iso6391.validate(lang)) {
128
- throw new Error(`Invalid language code ${lang}, please define valid ISO 639-1 language code (2 lowercase letters)`);
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 langName = iso6391.getName(lang);
446
- const requestSlavicPlurals = Object.keys(SLAVIC_PLURAL_EXAMPLES).includes(lang) && plurals;
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
- ${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]}"` : ''}
450
- 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:
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 , throw
684
+ // if lang is not supported, fallback to primaryLanguage, then to english
664
685
  if (!this.options.supportedLanguages.includes(lang)) {
665
- lang = 'en'; // for now simply fallback to english
666
- // throwing like line below might be too strict, e.g. for custom apis made with fetch which don't pass accept-language
667
- // throw new Error(`Language ${lang} is not entered to be supported by requested by browser in request headers accept-language`);
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
- translations[tr[this.enFieldName]] = tr[this.trFieldNames[lang]];
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, { LanguageCode } from 'iso-639-1';
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(langCode) {
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<LanguageCode, string>>;
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: LanguageCode): string => {
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
- // check each supported language is valid ISO 639-1 code
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 (!iso6391.validate(lang)) {
144
- throw new Error(`Invalid language code ${lang}, please define valid ISO 639-1 language code (2 lowercase letters)`);
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: LanguageCode) => {
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: LanguageCode[] = [];
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: LanguageCode,
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 langName = iso6391.getName(lang);
503
- const requestSlavicPlurals = Object.keys(SLAVIC_PLURAL_EXAMPLES).includes(lang) && plurals;
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
- ${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]}"` : ''}
508
- 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:
509
-
510
- \`\`\`json
511
- ${
512
- JSON.stringify(strings.reduce((acc: object, s: { en_string: string }): object => {
513
- acc[s.en_string] = '';
514
- return acc;
515
- }, {}), null, 2)
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
- LanguageCode,
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]: [LanguageCode, { en_string: string, category: string }[]]) => {
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
- // if lang is not supported , throw
789
- if (!this.options.supportedLanguages.includes(lang as LanguageCode)) {
790
- lang = 'en'; // for now simply fallback to english
791
-
792
- // throwing like line below might be too strict, e.g. for custom apis made with fetch which don't pass accept-language
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
- translations[tr[this.enFieldName]] = tr[this.trFieldNames[lang]];
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: LanguageCode;
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 as LanguageCode,
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.6",
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: LanguageCode[];
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<LanguageCode, string>>;
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
  }