@adminforth/i18n 2.4.0 → 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/base.ts +175 -12
- package/build.log +2 -2
- package/custom/BulkActionButton.vue +2 -2
- package/custom/LanguageInUserMenu.vue +4 -12
- package/custom/LanguageUnderLogin.vue +4 -12
- package/custom/TranslationJobViewComponent.vue +2 -3
- package/custom/langCommon.ts +12 -24
- package/dist/base.d.ts +8 -0
- package/dist/base.js +172 -12
- package/dist/custom/BulkActionButton.vue +2 -2
- package/dist/custom/LanguageInUserMenu.vue +4 -12
- package/dist/custom/LanguageUnderLogin.vue +4 -12
- package/dist/custom/TranslationJobViewComponent.vue +2 -3
- package/dist/custom/langCommon.ts +12 -24
- package/dist/index.d.ts +6 -2
- package/dist/index.js +53 -105
- package/index.ts +61 -106
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
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, 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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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.
|
|
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[
|
|
204
|
+
this.enFieldName = this.trFieldNames[SOURCE_LANGUAGE] || `${SOURCE_LANGUAGE}_string`;
|
|
199
205
|
|
|
200
|
-
this.fullCompleatedFieldValue = this.
|
|
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.
|
|
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
|
-
|
|
303
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 ===
|
|
775
|
-
// all strings are in
|
|
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.
|
|
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
|
|
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 =
|
|
983
|
+
const cacheKey = this.messageCacheKey(category, lang, msg);
|
|
1015
984
|
const cached = await this.cache.get(cacheKey);
|
|
1016
985
|
if (cached) {
|
|
1017
|
-
|
|
986
|
+
return cached;
|
|
1018
987
|
}
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
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
|
}
|
|
@@ -1268,5 +1223,5 @@ export default class I18nPlugin extends I18nBase<PluginOptions> {
|
|
|
1268
1223
|
|
|
1269
1224
|
}
|
|
1270
1225
|
|
|
1271
|
-
export { I18nBase, getCountryCodeFromLangCode, getPrimaryLanguageCode, isValidSupportedLanguageTag, requiresSlavicPlurals, SOURCE_LANGUAGE, PLURAL_SEPARATOR, SLAVIC_PLURAL_FORMS } from './base.js';
|
|
1226
|
+
export { I18nBase, getCountryCodeFromLangCode, getPrimaryLanguageCode, isValidSupportedLanguageTag, requiresSlavicPlurals, SOURCE_LANGUAGE, PLURAL_SEPARATOR, SLAVIC_PLURAL_FORMS, SLAVIC_PLURAL_FORMS_HINT } from './base.js';
|
|
1272
1227
|
export type { I18nBaseOptions, SupportedLanguage } from './types.js';
|