@adminforth/i18n 2.4.0 → 2.5.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/index.ts CHANGED
@@ -1,7 +1,7 @@
1
- import AdminForth, { Filters, suggestIfTypo, AdminForthDataTypes, RAMLock, filtersTools, AdminForthFilterOperators } from "adminforth";
1
+ import AdminForth, { Filters, suggestIfTypo, AdminForthDataTypes, RAMLock, filtersTools, AdminForthFilterOperators, interpretResource, ActionCheckSource, AllowedActionsEnum } 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, getPrimaryLanguageCode, requiresSlavicPlurals, SLAVIC_PLURAL_EXAMPLES } from './base.js';
4
+ import { I18nBase, getCountryCodeFromLangCode, getPrimaryLanguageCode, requiresSlavicPlurals, SLAVIC_PLURAL_EXAMPLES, SLAVIC_PLURAL_FORMS, SLAVIC_PLURAL_FORMS_HINT, SOURCE_LANGUAGE } from './base.js';
5
5
  import { z } from "zod";
6
6
  import iso6391 from 'iso-639-1';
7
7
 
@@ -100,19 +100,28 @@ export default class I18nPlugin extends I18nBase<PluginOptions> {
100
100
  this.trFieldNames = {};
101
101
  }
102
102
 
103
- async computeCompletedFieldValue(record: any) {
104
- return this.options.supportedLanguages.reduce((acc: string, lang: SupportedLanguage): string => {
105
- if (lang === 'en') {
106
- return acc;
107
- }
108
- if (record[this.trFieldNames[lang]]) {
109
- if (acc !== '') {
110
- acc += '|';
111
- }
112
- acc += (lang as string);
113
- }
114
- return acc;
115
- }, '');
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));
116
125
  }
117
126
 
118
127
  async updateUntranslatedMenuBadge() {
@@ -157,10 +166,7 @@ export default class I18nPlugin extends I18nBase<PluginOptions> {
157
166
  }
158
167
 
159
168
  // parse trFieldNames
160
- for (const lang of this.options.supportedLanguages) {
161
- if (lang === 'en') {
162
- continue;
163
- }
169
+ for (const lang of this.targetLanguages()) {
164
170
  if (this.options.translationFieldNames?.[lang]) {
165
171
  this.trFieldNames[lang] = this.options.translationFieldNames[lang];
166
172
  } else {
@@ -195,18 +201,9 @@ export default class I18nPlugin extends I18nBase<PluginOptions> {
195
201
  };
196
202
  }
197
203
 
198
- this.enFieldName = this.trFieldNames['en'] || 'en_string';
204
+ this.enFieldName = this.trFieldNames[SOURCE_LANGUAGE] || `${SOURCE_LANGUAGE}_string`;
199
205
 
200
- this.fullCompleatedFieldValue = this.options.supportedLanguages.reduce((acc: string, lang: SupportedLanguage) => {
201
- if (lang === 'en') {
202
- return acc;
203
- }
204
- if (acc !== '') {
205
- acc += '|';
206
- }
207
- acc += lang as string;
208
- return acc;
209
- }, '');
206
+ this.fullCompleatedFieldValue = this.completedLangsValue(() => true);
210
207
 
211
208
  // if not enFieldName column is not found, throw error
212
209
  const enColumn = resourceConfig.columns.find(c => c.name === this.enFieldName);
@@ -282,10 +279,7 @@ export default class I18nPlugin extends I18nBase<PluginOptions> {
282
279
 
283
280
  // add hook to validate user did not screw up with template params
284
281
  resourceConfig.hooks.edit.beforeSave.push(async ({ updates, oldRecord }: { updates: any, oldRecord?: any }): Promise<{ ok: boolean, error?: string }> => {
285
- for (const lang of this.options.supportedLanguages) {
286
- if (lang === 'en') {
287
- continue;
288
- }
282
+ for (const lang of this.targetLanguages()) {
289
283
  if (updates[this.trFieldNames[lang]]) { // if user set '', it will have '' in updates, then it is fine, we shoudl nto check it
290
284
  if (!ensureTemplateHasAllParams(oldRecord[this.enFieldName], updates[this.trFieldNames[lang]])) {
291
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}'!` };
@@ -299,20 +293,11 @@ export default class I18nPlugin extends I18nBase<PluginOptions> {
299
293
  resourceConfig.hooks.edit.afterSave.push(async ({ updates, oldRecord }: { updates: any, oldRecord?: any }): Promise<{ ok: boolean, error?: string }> => {
300
294
  if (oldRecord) {
301
295
  // find lang which changed
302
- let langsChanged: SupportedLanguage[] = [];
303
- for (const lang of this.options.supportedLanguages) {
304
- if (lang === 'en') {
305
- continue;
306
- }
307
- if (updates[this.trFieldNames[lang]] !== undefined) {
308
- langsChanged.push(lang);
309
- }
310
- }
311
-
296
+ const langsChanged = this.targetLanguages().filter((lang) => updates[this.trFieldNames[lang]] !== undefined);
297
+
312
298
  // clear frontend cache for all langsChanged
313
299
  for (const lang of langsChanged) {
314
- this.cache.clear(`${this.resourceConfig.resourceId}:${oldRecord[this.options.categoryFieldName]}:${lang}`);
315
- 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]);
316
301
  }
317
302
  this.updateUntranslatedMenuBadge();
318
303
  }
@@ -323,9 +308,7 @@ export default class I18nPlugin extends I18nBase<PluginOptions> {
323
308
  // add hook on delete of any translation to reset cache
324
309
  resourceConfig.hooks.delete.afterSave.push(async ({ record }: { record: any }): Promise<{ ok: boolean, error?: string }> => {
325
310
  for (const lang of this.options.supportedLanguages) {
326
- // if frontend, clear frontend cache
327
- this.cache.clear(`${this.resourceConfig.resourceId}:${record[this.options.categoryFieldName]}:${lang}`);
328
- 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]);
329
312
  }
330
313
  this.updateUntranslatedMenuBadge();
331
314
  return { ok: true };
@@ -394,6 +377,7 @@ export default class I18nPlugin extends I18nBase<PluginOptions> {
394
377
  file: this.componentPath('BulkActionButton.vue'),
395
378
  meta: {
396
379
  supportedLanguages: this.options.supportedLanguages,
380
+ countryCodes: Object.fromEntries(this.options.supportedLanguages.map((lang) => [lang, getCountryCodeFromLangCode(lang)])),
397
381
  pluginInstanceId: this.pluginInstanceId,
398
382
  }
399
383
  }
@@ -576,13 +560,8 @@ export default class I18nPlugin extends I18nBase<PluginOptions> {
576
560
 
577
561
 
578
562
  for (const lang of langsInvolved) {
579
- const categoriesInvolved = new Set();
580
563
  for (const { enStr, category } of Object.values(updateStrings)) {
581
- categoriesInvolved.add(category);
582
- await this.cache.clear(`${this.resourceConfig.resourceId}:${category}:${lang}:${enStr}`);
583
- }
584
- for (const category of categoriesInvolved) {
585
- await this.cache.clear(`${this.resourceConfig.resourceId}:${category}:${lang}`);
564
+ this.clearTranslationCache(category, lang, enStr);
586
565
  }
587
566
  }
588
567
 
@@ -658,7 +637,7 @@ export default class I18nPlugin extends I18nBase<PluginOptions> {
658
637
 
659
638
 
660
639
  ${region ? `Use the regional conventions for ${langCode} (region ${region}), including spelling, punctuation, and formatting.` : ''}
661
- ${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[primaryLang]}"` : ''}
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]}"` : ''}
662
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:
663
642
  \`\`\`json
664
643
  {
@@ -737,6 +716,7 @@ export default class I18nPlugin extends I18nBase<PluginOptions> {
737
716
  translations: translations.filter(t => stringBanchCopy.includes(t.en_string)),
738
717
  updateStrings,
739
718
  lang,
719
+ countryCode: getCountryCodeFromLangCode(lang as SupportedLanguage),
740
720
  failedToTranslate,
741
721
  needToTranslateByLang,
742
722
  promptCost: basePromptTokenLength + banchTokens,
@@ -771,8 +751,8 @@ export default class I18nPlugin extends I18nBase<PluginOptions> {
771
751
  const translations = await this.adminforth.resource(this.resourceConfig.resourceId).list(Filters.IN(this.primaryKeyFieldName, selectedIds));
772
752
  const languagesToProcess = selectedLanguages || this.options.supportedLanguages;
773
753
  for (const lang of languagesToProcess) {
774
- if (lang === 'en') {
775
- // 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
776
756
  continue;
777
757
  }
778
758
  for (const translation of translations) {
@@ -954,10 +934,7 @@ export default class I18nPlugin extends I18nBase<PluginOptions> {
954
934
  this.options.completeAdapter.validate();
955
935
  }
956
936
 
957
- for (const lang of this.options.supportedLanguages) {
958
- if (lang === 'en') {
959
- continue;
960
- }
937
+ for (const lang of this.targetLanguages()) {
961
938
  const column = resourceConfig.columns.find(c => c.name === this.trFieldNames[lang]);
962
939
  if (!column) {
963
940
  throw new Error(`Field ${this.trFieldNames[lang]} not found for storing translation for language ${lang}
@@ -997,69 +974,47 @@ export default class I18nPlugin extends I18nBase<PluginOptions> {
997
974
  if (!this.externalAppOnly) {
998
975
  this.tryProcessAndWatch(adminforth);
999
976
 
1000
- adminforth.tr = async (msg: string | null | undefined, category: string, lang: string, params, pluralizationNumber: number): Promise<string> => {
1001
- if (!msg) {
1002
- return msg;
1003
- }
1004
-
977
+ this.installBackendTranslator(adminforth, async (msg, category, lang) => {
1005
978
  if (category === 'frontend') {
1006
979
  throw new Error(`Category 'frontend' is reserved for frontend messages, use any other category for backend messages`);
1007
980
  }
1008
- // console.log('🪲tr', msg, category, lang);
1009
-
1010
- lang = this.resolveLanguage(lang);
1011
981
 
1012
- let result;
1013
982
  // try to get translation from cache
1014
- const cacheKey = `${resourceConfig.resourceId}:${category}:${lang}:${msg}`;
983
+ const cacheKey = this.messageCacheKey(category, lang, msg);
1015
984
  const cached = await this.cache.get(cacheKey);
1016
985
  if (cached) {
1017
- result = cached;
986
+ return cached;
1018
987
  }
1019
- if (!result) {
1020
- const resource = adminforth.resource(resourceConfig.resourceId);
1021
- const translation = await resource.get([Filters.EQ(this.enFieldName, msg), Filters.EQ(this.options.categoryFieldName, category)]);
1022
- if (!translation) {
1023
- await resource.create({
1024
- [this.enFieldName]: msg,
1025
- [this.options.categoryFieldName]: category,
1026
- });
1027
- this.updateUntranslatedMenuBadge();
1028
- }
1029
-
1030
- // do this check here, to faster register missing translations
1031
- // also not cache it - no sense to cache english strings
1032
- if (lang === 'en') {
1033
- // set to cache to return faster next time
1034
- result = msg;
1035
- } else {
1036
- result = translation?.[this.trFieldNames[lang]];
1037
- if (!result) {
1038
- // return english
1039
- result = msg;
1040
- }
1041
- }
1042
- // cache so even if key does not exist, we will not hit database
1043
- await this.cache.set(cacheKey, result);
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();
1044
996
  }
1045
- return this.formatTranslation(msg, result, lang, params, pluralizationNumber);
1046
- }
997
+
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);
1002
+ return result;
1003
+ });
1047
1004
  }
1048
1005
  }
1049
1006
 
1050
1007
  async getCategoryTranslations(category: string, lang: string): Promise<Record<string, string>> {
1051
1008
  const resource = this.adminforth.resource(this.resourceConfig.resourceId);
1052
- const cacheKey = `${this.resourceConfig.resourceId}:${category}:${lang}`;
1009
+ const cacheKey = this.categoryCacheKey(category, lang);
1053
1010
  const cached = await this.cache.get(cacheKey);
1054
1011
  if (cached) {
1055
1012
  return cached;
1056
1013
  }
1057
- const translations = {};
1058
1014
  const allTranslations = await resource.list([Filters.EQ(this.options.categoryFieldName, category)]);
1059
- for (const tr of allTranslations) {
1060
- const translatedValue = tr[this.trFieldNames[lang]];
1061
- translations[tr[this.enFieldName]] = translatedValue || tr[this.enFieldName];
1062
- }
1015
+ const translations = this.fallbackToOriginal(Object.fromEntries(
1016
+ allTranslations.map((tr) => [tr[this.enFieldName], tr[this.trFieldNames[lang]]]),
1017
+ ));
1063
1018
  await this.cache.set(cacheKey, translations);
1064
1019
  return translations;
1065
1020
  }
@@ -1155,6 +1110,19 @@ export default class I18nPlugin extends I18nBase<PluginOptions> {
1155
1110
  updateRecord[this.options.reviewedCheckboxesFieldName] = { ...oldValue };
1156
1111
  }
1157
1112
 
1113
+ const { allowedActions } = await interpretResource(
1114
+ adminUser,
1115
+ resource,
1116
+ { requestBody: body, newRecord: updateRecord, oldRecord, pk: recordId },
1117
+ ActionCheckSource.EditRequest,
1118
+ this.adminforth
1119
+ );
1120
+ const editAllowed = allowedActions[AllowedActionsEnum.edit];
1121
+ if (editAllowed !== true) {
1122
+ result = { error: typeof editAllowed === 'string' ? editAllowed : 'You do not have permission to edit this record' };
1123
+ return;
1124
+ }
1125
+
1158
1126
  result = await this.adminforth.updateResourceRecord({
1159
1127
  resource,
1160
1128
  recordId,
@@ -1268,5 +1236,5 @@ export default class I18nPlugin extends I18nBase<PluginOptions> {
1268
1236
 
1269
1237
  }
1270
1238
 
1271
- export { I18nBase, getCountryCodeFromLangCode, getPrimaryLanguageCode, isValidSupportedLanguageTag, requiresSlavicPlurals, SOURCE_LANGUAGE, PLURAL_SEPARATOR, SLAVIC_PLURAL_FORMS } from './base.js';
1239
+ export { I18nBase, getCountryCodeFromLangCode, getPrimaryLanguageCode, isValidSupportedLanguageTag, requiresSlavicPlurals, SOURCE_LANGUAGE, PLURAL_SEPARATOR, SLAVIC_PLURAL_FORMS, SLAVIC_PLURAL_FORMS_HINT } from './base.js';
1272
1240
  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.4.0",
3
+ "version": "2.5.1",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",