@adminforth/i18n 2.3.2 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.ts CHANGED
@@ -1,6 +1,7 @@
1
- import AdminForth, { AdminForthPlugin, Filters, suggestIfTypo, AdminForthDataTypes, RAMLock, filtersTools, AdminForthFilterOperators } from "adminforth";
1
+ import AdminForth, { Filters, suggestIfTypo, AdminForthDataTypes, RAMLock, filtersTools, AdminForthFilterOperators } from "adminforth";
2
2
  import type { IAdminForth, IHttpServer, AdminForthComponentDeclaration, AdminForthResourceColumn, AdminForthResource, BeforeLoginConfirmationFunction, AdminForthConfigMenuItem, AdminUser } from "adminforth";
3
3
  import type { PluginOptions, SupportedLanguage } from './types.js';
4
+ import { I18nBase, getCountryCodeFromLangCode, getPrimaryLanguageCode, requiresSlavicPlurals, SLAVIC_PLURAL_EXAMPLES, SLAVIC_PLURAL_FORMS, SLAVIC_PLURAL_FORMS_HINT, SOURCE_LANGUAGE } from './base.js';
4
5
  import { z } from "zod";
5
6
  import iso6391 from 'iso-639-1';
6
7
 
@@ -25,7 +26,6 @@ import path from 'path';
25
26
  import fs from 'fs-extra';
26
27
  import chokidar from 'chokidar';
27
28
  import { AsyncQueue } from '@sapphire/async-queue';
28
- import getFlagEmoji from 'country-flag-svg';
29
29
  import { parse } from 'bcp-47';
30
30
  import pLimit from 'p-limit';
31
31
  import { randomUUID } from 'crypto';
@@ -33,62 +33,6 @@ import { afLogger } from "adminforth";
33
33
 
34
34
  const processFrontendMessagesQueue = new AsyncQueue();
35
35
 
36
- const SLAVIC_PLURAL_EXAMPLES = {
37
- uk: 'яблук | Яблуко | Яблука | Яблук', // zero | singular | 2-4 | 5+
38
- bg: 'ябълки | ябълка | ябълки | ябълки', // zero | singular | 2-4 | 5+
39
- cs: 'jablek | jablko | jablka | jablek', // zero | singular | 2-4 | 5+
40
- hr: 'jabuka | jabuka | jabuke | jabuka', // zero | singular | 2-4 | 5+
41
- mk: 'јаболка | јаболко | јаболка | јаболка', // zero | singular | 2-4 | 5+
42
- pl: 'jabłek | jabłko | jabłka | jabłek', // zero | singular | 2-4 | 5+
43
- sk: 'jabĺk | jablko | jablká | jabĺk', // zero | singular | 2-4 | 5+
44
- sl: 'jabolk | jabolko | jabolka | jabolk', // zero | singular | 2-4 | 5+
45
- sr: 'јабука | јабука | јабуке | јабука', // zero | singular | 2-4 | 5+
46
- be: 'яблыкаў | яблык | яблыкі | яблыкаў', // zero | singular | 2-4 | 5+
47
- ru: 'яблок | яблоко | яблока | яблок', // zero | singular | 2-4 | 5+
48
- };
49
-
50
- const countryISO31661ByLangISO6391 = {
51
- en: 'us', // English → United States
52
- zh: 'cn', // Chinese → China
53
- hi: 'in', // Hindi → India
54
- ar: 'sa', // Arabic → Saudi Arabia
55
- ko: 'kr', // Korean → South Korea
56
- ja: 'jp', // Japanese → Japan
57
- uk: 'ua', // Ukrainian → Ukraine
58
- ur: 'pk', // Urdu → Pakistan
59
- sr: 'rs', // Serbian → Serbia
60
- da: 'dk' // Danish → Denmark
61
- };
62
-
63
- function getCountryCodeFromLangCode(lang: SupportedLanguage) {
64
- const [langCode, region] = String(lang).split('-');
65
- if (region && /^[A-Z]{2}$/.test(region)) {
66
- return region.toLowerCase();
67
- }
68
- return countryISO31661ByLangISO6391[langCode] || langCode;
69
- }
70
-
71
- function getPrimaryLanguageCode(langCode: SupportedLanguage): string {
72
- return String(langCode).split('-')[0];
73
- }
74
-
75
- function isValidSupportedLanguageTag(langCode: SupportedLanguage): boolean {
76
- try {
77
- const schema = parse(String(langCode),
78
- {
79
- normalize: true,
80
- warning: (reason, code, offset) => {
81
- console.warn(`Warning in validating language tag ${langCode}: reason=${reason}, code=${code}, offset=${offset}`);
82
- }
83
- }
84
- );
85
- const isValid = schema.language.length === 2;
86
- return isValid;
87
- } catch (e) {
88
- return false;
89
- }
90
- }
91
-
92
36
  interface IFailedTranslation {
93
37
  lang: SupportedLanguage;
94
38
  en_string: string;
@@ -134,8 +78,7 @@ class AiTranslateError extends Error {
134
78
  this.name = 'AiTranslateError';
135
79
  }
136
80
  }
137
- export default class I18nPlugin extends AdminForthPlugin {
138
- options: PluginOptions;
81
+ export default class I18nPlugin extends I18nBase<PluginOptions> {
139
82
  emailField: AdminForthResourceColumn;
140
83
  passwordField: AdminForthResourceColumn;
141
84
  authResource: AdminForthResource;
@@ -146,35 +89,39 @@ export default class I18nPlugin extends AdminForthPlugin {
146
89
  primaryKeyFieldName: string;
147
90
  menuItemWithBadgeId: string;
148
91
 
149
- adminforth: IAdminForth;
150
-
151
92
  externalAppOnly: boolean;
152
93
 
153
94
  // sorted by name list of all supported languages, without en e.g. 'al|ro|uk'
154
95
  fullCompleatedFieldValue: string;
155
- primaryLanguage: SupportedLanguage;
156
96
 
157
97
  constructor(options: PluginOptions) {
158
- super(options, import.meta.url);
159
- this.options = options;
98
+ super(options);
160
99
  this.cache = new CachingAdapterMemory();
161
100
  this.trFieldNames = {};
162
- this.primaryLanguage = options.primaryLanguage || 'en';
163
101
  }
164
102
 
165
- async computeCompletedFieldValue(record: any) {
166
- return this.options.supportedLanguages.reduce((acc: string, lang: SupportedLanguage): string => {
167
- if (lang === 'en') {
168
- return acc;
169
- }
170
- if (record[this.trFieldNames[lang]]) {
171
- if (acc !== '') {
172
- acc += '|';
173
- }
174
- acc += (lang as string);
175
- }
176
- return acc;
177
- }, '');
103
+ // translated languages joined by '|', e.g. 'uk|ja', stored in completedFieldName to filter not fully translated strings
104
+ completedLangsValue(isTranslated: (lang: SupportedLanguage) => boolean): string {
105
+ return this.targetLanguages().filter(isTranslated).join('|');
106
+ }
107
+
108
+ computeCompletedFieldValue(record: any): string {
109
+ return this.completedLangsValue((lang) => Boolean(record[this.trFieldNames[lang]]));
110
+ }
111
+
112
+ // all translations of category for frontend_messages
113
+ categoryCacheKey(category: string, lang: string): string {
114
+ return `${this.resourceConfig.resourceId}:${category}:${lang}`;
115
+ }
116
+
117
+ // one string translated by backend tr
118
+ messageCacheKey(category: string, lang: string, enString: string): string {
119
+ return `${this.categoryCacheKey(category, lang)}:${enString}`;
120
+ }
121
+
122
+ clearTranslationCache(category: string, lang: string, enString: string) {
123
+ this.cache.clear(this.categoryCacheKey(category, lang));
124
+ this.cache.clear(this.messageCacheKey(category, lang, enString));
178
125
  }
179
126
 
180
127
  async updateUntranslatedMenuBadge() {
@@ -201,13 +148,6 @@ export default class I18nPlugin extends AdminForthPlugin {
201
148
  }
202
149
  );
203
150
 
204
- // validate each supported language: ISO 639-1 or BCP-47 with region (e.g., en-GB)
205
- this.options.supportedLanguages.forEach((lang) => {
206
- if (!isValidSupportedLanguageTag(lang)) {
207
- throw new Error(`Invalid language code ${lang}. Use ISO 639-1 (e.g., 'en') or BCP-47 with region (e.g., 'en-GB').`);
208
- }
209
- });
210
-
211
151
  if (this.options.translateLangAsBCP47Code) {
212
152
  for (const [lang, bcp47] of Object.entries(this.options.translateLangAsBCP47Code)) {
213
153
  if (!this.options.supportedLanguages.includes(lang as SupportedLanguage)) {
@@ -226,10 +166,7 @@ export default class I18nPlugin extends AdminForthPlugin {
226
166
  }
227
167
 
228
168
  // parse trFieldNames
229
- for (const lang of this.options.supportedLanguages) {
230
- if (lang === 'en') {
231
- continue;
232
- }
169
+ for (const lang of this.targetLanguages()) {
233
170
  if (this.options.translationFieldNames?.[lang]) {
234
171
  this.trFieldNames[lang] = this.options.translationFieldNames[lang];
235
172
  } else {
@@ -264,18 +201,9 @@ export default class I18nPlugin extends AdminForthPlugin {
264
201
  };
265
202
  }
266
203
 
267
- this.enFieldName = this.trFieldNames['en'] || 'en_string';
204
+ this.enFieldName = this.trFieldNames[SOURCE_LANGUAGE] || `${SOURCE_LANGUAGE}_string`;
268
205
 
269
- this.fullCompleatedFieldValue = this.options.supportedLanguages.reduce((acc: string, lang: SupportedLanguage) => {
270
- if (lang === 'en') {
271
- return acc;
272
- }
273
- if (acc !== '') {
274
- acc += '|';
275
- }
276
- acc += lang as string;
277
- return acc;
278
- }, '');
206
+ this.fullCompleatedFieldValue = this.completedLangsValue(() => true);
279
207
 
280
208
  // if not enFieldName column is not found, throw error
281
209
  const enColumn = resourceConfig.columns.find(c => c.name === this.enFieldName);
@@ -341,35 +269,9 @@ export default class I18nPlugin extends AdminForthPlugin {
341
269
  });
342
270
  }
343
271
 
344
- const compMeta = {
345
- brandSlug: adminforth.config.customization.brandNameSlug,
346
- pluginInstanceId: this.pluginInstanceId,
347
- primaryLanguage: this.primaryLanguage,
348
- afOrder: this.options.loginPageLanguageSelectorOrder || 0,
349
- supportedLanguages: this.options.supportedLanguages.map(lang => (
350
- {
351
- code: lang,
352
- // lang name on on language native name
353
- name: iso6391.getNativeName(getPrimaryLanguageCode(lang)),
354
- }
355
- ))
356
- };
357
272
  // add underLogin component
358
273
  if (!this.externalAppOnly) {
359
- (adminforth.config.customization.loginPageInjections.underLoginButton as Array<any>).push({
360
- file: this.componentPath('LanguageUnderLogin.vue'),
361
- meta: compMeta
362
- });
363
-
364
- (adminforth.config.customization.globalInjections.userMenu).push({
365
- file: this.componentPath('LanguageInUserMenu.vue'),
366
- meta: compMeta
367
- });
368
-
369
- adminforth.config.customization.globalInjections.everyPageBottom.push({
370
- file: this.componentPath('LanguageEveryPageLoader.vue'),
371
- meta: compMeta
372
- });
274
+ this.injectLanguageSelectors(adminforth);
373
275
  }
374
276
 
375
277
  // disable create allowedActions for translations
@@ -377,10 +279,7 @@ export default class I18nPlugin extends AdminForthPlugin {
377
279
 
378
280
  // add hook to validate user did not screw up with template params
379
281
  resourceConfig.hooks.edit.beforeSave.push(async ({ updates, oldRecord }: { updates: any, oldRecord?: any }): Promise<{ ok: boolean, error?: string }> => {
380
- for (const lang of this.options.supportedLanguages) {
381
- if (lang === 'en') {
382
- continue;
383
- }
282
+ for (const lang of this.targetLanguages()) {
384
283
  if (updates[this.trFieldNames[lang]]) { // if user set '', it will have '' in updates, then it is fine, we shoudl nto check it
385
284
  if (!ensureTemplateHasAllParams(oldRecord[this.enFieldName], updates[this.trFieldNames[lang]])) {
386
285
  return { ok: false, error: `Template params mismatch for ${updates[this.enFieldName]}. Template param names should be the same as in original string. E. g. 'Hello {name}', should be 'Hola {name}' and not 'Hola {nombre}'!` };
@@ -394,20 +293,11 @@ export default class I18nPlugin extends AdminForthPlugin {
394
293
  resourceConfig.hooks.edit.afterSave.push(async ({ updates, oldRecord }: { updates: any, oldRecord?: any }): Promise<{ ok: boolean, error?: string }> => {
395
294
  if (oldRecord) {
396
295
  // find lang which changed
397
- let langsChanged: SupportedLanguage[] = [];
398
- for (const lang of this.options.supportedLanguages) {
399
- if (lang === 'en') {
400
- continue;
401
- }
402
- if (updates[this.trFieldNames[lang]] !== undefined) {
403
- langsChanged.push(lang);
404
- }
405
- }
406
-
296
+ const langsChanged = this.targetLanguages().filter((lang) => updates[this.trFieldNames[lang]] !== undefined);
297
+
407
298
  // clear frontend cache for all langsChanged
408
299
  for (const lang of langsChanged) {
409
- this.cache.clear(`${this.resourceConfig.resourceId}:${oldRecord[this.options.categoryFieldName]}:${lang}`);
410
- this.cache.clear(`${this.resourceConfig.resourceId}:${oldRecord[this.options.categoryFieldName]}:${lang}:${oldRecord[this.enFieldName]}`);
300
+ this.clearTranslationCache(oldRecord[this.options.categoryFieldName], lang, oldRecord[this.enFieldName]);
411
301
  }
412
302
  this.updateUntranslatedMenuBadge();
413
303
  }
@@ -418,9 +308,7 @@ export default class I18nPlugin extends AdminForthPlugin {
418
308
  // add hook on delete of any translation to reset cache
419
309
  resourceConfig.hooks.delete.afterSave.push(async ({ record }: { record: any }): Promise<{ ok: boolean, error?: string }> => {
420
310
  for (const lang of this.options.supportedLanguages) {
421
- // if frontend, clear frontend cache
422
- this.cache.clear(`${this.resourceConfig.resourceId}:${record[this.options.categoryFieldName]}:${lang}`);
423
- this.cache.clear(`${this.resourceConfig.resourceId}:${record[this.options.categoryFieldName]}:${lang}:${record[this.enFieldName]}`);
311
+ this.clearTranslationCache(record[this.options.categoryFieldName], lang, record[this.enFieldName]);
424
312
  }
425
313
  this.updateUntranslatedMenuBadge();
426
314
  return { ok: true };
@@ -489,6 +377,7 @@ export default class I18nPlugin extends AdminForthPlugin {
489
377
  file: this.componentPath('BulkActionButton.vue'),
490
378
  meta: {
491
379
  supportedLanguages: this.options.supportedLanguages,
380
+ countryCodes: Object.fromEntries(this.options.supportedLanguages.map((lang) => [lang, getCountryCodeFromLangCode(lang)])),
492
381
  pluginInstanceId: this.pluginInstanceId,
493
382
  }
494
383
  }
@@ -671,13 +560,8 @@ export default class I18nPlugin extends AdminForthPlugin {
671
560
 
672
561
 
673
562
  for (const lang of langsInvolved) {
674
- const categoriesInvolved = new Set();
675
563
  for (const { enStr, category } of Object.values(updateStrings)) {
676
- categoriesInvolved.add(category);
677
- await this.cache.clear(`${this.resourceConfig.resourceId}:${category}:${lang}:${enStr}`);
678
- }
679
- for (const category of categoriesInvolved) {
680
- await this.cache.clear(`${this.resourceConfig.resourceId}:${category}:${lang}`);
564
+ this.clearTranslationCache(category, lang, enStr);
681
565
  }
682
566
  }
683
567
 
@@ -746,14 +630,14 @@ export default class I18nPlugin extends AdminForthPlugin {
746
630
  const lang = langIsoCode;
747
631
  const primaryLang = getPrimaryLanguageCode(lang);
748
632
  const langName = iso6391.getName(primaryLang);
749
- const requestSlavicPlurals = Object.keys(SLAVIC_PLURAL_EXAMPLES).includes(primaryLang) && plurals;
633
+ const requestSlavicPlurals = requiresSlavicPlurals(lang) && plurals;
750
634
  const region = langSchema.region;
751
635
  const basePrompt = `
752
636
  I need to translate strings in JSON to ${langName} language ${replacedLanguageCodeForTranslations || lang.length > 2 ? `BCP-47 code ${langCode}` : `ISO 639-1 code ${langIsoCode}`} from English for my web app.
753
637
 
754
638
 
755
639
  ${region ? `Use the regional conventions for ${langCode} (region ${region}), including spelling, punctuation, and formatting.` : ''}
756
- ${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]}"` : ''}
640
+ ${requestSlavicPlurals ? `You should provide ${SLAVIC_PLURAL_FORMS} slavic forms (in format "${SLAVIC_PLURAL_FORMS_HINT}") e.g. "apple | apples" should become "${SLAVIC_PLURAL_EXAMPLES[primaryLang]}"` : ''}
757
641
  JSON keys are internal identifiers. Keep keys as is and write translations into values. If values have variables (in curly brackets), then translated strings should have them as well (variables itself should not be translated). Here are the strings:
758
642
  \`\`\`json
759
643
  {
@@ -832,6 +716,7 @@ export default class I18nPlugin extends AdminForthPlugin {
832
716
  translations: translations.filter(t => stringBanchCopy.includes(t.en_string)),
833
717
  updateStrings,
834
718
  lang,
719
+ countryCode: getCountryCodeFromLangCode(lang as SupportedLanguage),
835
720
  failedToTranslate,
836
721
  needToTranslateByLang,
837
722
  promptCost: basePromptTokenLength + banchTokens,
@@ -866,8 +751,8 @@ export default class I18nPlugin extends AdminForthPlugin {
866
751
  const translations = await this.adminforth.resource(this.resourceConfig.resourceId).list(Filters.IN(this.primaryKeyFieldName, selectedIds));
867
752
  const languagesToProcess = selectedLanguages || this.options.supportedLanguages;
868
753
  for (const lang of languagesToProcess) {
869
- if (lang === 'en') {
870
- // all strings are in English, no need to translate
754
+ if (lang === SOURCE_LANGUAGE) {
755
+ // all strings are in source language, no need to translate
871
756
  continue;
872
757
  }
873
758
  for (const translation of translations) {
@@ -1049,10 +934,7 @@ export default class I18nPlugin extends AdminForthPlugin {
1049
934
  this.options.completeAdapter.validate();
1050
935
  }
1051
936
 
1052
- for (const lang of this.options.supportedLanguages) {
1053
- if (lang === 'en') {
1054
- continue;
1055
- }
937
+ for (const lang of this.targetLanguages()) {
1056
938
  const column = resourceConfig.columns.find(c => c.name === this.trFieldNames[lang]);
1057
939
  if (!column) {
1058
940
  throw new Error(`Field ${this.trFieldNames[lang]} not found for storing translation for language ${lang}
@@ -1092,151 +974,51 @@ export default class I18nPlugin extends AdminForthPlugin {
1092
974
  if (!this.externalAppOnly) {
1093
975
  this.tryProcessAndWatch(adminforth);
1094
976
 
1095
- adminforth.tr = async (msg: string | null | undefined, category: string, lang: string, params, pluralizationNumber: number): Promise<string> => {
1096
- if (!msg) {
1097
- return msg;
1098
- }
1099
-
977
+ this.installBackendTranslator(adminforth, async (msg, category, lang) => {
1100
978
  if (category === 'frontend') {
1101
979
  throw new Error(`Category 'frontend' is reserved for frontend messages, use any other category for backend messages`);
1102
980
  }
1103
- // console.log('🪲tr', msg, category, lang);
1104
981
 
1105
- // if lang is not supported, fallback to primaryLanguage, then to english
1106
- if (!this.options.supportedLanguages.includes(lang as SupportedLanguage)) {
1107
- lang = this.primaryLanguage; // fallback to primary language first
1108
- if (!this.options.supportedLanguages.includes(lang as SupportedLanguage)) {
1109
- lang = 'en'; // final fallback to english
1110
- }
1111
- }
1112
-
1113
- let result;
1114
982
  // try to get translation from cache
1115
- const cacheKey = `${resourceConfig.resourceId}:${category}:${lang}:${msg}`;
983
+ const cacheKey = this.messageCacheKey(category, lang, msg);
1116
984
  const cached = await this.cache.get(cacheKey);
1117
985
  if (cached) {
1118
- result = cached;
986
+ return cached;
1119
987
  }
1120
- if (!result) {
1121
- const resource = adminforth.resource(resourceConfig.resourceId);
1122
- const translation = await resource.get([Filters.EQ(this.enFieldName, msg), Filters.EQ(this.options.categoryFieldName, category)]);
1123
- if (!translation) {
1124
- await resource.create({
1125
- [this.enFieldName]: msg,
1126
- [this.options.categoryFieldName]: category,
1127
- });
1128
- this.updateUntranslatedMenuBadge();
1129
- }
1130
-
1131
- // do this check here, to faster register missing translations
1132
- // also not cache it - no sense to cache english strings
1133
- if (lang === 'en') {
1134
- // set to cache to return faster next time
1135
- result = msg;
1136
- } else {
1137
- result = translation?.[this.trFieldNames[lang]];
1138
- if (!result) {
1139
- // return english
1140
- result = msg;
1141
- }
1142
- }
1143
- // cache so even if key does not exist, we will not hit database
1144
- await this.cache.set(cacheKey, result);
1145
- }
1146
- // if msg has '|' in it, then we need to aplly pluralization
1147
- if (msg.includes('|') && pluralizationNumber !== undefined) {
1148
- result = this.applyPluralization(result, pluralizationNumber, lang);
988
+ const resource = adminforth.resource(resourceConfig.resourceId);
989
+ const translation = await resource.get([Filters.EQ(this.enFieldName, msg), Filters.EQ(this.options.categoryFieldName, category)]);
990
+ if (!translation) {
991
+ await resource.create({
992
+ [this.enFieldName]: msg,
993
+ [this.options.categoryFieldName]: category,
994
+ });
995
+ this.updateUntranslatedMenuBadge();
1149
996
  }
1150
997
 
1151
- if (params) {
1152
- for (const [key, value] of Object.entries(params)) {
1153
- if (result) {
1154
- result = result.replace(`{${key}}`, value);
1155
- }
1156
- }
1157
- }
998
+ // do this check here, to faster register missing translations
999
+ const result = lang === SOURCE_LANGUAGE ? msg : translation?.[this.trFieldNames[lang]] || msg;
1000
+ // cache so even if key does not exist, we will not hit database
1001
+ await this.cache.set(cacheKey, result);
1158
1002
  return result;
1159
- }
1160
- }
1161
- }
1162
-
1163
- applyPluralization(str: string, number: number, lang: string): string {
1164
- const choices = str.split('|');
1165
- const choicesLength = choices.length;
1166
-
1167
- if (choicesLength > 2) {
1168
- // this is slavic pluralization
1169
- return choices[this.slavicPluralRule(number, choicesLength)];
1170
- } else {
1171
- return number === 1 ? choices[0] : choices[1];
1172
- }
1173
- }
1174
-
1175
- // taken from here https://vue-i18n.intlify.dev/guide/essentials/pluralization.html#custom-pluralization
1176
- slavicPluralRule(choice, choicesLength) {
1177
- if (choice === 0) {
1178
- return 0
1179
- }
1180
-
1181
- const teen = choice > 10 && choice < 20
1182
- const endsWithOne = choice % 10 === 1
1183
-
1184
- if (!teen && endsWithOne) {
1185
- return 1
1186
- }
1187
- if (!teen && choice % 10 >= 2 && choice % 10 <= 4) {
1188
- return 2
1003
+ });
1189
1004
  }
1190
-
1191
- return choicesLength < 4 ? 2 : 3
1192
- }
1193
- instanceUniqueRepresentation(pluginOptions: any) : string {
1194
- // optional method to return unique string representation of plugin instance.
1195
- // Needed if plugin can have multiple instances on one resource
1196
- return `single`;
1197
1005
  }
1198
1006
 
1199
1007
  async getCategoryTranslations(category: string, lang: string): Promise<Record<string, string>> {
1200
1008
  const resource = this.adminforth.resource(this.resourceConfig.resourceId);
1201
- const cacheKey = `${this.resourceConfig.resourceId}:${category}:${lang}`;
1009
+ const cacheKey = this.categoryCacheKey(category, lang);
1202
1010
  const cached = await this.cache.get(cacheKey);
1203
1011
  if (cached) {
1204
1012
  return cached;
1205
1013
  }
1206
- const translations = {};
1207
1014
  const allTranslations = await resource.list([Filters.EQ(this.options.categoryFieldName, category)]);
1208
- for (const tr of allTranslations) {
1209
- const translatedValue = tr[this.trFieldNames[lang]];
1210
- translations[tr[this.enFieldName]] = translatedValue || tr[this.enFieldName];
1211
- }
1015
+ const translations = this.fallbackToOriginal(Object.fromEntries(
1016
+ allTranslations.map((tr) => [tr[this.enFieldName], tr[this.trFieldNames[lang]]]),
1017
+ ));
1212
1018
  await this.cache.set(cacheKey, translations);
1213
1019
  return translations;
1214
1020
  }
1215
1021
 
1216
- async languagesList(): Promise<{
1217
- code: SupportedLanguage;
1218
- nameOnNative: string;
1219
- nameEnglish: string;
1220
- emojiFlag: string;
1221
- svgFlagB64: string;
1222
- }[]> {
1223
- return Promise.all(this.options.supportedLanguages.map(async (lang) => {
1224
- const flagEmoji =
1225
- typeof getFlagEmoji === 'function'
1226
- ? getFlagEmoji
1227
- : getFlagEmoji.default;
1228
-
1229
-
1230
- return {
1231
- code: lang,
1232
- nameOnNative: iso6391.getNativeName(getPrimaryLanguageCode(lang)),
1233
- nameEnglish: iso6391.getName(getPrimaryLanguageCode(lang)),
1234
- emojiFlag: getCountryCodeFromLangCode(lang).toUpperCase().replace(/./g, char => String.fromCodePoint(char.charCodeAt(0) + 127397)),
1235
- svgFlagB64: await flagEmoji(getCountryCodeFromLangCode(lang)),
1236
- };
1237
- }));
1238
- }
1239
-
1240
1022
  async feedCategoryTranslations(messages: {
1241
1023
  en_string: string;
1242
1024
  source: string;
@@ -1279,19 +1061,7 @@ export default class I18nPlugin extends AdminForthPlugin {
1279
1061
 
1280
1062
 
1281
1063
  setupEndpoints(server: IHttpServer) {
1282
- server.endpoint({
1283
- method: 'GET',
1284
- path: `/plugin/${this.pluginInstanceId}/frontend_messages`,
1285
- noAuth: true,
1286
- handler: async ({ query }) => {
1287
- const lang = query.lang;
1288
-
1289
- // form map of translations
1290
- const translations = await this.getCategoryTranslations('frontend', lang);
1291
- return translations;
1292
-
1293
- }
1294
- });
1064
+ super.setupEndpoints(server);
1295
1065
 
1296
1066
  const lock = new RAMLock();
1297
1067
 
@@ -1452,3 +1222,6 @@ export default class I18nPlugin extends AdminForthPlugin {
1452
1222
  }
1453
1223
 
1454
1224
  }
1225
+
1226
+ export { I18nBase, getCountryCodeFromLangCode, getPrimaryLanguageCode, isValidSupportedLanguageTag, requiresSlavicPlurals, SOURCE_LANGUAGE, PLURAL_SEPARATOR, SLAVIC_PLURAL_FORMS, SLAVIC_PLURAL_FORMS_HINT } from './base.js';
1227
+ export type { I18nBaseOptions, SupportedLanguage } from './types.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/i18n",
3
- "version": "2.3.2",
3
+ "version": "2.5.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
package/types.ts CHANGED
@@ -8,11 +8,30 @@ type Alpha2Code = keyof typeof iso31661Alpha2ToAlpha3;
8
8
  type Bcp47LanguageTag = `${LanguageCode}-${Alpha2Code}`;
9
9
  export type SupportedLanguage = LanguageCode | Bcp47LanguageTag;
10
10
 
11
- export interface PluginOptions extends PluginsCommonOptions {
11
+ /**
12
+ * Options shared by all i18n plugins built on top of I18nBase
13
+ */
14
+ export interface I18nBaseOptions extends PluginsCommonOptions {
12
15
 
13
16
  /* List of language codes which you want to support. Can be either short ISO 639-1 language codes or/and BCP47 tags */
14
17
  supportedLanguages: SupportedLanguage[];
15
18
 
19
+ /**
20
+ * Language selector on login page order
21
+ */
22
+ loginPageLanguageSelectorOrder?: number;
23
+
24
+ /**
25
+ * Primary language for the application. This is the default language shown to users.
26
+ * English is always used as the source language for translations, even if primaryLanguage is different.
27
+ * When a translation is missing for the primaryLanguage, English will be shown as fallback.
28
+ * Defaults to 'en' if not specified.
29
+ */
30
+ primaryLanguage?: SupportedLanguage;
31
+ }
32
+
33
+ export interface PluginOptions extends I18nBaseOptions {
34
+
16
35
  /**
17
36
  * Each translation string will be stored in a separate field, you can remap it to existing columns using this option
18
37
  * By default it will assume field are named like `${lang_code}_string` (e.g. 'en_string', 'uk_string', 'ja_string', 'fr_string')
@@ -26,11 +45,6 @@ export interface PluginOptions extends PluginsCommonOptions {
26
45
  */
27
46
  categoryFieldName: string;
28
47
 
29
- /**
30
- * Language selector on login page order
31
- */
32
- loginPageLanguageSelectorOrder?: number;
33
-
34
48
  /**
35
49
  * Optional source field to store e.g. file name where it first was captured
36
50
  */
@@ -61,14 +75,6 @@ export interface PluginOptions extends PluginsCommonOptions {
61
75
  */
62
76
  reviewedCheckboxesFieldName?: string;
63
77
 
64
- /**
65
- * Primary language for the application. This is the default language shown to users.
66
- * English is always used as the source language for translations, even if primaryLanguage is different.
67
- * When a translation is missing for the primaryLanguage, English will be shown as fallback.
68
- * Defaults to 'en' if not specified.
69
- */
70
- primaryLanguage?: SupportedLanguage;
71
-
72
78
  /**
73
79
  * Ask translator to treat some code from supportedLanguages as exact BCP47 tag. Read docs for details.
74
80
  * key - one of the values form supportedLanguages, value -BCP47 tag