@adminforth/i18n 2.3.2 → 2.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/base.ts ADDED
@@ -0,0 +1,239 @@
1
+ import { AdminForthPlugin } from "adminforth";
2
+ import type { IAdminForth, IHttpServer } from "adminforth";
3
+ import type { I18nBaseOptions, SupportedLanguage } from './types.js';
4
+ import iso6391 from 'iso-639-1';
5
+ import getFlagEmoji from 'country-flag-svg';
6
+ import { parse } from 'bcp-47';
7
+
8
+ const REGION_CODE_RE = /^[A-Z]{2}$/;
9
+
10
+ // keys of all translations are original English strings
11
+ export const SOURCE_LANGUAGE = 'en';
12
+ export const PLURAL_SEPARATOR = '|';
13
+ export const SLAVIC_PLURAL_FORMS = 4;
14
+
15
+ export const SLAVIC_PLURAL_EXAMPLES = {
16
+ uk: 'яблук | яблуко | яблука | яблук', // zero | singular | 2-4 | 5+
17
+ bg: 'ябълки | ябълка | ябълки | ябълки', // zero | singular | 2-4 | 5+
18
+ cs: 'jablek | jablko | jablka | jablek', // zero | singular | 2-4 | 5+
19
+ hr: 'jabuka | jabuka | jabuke | jabuka', // zero | singular | 2-4 | 5+
20
+ mk: 'јаболка | јаболко | јаболка | јаболка', // zero | singular | 2-4 | 5+
21
+ pl: 'jabłek | jabłko | jabłka | jabłek', // zero | singular | 2-4 | 5+
22
+ sk: 'jabĺk | jablko | jablká | jabĺk', // zero | singular | 2-4 | 5+
23
+ sl: 'jabolk | jabolko | jabolka | jabolk', // zero | singular | 2-4 | 5+
24
+ sr: 'јабука | јабука | јабуке | јабука', // zero | singular | 2-4 | 5+
25
+ be: 'яблыкаў | яблык | яблыкі | яблыкаў', // zero | singular | 2-4 | 5+
26
+ ru: 'яблок | яблоко | яблока | яблок', // zero | singular | 2-4 | 5+
27
+ };
28
+
29
+ const countryISO31661ByLangISO6391 = {
30
+ en: 'us', // English → United States
31
+ zh: 'cn', // Chinese → China
32
+ hi: 'in', // Hindi → India
33
+ ar: 'sa', // Arabic → Saudi Arabia
34
+ ko: 'kr', // Korean → South Korea
35
+ ja: 'jp', // Japanese → Japan
36
+ uk: 'ua', // Ukrainian → Ukraine
37
+ ur: 'pk', // Urdu → Pakistan
38
+ sr: 'rs', // Serbian → Serbia
39
+ da: 'dk' // Danish → Denmark
40
+ };
41
+
42
+ export function getCountryCodeFromLangCode(lang: SupportedLanguage) {
43
+ const [langCode, region] = String(lang).split('-');
44
+ if (region && REGION_CODE_RE.test(region)) {
45
+ return region.toLowerCase();
46
+ }
47
+ return countryISO31661ByLangISO6391[langCode] || langCode;
48
+ }
49
+
50
+ export function getPrimaryLanguageCode(langCode: SupportedLanguage): string {
51
+ return String(langCode).split('-')[0];
52
+ }
53
+
54
+ // languages which need 4 plural forms "zero | singular | 2-4 | 5+" instead of English ones
55
+ export function requiresSlavicPlurals(lang: string): boolean {
56
+ return getPrimaryLanguageCode(lang as SupportedLanguage) in SLAVIC_PLURAL_EXAMPLES;
57
+ }
58
+
59
+ export function isValidSupportedLanguageTag(langCode: SupportedLanguage): boolean {
60
+ try {
61
+ const schema = parse(String(langCode),
62
+ {
63
+ normalize: true,
64
+ warning: (reason, code, offset) => {
65
+ console.warn(`Warning in validating language tag ${langCode}: reason=${reason}, code=${code}, offset=${offset}`);
66
+ }
67
+ }
68
+ );
69
+ const isValid = schema.language.length === 2;
70
+ return isValid;
71
+ } catch (e) {
72
+ return false;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Storage-independent part of i18n plugins: language validation, language selector components,
78
+ * frontend messages endpoint and formatting of translated strings (params and pluralization).
79
+ * Subclasses provide translations storage by implementing getCategoryTranslations.
80
+ * Custom components are resolved from this package's custom folder, so subclasses from other packages reuse them.
81
+ */
82
+ export abstract class I18nBase<O extends I18nBaseOptions = I18nBaseOptions> extends AdminForthPlugin {
83
+ options: O;
84
+ primaryLanguage: SupportedLanguage;
85
+ adminforth: IAdminForth;
86
+
87
+ constructor(options: O) {
88
+ super(options, import.meta.url);
89
+ this.options = options;
90
+ this.primaryLanguage = options.primaryLanguage || SOURCE_LANGUAGE;
91
+ this.validateSupportedLanguages();
92
+ }
93
+
94
+ // app has one translations storage, so plugin instance id does not depend on options
95
+ instanceUniqueRepresentation(): string {
96
+ return 'single';
97
+ }
98
+
99
+ abstract getCategoryTranslations(category: string, lang: string): Promise<Record<string, string>>;
100
+
101
+ // validate each supported language: ISO 639-1 or BCP-47 with region (e.g., en-GB)
102
+ validateSupportedLanguages() {
103
+ this.options.supportedLanguages.forEach((lang) => {
104
+ if (!isValidSupportedLanguageTag(lang)) {
105
+ throw new Error(`Invalid language code ${lang}. Use ISO 639-1 (e.g., 'en') or BCP-47 with region (e.g., 'en-GB').`);
106
+ }
107
+ });
108
+ }
109
+
110
+ injectLanguageSelectors(adminforth: IAdminForth) {
111
+ const compMeta = {
112
+ brandSlug: adminforth.config.customization.brandNameSlug,
113
+ pluginInstanceId: this.pluginInstanceId,
114
+ primaryLanguage: this.primaryLanguage,
115
+ afOrder: this.options.loginPageLanguageSelectorOrder || 0,
116
+ supportedLanguages: this.options.supportedLanguages.map(lang => (
117
+ {
118
+ code: lang,
119
+ // lang name on on language native name
120
+ name: iso6391.getNativeName(getPrimaryLanguageCode(lang)),
121
+ }
122
+ ))
123
+ };
124
+
125
+ (adminforth.config.customization.loginPageInjections.underLoginButton as Array<any>).push({
126
+ file: this.componentPath('LanguageUnderLogin.vue'),
127
+ meta: compMeta
128
+ });
129
+
130
+ (adminforth.config.customization.globalInjections.userMenu).push({
131
+ file: this.componentPath('LanguageInUserMenu.vue'),
132
+ meta: compMeta
133
+ });
134
+
135
+ adminforth.config.customization.globalInjections.everyPageBottom.push({
136
+ file: this.componentPath('LanguageEveryPageLoader.vue'),
137
+ meta: compMeta
138
+ });
139
+ }
140
+
141
+ // if lang is not supported, fallback to primaryLanguage, then to english
142
+ resolveLanguage(lang: string): string {
143
+ if (!this.options.supportedLanguages.includes(lang as SupportedLanguage)) {
144
+ lang = this.primaryLanguage; // fallback to primary language first
145
+ if (!this.options.supportedLanguages.includes(lang as SupportedLanguage)) {
146
+ lang = SOURCE_LANGUAGE; // final fallback to english
147
+ }
148
+ }
149
+ return lang;
150
+ }
151
+
152
+ formatTranslation(msg: string, translation: string, lang: string, params: Record<string, string> | undefined, pluralizationNumber: number | undefined): string {
153
+ let result = translation;
154
+ // if msg has '|' in it, then we need to aplly pluralization
155
+ if (msg.includes(PLURAL_SEPARATOR) && pluralizationNumber !== undefined) {
156
+ result = this.applyPluralization(result, pluralizationNumber, lang);
157
+ }
158
+
159
+ if (params) {
160
+ for (const [key, value] of Object.entries(params)) {
161
+ if (result) {
162
+ result = result.replace(`{${key}}`, value);
163
+ }
164
+ }
165
+ }
166
+ return result;
167
+ }
168
+
169
+ applyPluralization(str: string, number: number, lang: string): string {
170
+ const choices = str.split(PLURAL_SEPARATOR);
171
+ const choicesLength = choices.length;
172
+
173
+ if (choicesLength > 2) {
174
+ // this is slavic pluralization
175
+ return choices[this.slavicPluralRule(number, choicesLength)];
176
+ } else {
177
+ return number === 1 ? choices[0] : choices[1];
178
+ }
179
+ }
180
+
181
+ // taken from here https://vue-i18n.intlify.dev/guide/essentials/pluralization.html#custom-pluralization
182
+ slavicPluralRule(choice, choicesLength) {
183
+ if (choice === 0) {
184
+ return 0
185
+ }
186
+
187
+ const teen = choice > 10 && choice < 20
188
+ const endsWithOne = choice % 10 === 1
189
+
190
+ if (!teen && endsWithOne) {
191
+ return 1
192
+ }
193
+ if (!teen && choice % 10 >= 2 && choice % 10 <= 4) {
194
+ return 2
195
+ }
196
+
197
+ return choicesLength < 4 ? 2 : 3
198
+ }
199
+
200
+ async languagesList(): Promise<{
201
+ code: SupportedLanguage;
202
+ nameOnNative: string;
203
+ nameEnglish: string;
204
+ emojiFlag: string;
205
+ svgFlagB64: string;
206
+ }[]> {
207
+ return Promise.all(this.options.supportedLanguages.map(async (lang) => {
208
+ const flagEmoji =
209
+ typeof getFlagEmoji === 'function'
210
+ ? getFlagEmoji
211
+ : getFlagEmoji.default;
212
+
213
+
214
+ return {
215
+ code: lang,
216
+ nameOnNative: iso6391.getNativeName(getPrimaryLanguageCode(lang)),
217
+ nameEnglish: iso6391.getName(getPrimaryLanguageCode(lang)),
218
+ emojiFlag: getCountryCodeFromLangCode(lang).toUpperCase().replace(/./g, char => String.fromCodePoint(char.charCodeAt(0) + 127397)),
219
+ svgFlagB64: await flagEmoji(getCountryCodeFromLangCode(lang)),
220
+ };
221
+ }));
222
+ }
223
+
224
+ setupEndpoints(server: IHttpServer) {
225
+ server.endpoint({
226
+ method: 'GET',
227
+ path: `/plugin/${this.pluginInstanceId}/frontend_messages`,
228
+ noAuth: true,
229
+ handler: async ({ query }) => {
230
+ const lang = query.lang;
231
+
232
+ // form map of translations
233
+ const translations = await this.getCategoryTranslations('frontend', lang);
234
+ return translations;
235
+
236
+ }
237
+ });
238
+ }
239
+ }
package/build.log CHANGED
@@ -20,5 +20,5 @@ custom/package.json
20
20
  custom/pnpm-lock.yaml
21
21
  custom/tsconfig.json
22
22
 
23
- sent 48,185 bytes received 305 bytes 96,980.00 bytes/sec
23
+ sent 48,201 bytes received 305 bytes 32,337.33 bytes/sec
24
24
  total size is 47,033 speedup is 0.97
package/dist/base.d.ts ADDED
@@ -0,0 +1,51 @@
1
+ import { AdminForthPlugin } from "adminforth";
2
+ import type { IAdminForth, IHttpServer } from "adminforth";
3
+ import type { I18nBaseOptions, SupportedLanguage } from './types.js';
4
+ export declare const SOURCE_LANGUAGE = "en";
5
+ export declare const PLURAL_SEPARATOR = "|";
6
+ export declare const SLAVIC_PLURAL_FORMS = 4;
7
+ export declare const SLAVIC_PLURAL_EXAMPLES: {
8
+ uk: string;
9
+ bg: string;
10
+ cs: string;
11
+ hr: string;
12
+ mk: string;
13
+ pl: string;
14
+ sk: string;
15
+ sl: string;
16
+ sr: string;
17
+ be: string;
18
+ ru: string;
19
+ };
20
+ export declare function getCountryCodeFromLangCode(lang: SupportedLanguage): any;
21
+ export declare function getPrimaryLanguageCode(langCode: SupportedLanguage): string;
22
+ export declare function requiresSlavicPlurals(lang: string): boolean;
23
+ export declare function isValidSupportedLanguageTag(langCode: SupportedLanguage): boolean;
24
+ /**
25
+ * Storage-independent part of i18n plugins: language validation, language selector components,
26
+ * frontend messages endpoint and formatting of translated strings (params and pluralization).
27
+ * Subclasses provide translations storage by implementing getCategoryTranslations.
28
+ * Custom components are resolved from this package's custom folder, so subclasses from other packages reuse them.
29
+ */
30
+ export declare abstract class I18nBase<O extends I18nBaseOptions = I18nBaseOptions> extends AdminForthPlugin {
31
+ options: O;
32
+ primaryLanguage: SupportedLanguage;
33
+ adminforth: IAdminForth;
34
+ constructor(options: O);
35
+ instanceUniqueRepresentation(): string;
36
+ abstract getCategoryTranslations(category: string, lang: string): Promise<Record<string, string>>;
37
+ validateSupportedLanguages(): void;
38
+ injectLanguageSelectors(adminforth: IAdminForth): void;
39
+ resolveLanguage(lang: string): string;
40
+ formatTranslation(msg: string, translation: string, lang: string, params: Record<string, string> | undefined, pluralizationNumber: number | undefined): string;
41
+ applyPluralization(str: string, number: number, lang: string): string;
42
+ slavicPluralRule(choice: any, choicesLength: any): 0 | 2 | 1 | 3;
43
+ languagesList(): Promise<{
44
+ code: SupportedLanguage;
45
+ nameOnNative: string;
46
+ nameEnglish: string;
47
+ emojiFlag: string;
48
+ svgFlagB64: string;
49
+ }[]>;
50
+ setupEndpoints(server: IHttpServer): void;
51
+ }
package/dist/base.js ADDED
@@ -0,0 +1,203 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { AdminForthPlugin } from "adminforth";
11
+ import iso6391 from 'iso-639-1';
12
+ import getFlagEmoji from 'country-flag-svg';
13
+ import { parse } from 'bcp-47';
14
+ const REGION_CODE_RE = /^[A-Z]{2}$/;
15
+ // keys of all translations are original English strings
16
+ export const SOURCE_LANGUAGE = 'en';
17
+ export const PLURAL_SEPARATOR = '|';
18
+ export const SLAVIC_PLURAL_FORMS = 4;
19
+ export const SLAVIC_PLURAL_EXAMPLES = {
20
+ uk: 'яблук | яблуко | яблука | яблук', // zero | singular | 2-4 | 5+
21
+ bg: 'ябълки | ябълка | ябълки | ябълки', // zero | singular | 2-4 | 5+
22
+ cs: 'jablek | jablko | jablka | jablek', // zero | singular | 2-4 | 5+
23
+ hr: 'jabuka | jabuka | jabuke | jabuka', // zero | singular | 2-4 | 5+
24
+ mk: 'јаболка | јаболко | јаболка | јаболка', // zero | singular | 2-4 | 5+
25
+ pl: 'jabłek | jabłko | jabłka | jabłek', // zero | singular | 2-4 | 5+
26
+ sk: 'jabĺk | jablko | jablká | jabĺk', // zero | singular | 2-4 | 5+
27
+ sl: 'jabolk | jabolko | jabolka | jabolk', // zero | singular | 2-4 | 5+
28
+ sr: 'јабука | јабука | јабуке | јабука', // zero | singular | 2-4 | 5+
29
+ be: 'яблыкаў | яблык | яблыкі | яблыкаў', // zero | singular | 2-4 | 5+
30
+ ru: 'яблок | яблоко | яблока | яблок', // zero | singular | 2-4 | 5+
31
+ };
32
+ const countryISO31661ByLangISO6391 = {
33
+ en: 'us', // English → United States
34
+ zh: 'cn', // Chinese → China
35
+ hi: 'in', // Hindi → India
36
+ ar: 'sa', // Arabic → Saudi Arabia
37
+ ko: 'kr', // Korean → South Korea
38
+ ja: 'jp', // Japanese → Japan
39
+ uk: 'ua', // Ukrainian → Ukraine
40
+ ur: 'pk', // Urdu → Pakistan
41
+ sr: 'rs', // Serbian → Serbia
42
+ da: 'dk' // Danish → Denmark
43
+ };
44
+ export function getCountryCodeFromLangCode(lang) {
45
+ const [langCode, region] = String(lang).split('-');
46
+ if (region && REGION_CODE_RE.test(region)) {
47
+ return region.toLowerCase();
48
+ }
49
+ return countryISO31661ByLangISO6391[langCode] || langCode;
50
+ }
51
+ export function getPrimaryLanguageCode(langCode) {
52
+ return String(langCode).split('-')[0];
53
+ }
54
+ // languages which need 4 plural forms "zero | singular | 2-4 | 5+" instead of English ones
55
+ export function requiresSlavicPlurals(lang) {
56
+ return getPrimaryLanguageCode(lang) in SLAVIC_PLURAL_EXAMPLES;
57
+ }
58
+ export function isValidSupportedLanguageTag(langCode) {
59
+ try {
60
+ const schema = parse(String(langCode), {
61
+ normalize: true,
62
+ warning: (reason, code, offset) => {
63
+ console.warn(`Warning in validating language tag ${langCode}: reason=${reason}, code=${code}, offset=${offset}`);
64
+ }
65
+ });
66
+ const isValid = schema.language.length === 2;
67
+ return isValid;
68
+ }
69
+ catch (e) {
70
+ return false;
71
+ }
72
+ }
73
+ /**
74
+ * Storage-independent part of i18n plugins: language validation, language selector components,
75
+ * frontend messages endpoint and formatting of translated strings (params and pluralization).
76
+ * Subclasses provide translations storage by implementing getCategoryTranslations.
77
+ * Custom components are resolved from this package's custom folder, so subclasses from other packages reuse them.
78
+ */
79
+ export class I18nBase extends AdminForthPlugin {
80
+ constructor(options) {
81
+ super(options, import.meta.url);
82
+ this.options = options;
83
+ this.primaryLanguage = options.primaryLanguage || SOURCE_LANGUAGE;
84
+ this.validateSupportedLanguages();
85
+ }
86
+ // app has one translations storage, so plugin instance id does not depend on options
87
+ instanceUniqueRepresentation() {
88
+ return 'single';
89
+ }
90
+ // validate each supported language: ISO 639-1 or BCP-47 with region (e.g., en-GB)
91
+ validateSupportedLanguages() {
92
+ this.options.supportedLanguages.forEach((lang) => {
93
+ if (!isValidSupportedLanguageTag(lang)) {
94
+ throw new Error(`Invalid language code ${lang}. Use ISO 639-1 (e.g., 'en') or BCP-47 with region (e.g., 'en-GB').`);
95
+ }
96
+ });
97
+ }
98
+ injectLanguageSelectors(adminforth) {
99
+ const compMeta = {
100
+ brandSlug: adminforth.config.customization.brandNameSlug,
101
+ pluginInstanceId: this.pluginInstanceId,
102
+ primaryLanguage: this.primaryLanguage,
103
+ afOrder: this.options.loginPageLanguageSelectorOrder || 0,
104
+ supportedLanguages: this.options.supportedLanguages.map(lang => ({
105
+ code: lang,
106
+ // lang name on on language native name
107
+ name: iso6391.getNativeName(getPrimaryLanguageCode(lang)),
108
+ }))
109
+ };
110
+ adminforth.config.customization.loginPageInjections.underLoginButton.push({
111
+ file: this.componentPath('LanguageUnderLogin.vue'),
112
+ meta: compMeta
113
+ });
114
+ (adminforth.config.customization.globalInjections.userMenu).push({
115
+ file: this.componentPath('LanguageInUserMenu.vue'),
116
+ meta: compMeta
117
+ });
118
+ adminforth.config.customization.globalInjections.everyPageBottom.push({
119
+ file: this.componentPath('LanguageEveryPageLoader.vue'),
120
+ meta: compMeta
121
+ });
122
+ }
123
+ // if lang is not supported, fallback to primaryLanguage, then to english
124
+ resolveLanguage(lang) {
125
+ if (!this.options.supportedLanguages.includes(lang)) {
126
+ lang = this.primaryLanguage; // fallback to primary language first
127
+ if (!this.options.supportedLanguages.includes(lang)) {
128
+ lang = SOURCE_LANGUAGE; // final fallback to english
129
+ }
130
+ }
131
+ return lang;
132
+ }
133
+ formatTranslation(msg, translation, lang, params, pluralizationNumber) {
134
+ let result = translation;
135
+ // if msg has '|' in it, then we need to aplly pluralization
136
+ if (msg.includes(PLURAL_SEPARATOR) && pluralizationNumber !== undefined) {
137
+ result = this.applyPluralization(result, pluralizationNumber, lang);
138
+ }
139
+ if (params) {
140
+ for (const [key, value] of Object.entries(params)) {
141
+ if (result) {
142
+ result = result.replace(`{${key}}`, value);
143
+ }
144
+ }
145
+ }
146
+ return result;
147
+ }
148
+ applyPluralization(str, number, lang) {
149
+ const choices = str.split(PLURAL_SEPARATOR);
150
+ const choicesLength = choices.length;
151
+ if (choicesLength > 2) {
152
+ // this is slavic pluralization
153
+ return choices[this.slavicPluralRule(number, choicesLength)];
154
+ }
155
+ else {
156
+ return number === 1 ? choices[0] : choices[1];
157
+ }
158
+ }
159
+ // taken from here https://vue-i18n.intlify.dev/guide/essentials/pluralization.html#custom-pluralization
160
+ slavicPluralRule(choice, choicesLength) {
161
+ if (choice === 0) {
162
+ return 0;
163
+ }
164
+ const teen = choice > 10 && choice < 20;
165
+ const endsWithOne = choice % 10 === 1;
166
+ if (!teen && endsWithOne) {
167
+ return 1;
168
+ }
169
+ if (!teen && choice % 10 >= 2 && choice % 10 <= 4) {
170
+ return 2;
171
+ }
172
+ return choicesLength < 4 ? 2 : 3;
173
+ }
174
+ languagesList() {
175
+ return __awaiter(this, void 0, void 0, function* () {
176
+ return Promise.all(this.options.supportedLanguages.map((lang) => __awaiter(this, void 0, void 0, function* () {
177
+ const flagEmoji = typeof getFlagEmoji === 'function'
178
+ ? getFlagEmoji
179
+ : getFlagEmoji.default;
180
+ return {
181
+ code: lang,
182
+ nameOnNative: iso6391.getNativeName(getPrimaryLanguageCode(lang)),
183
+ nameEnglish: iso6391.getName(getPrimaryLanguageCode(lang)),
184
+ emojiFlag: getCountryCodeFromLangCode(lang).toUpperCase().replace(/./g, char => String.fromCodePoint(char.charCodeAt(0) + 127397)),
185
+ svgFlagB64: yield flagEmoji(getCountryCodeFromLangCode(lang)),
186
+ };
187
+ })));
188
+ });
189
+ }
190
+ setupEndpoints(server) {
191
+ server.endpoint({
192
+ method: 'GET',
193
+ path: `/plugin/${this.pluginInstanceId}/frontend_messages`,
194
+ noAuth: true,
195
+ handler: (_a) => __awaiter(this, [_a], void 0, function* ({ query }) {
196
+ const lang = query.lang;
197
+ // form map of translations
198
+ const translations = yield this.getCategoryTranslations('frontend', lang);
199
+ return translations;
200
+ })
201
+ });
202
+ }
203
+ }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { AdminForthPlugin } from "adminforth";
2
1
  import type { IAdminForth, IHttpServer, AdminForthResourceColumn, AdminForthResource, AdminUser } from "adminforth";
3
2
  import type { PluginOptions, SupportedLanguage } from './types.js';
3
+ import { I18nBase } from './base.js';
4
4
  interface IFailedTranslation {
5
5
  lang: SupportedLanguage;
6
6
  en_string: string;
@@ -11,8 +11,7 @@ interface ICachingAdapter {
11
11
  set(key: string, value: any): Promise<void>;
12
12
  clear(key: string): Promise<void>;
13
13
  }
14
- export default class I18nPlugin extends AdminForthPlugin {
15
- options: PluginOptions;
14
+ export default class I18nPlugin extends I18nBase<PluginOptions> {
16
15
  emailField: AdminForthResourceColumn;
17
16
  passwordField: AdminForthResourceColumn;
18
17
  authResource: AdminForthResource;
@@ -22,10 +21,8 @@ export default class I18nPlugin extends AdminForthPlugin {
22
21
  cache: ICachingAdapter;
23
22
  primaryKeyFieldName: string;
24
23
  menuItemWithBadgeId: string;
25
- adminforth: IAdminForth;
26
24
  externalAppOnly: boolean;
27
25
  fullCompleatedFieldValue: string;
28
- primaryLanguage: SupportedLanguage;
29
26
  constructor(options: PluginOptions);
30
27
  computeCompletedFieldValue(record: any): Promise<string>;
31
28
  updateUntranslatedMenuBadge(): Promise<void>;
@@ -63,21 +60,12 @@ export default class I18nPlugin extends AdminForthPlugin {
63
60
  processExtractedMessages(adminforth: IAdminForth, filePath: string): Promise<void>;
64
61
  tryProcessAndWatch(adminforth: IAdminForth): Promise<void>;
65
62
  validateConfigAfterDiscover(adminforth: IAdminForth, resourceConfig: AdminForthResource): void;
66
- applyPluralization(str: string, number: number, lang: string): string;
67
- slavicPluralRule(choice: any, choicesLength: any): 3 | 0 | 2 | 1;
68
- instanceUniqueRepresentation(pluginOptions: any): string;
69
63
  getCategoryTranslations(category: string, lang: string): Promise<Record<string, string>>;
70
- languagesList(): Promise<{
71
- code: SupportedLanguage;
72
- nameOnNative: string;
73
- nameEnglish: string;
74
- emojiFlag: string;
75
- svgFlagB64: string;
76
- }[]>;
77
64
  feedCategoryTranslations(messages: {
78
65
  en_string: string;
79
66
  source: string;
80
67
  }[], category: string): Promise<void>;
81
68
  setupEndpoints(server: IHttpServer): void;
82
69
  }
83
- export {};
70
+ export { I18nBase, getCountryCodeFromLangCode, getPrimaryLanguageCode, isValidSupportedLanguageTag, requiresSlavicPlurals, SOURCE_LANGUAGE, PLURAL_SEPARATOR, SLAVIC_PLURAL_FORMS } from './base.js';
71
+ export type { I18nBaseOptions, SupportedLanguage } from './types.js';
package/dist/index.js CHANGED
@@ -7,7 +7,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
7
7
  step((generator = generator.apply(thisArg, _arguments || [])).next());
8
8
  });
9
9
  };
10
- import { AdminForthPlugin, Filters, suggestIfTypo, AdminForthDataTypes, RAMLock, filtersTools, AdminForthFilterOperators } from "adminforth";
10
+ import { Filters, suggestIfTypo, AdminForthDataTypes, RAMLock, filtersTools, AdminForthFilterOperators } from "adminforth";
11
+ import { I18nBase, getPrimaryLanguageCode, requiresSlavicPlurals, SLAVIC_PLURAL_EXAMPLES } from './base.js';
11
12
  import { z } from "zod";
12
13
  import iso6391 from 'iso-639-1';
13
14
  const updateFieldBodySchema = z.object({
@@ -28,61 +29,10 @@ import path from 'path';
28
29
  import fs from 'fs-extra';
29
30
  import chokidar from 'chokidar';
30
31
  import { AsyncQueue } from '@sapphire/async-queue';
31
- import getFlagEmoji from 'country-flag-svg';
32
32
  import { parse } from 'bcp-47';
33
33
  import pLimit from 'p-limit';
34
34
  import { afLogger } from "adminforth";
35
35
  const processFrontendMessagesQueue = new AsyncQueue();
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
- const countryISO31661ByLangISO6391 = {
50
- en: 'us', // English → United States
51
- zh: 'cn', // Chinese → China
52
- hi: 'in', // Hindi → India
53
- ar: 'sa', // Arabic → Saudi Arabia
54
- ko: 'kr', // Korean → South Korea
55
- ja: 'jp', // Japanese → Japan
56
- uk: 'ua', // Ukrainian → Ukraine
57
- ur: 'pk', // Urdu → Pakistan
58
- sr: 'rs', // Serbian → Serbia
59
- da: 'dk' // Danish → Denmark
60
- };
61
- function getCountryCodeFromLangCode(lang) {
62
- const [langCode, region] = String(lang).split('-');
63
- if (region && /^[A-Z]{2}$/.test(region)) {
64
- return region.toLowerCase();
65
- }
66
- return countryISO31661ByLangISO6391[langCode] || langCode;
67
- }
68
- function getPrimaryLanguageCode(langCode) {
69
- return String(langCode).split('-')[0];
70
- }
71
- function isValidSupportedLanguageTag(langCode) {
72
- try {
73
- const schema = parse(String(langCode), {
74
- normalize: true,
75
- warning: (reason, code, offset) => {
76
- console.warn(`Warning in validating language tag ${langCode}: reason=${reason}, code=${code}, offset=${offset}`);
77
- }
78
- });
79
- const isValid = schema.language.length === 2;
80
- return isValid;
81
- }
82
- catch (e) {
83
- return false;
84
- }
85
- }
86
36
  class CachingAdapterMemory {
87
37
  constructor() {
88
38
  this.cache = {};
@@ -121,13 +71,11 @@ class AiTranslateError extends Error {
121
71
  this.name = 'AiTranslateError';
122
72
  }
123
73
  }
124
- export default class I18nPlugin extends AdminForthPlugin {
74
+ export default class I18nPlugin extends I18nBase {
125
75
  constructor(options) {
126
- super(options, import.meta.url);
127
- this.options = options;
76
+ super(options);
128
77
  this.cache = new CachingAdapterMemory();
129
78
  this.trFieldNames = {};
130
- this.primaryLanguage = options.primaryLanguage || 'en';
131
79
  }
132
80
  computeCompletedFieldValue(record) {
133
81
  return __awaiter(this, void 0, void 0, function* () {
@@ -169,12 +117,6 @@ export default class I18nPlugin extends AdminForthPlugin {
169
117
  this.adminforth.config.componentsToExplicitRegister.push({
170
118
  file: this.componentPath('TranslationJobViewComponent.vue')
171
119
  });
172
- // validate each supported language: ISO 639-1 or BCP-47 with region (e.g., en-GB)
173
- this.options.supportedLanguages.forEach((lang) => {
174
- if (!isValidSupportedLanguageTag(lang)) {
175
- throw new Error(`Invalid language code ${lang}. Use ISO 639-1 (e.g., 'en') or BCP-47 with region (e.g., 'en-GB').`);
176
- }
177
- });
178
120
  if (this.options.translateLangAsBCP47Code) {
179
121
  for (const [lang, bcp47] of Object.entries(this.options.translateLangAsBCP47Code)) {
180
122
  if (!this.options.supportedLanguages.includes(lang)) {
@@ -293,31 +235,9 @@ export default class I18nPlugin extends AdminForthPlugin {
293
235
  type: AdminForthDataTypes.BOOLEAN,
294
236
  });
295
237
  }
296
- const compMeta = {
297
- brandSlug: adminforth.config.customization.brandNameSlug,
298
- pluginInstanceId: this.pluginInstanceId,
299
- primaryLanguage: this.primaryLanguage,
300
- afOrder: this.options.loginPageLanguageSelectorOrder || 0,
301
- supportedLanguages: this.options.supportedLanguages.map(lang => ({
302
- code: lang,
303
- // lang name on on language native name
304
- name: iso6391.getNativeName(getPrimaryLanguageCode(lang)),
305
- }))
306
- };
307
238
  // add underLogin component
308
239
  if (!this.externalAppOnly) {
309
- adminforth.config.customization.loginPageInjections.underLoginButton.push({
310
- file: this.componentPath('LanguageUnderLogin.vue'),
311
- meta: compMeta
312
- });
313
- (adminforth.config.customization.globalInjections.userMenu).push({
314
- file: this.componentPath('LanguageInUserMenu.vue'),
315
- meta: compMeta
316
- });
317
- adminforth.config.customization.globalInjections.everyPageBottom.push({
318
- file: this.componentPath('LanguageEveryPageLoader.vue'),
319
- meta: compMeta
320
- });
240
+ this.injectLanguageSelectors(adminforth);
321
241
  }
322
242
  // disable create allowedActions for translations
323
243
  resourceConfig.options.allowedActions.create = false;
@@ -628,14 +548,14 @@ export default class I18nPlugin extends AdminForthPlugin {
628
548
  const lang = langIsoCode;
629
549
  const primaryLang = getPrimaryLanguageCode(lang);
630
550
  const langName = iso6391.getName(primaryLang);
631
- const requestSlavicPlurals = Object.keys(SLAVIC_PLURAL_EXAMPLES).includes(primaryLang) && plurals;
551
+ const requestSlavicPlurals = requiresSlavicPlurals(lang) && plurals;
632
552
  const region = langSchema.region;
633
553
  const basePrompt = `
634
554
  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.
635
555
 
636
556
 
637
557
  ${region ? `Use the regional conventions for ${langCode} (region ${region}), including spelling, punctuation, and formatting.` : ''}
638
- ${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]}"` : ''}
558
+ ${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]}"` : ''}
639
559
  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:
640
560
  \`\`\`json
641
561
  {
@@ -895,13 +815,7 @@ export default class I18nPlugin extends AdminForthPlugin {
895
815
  throw new Error(`Category 'frontend' is reserved for frontend messages, use any other category for backend messages`);
896
816
  }
897
817
  // console.log('🪲tr', msg, category, lang);
898
- // if lang is not supported, fallback to primaryLanguage, then to english
899
- if (!this.options.supportedLanguages.includes(lang)) {
900
- lang = this.primaryLanguage; // fallback to primary language first
901
- if (!this.options.supportedLanguages.includes(lang)) {
902
- lang = 'en'; // final fallback to english
903
- }
904
- }
818
+ lang = this.resolveLanguage(lang);
905
819
  let result;
906
820
  // try to get translation from cache
907
821
  const cacheKey = `${resourceConfig.resourceId}:${category}:${lang}:${msg}`;
@@ -935,52 +849,10 @@ export default class I18nPlugin extends AdminForthPlugin {
935
849
  // cache so even if key does not exist, we will not hit database
936
850
  yield this.cache.set(cacheKey, result);
937
851
  }
938
- // if msg has '|' in it, then we need to aplly pluralization
939
- if (msg.includes('|') && pluralizationNumber !== undefined) {
940
- result = this.applyPluralization(result, pluralizationNumber, lang);
941
- }
942
- if (params) {
943
- for (const [key, value] of Object.entries(params)) {
944
- if (result) {
945
- result = result.replace(`{${key}}`, value);
946
- }
947
- }
948
- }
949
- return result;
852
+ return this.formatTranslation(msg, result, lang, params, pluralizationNumber);
950
853
  });
951
854
  }
952
855
  }
953
- applyPluralization(str, number, lang) {
954
- const choices = str.split('|');
955
- const choicesLength = choices.length;
956
- if (choicesLength > 2) {
957
- // this is slavic pluralization
958
- return choices[this.slavicPluralRule(number, choicesLength)];
959
- }
960
- else {
961
- return number === 1 ? choices[0] : choices[1];
962
- }
963
- }
964
- // taken from here https://vue-i18n.intlify.dev/guide/essentials/pluralization.html#custom-pluralization
965
- slavicPluralRule(choice, choicesLength) {
966
- if (choice === 0) {
967
- return 0;
968
- }
969
- const teen = choice > 10 && choice < 20;
970
- const endsWithOne = choice % 10 === 1;
971
- if (!teen && endsWithOne) {
972
- return 1;
973
- }
974
- if (!teen && choice % 10 >= 2 && choice % 10 <= 4) {
975
- return 2;
976
- }
977
- return choicesLength < 4 ? 2 : 3;
978
- }
979
- instanceUniqueRepresentation(pluginOptions) {
980
- // optional method to return unique string representation of plugin instance.
981
- // Needed if plugin can have multiple instances on one resource
982
- return `single`;
983
- }
984
856
  getCategoryTranslations(category, lang) {
985
857
  return __awaiter(this, void 0, void 0, function* () {
986
858
  const resource = this.adminforth.resource(this.resourceConfig.resourceId);
@@ -999,22 +871,6 @@ export default class I18nPlugin extends AdminForthPlugin {
999
871
  return translations;
1000
872
  });
1001
873
  }
1002
- languagesList() {
1003
- return __awaiter(this, void 0, void 0, function* () {
1004
- return Promise.all(this.options.supportedLanguages.map((lang) => __awaiter(this, void 0, void 0, function* () {
1005
- const flagEmoji = typeof getFlagEmoji === 'function'
1006
- ? getFlagEmoji
1007
- : getFlagEmoji.default;
1008
- return {
1009
- code: lang,
1010
- nameOnNative: iso6391.getNativeName(getPrimaryLanguageCode(lang)),
1011
- nameEnglish: iso6391.getName(getPrimaryLanguageCode(lang)),
1012
- emojiFlag: getCountryCodeFromLangCode(lang).toUpperCase().replace(/./g, char => String.fromCodePoint(char.charCodeAt(0) + 127397)),
1013
- svgFlagB64: yield flagEmoji(getCountryCodeFromLangCode(lang)),
1014
- };
1015
- })));
1016
- });
1017
- }
1018
874
  feedCategoryTranslations(messages, category) {
1019
875
  return __awaiter(this, void 0, void 0, function* () {
1020
876
  const adminforth = this.adminforth;
@@ -1049,17 +905,7 @@ export default class I18nPlugin extends AdminForthPlugin {
1049
905
  });
1050
906
  }
1051
907
  setupEndpoints(server) {
1052
- server.endpoint({
1053
- method: 'GET',
1054
- path: `/plugin/${this.pluginInstanceId}/frontend_messages`,
1055
- noAuth: true,
1056
- handler: (_a) => __awaiter(this, [_a], void 0, function* ({ query }) {
1057
- const lang = query.lang;
1058
- // form map of translations
1059
- const translations = yield this.getCategoryTranslations('frontend', lang);
1060
- return translations;
1061
- })
1062
- });
908
+ super.setupEndpoints(server);
1063
909
  const lock = new RAMLock();
1064
910
  server.endpoint({
1065
911
  method: 'POST',
@@ -1203,3 +1049,4 @@ export default class I18nPlugin extends AdminForthPlugin {
1203
1049
  });
1204
1050
  }
1205
1051
  }
1052
+ export { I18nBase, getCountryCodeFromLangCode, getPrimaryLanguageCode, isValidSupportedLanguageTag, requiresSlavicPlurals, SOURCE_LANGUAGE, PLURAL_SEPARATOR, SLAVIC_PLURAL_FORMS } from './base.js';
package/dist/types.d.ts CHANGED
@@ -5,8 +5,24 @@ import { type PluginsCommonOptions } from "adminforth";
5
5
  type Alpha2Code = keyof typeof iso31661Alpha2ToAlpha3;
6
6
  type Bcp47LanguageTag = `${LanguageCode}-${Alpha2Code}`;
7
7
  export type SupportedLanguage = LanguageCode | Bcp47LanguageTag;
8
- export interface PluginOptions extends PluginsCommonOptions {
8
+ /**
9
+ * Options shared by all i18n plugins built on top of I18nBase
10
+ */
11
+ export interface I18nBaseOptions extends PluginsCommonOptions {
9
12
  supportedLanguages: SupportedLanguage[];
13
+ /**
14
+ * Language selector on login page order
15
+ */
16
+ loginPageLanguageSelectorOrder?: number;
17
+ /**
18
+ * Primary language for the application. This is the default language shown to users.
19
+ * English is always used as the source language for translations, even if primaryLanguage is different.
20
+ * When a translation is missing for the primaryLanguage, English will be shown as fallback.
21
+ * Defaults to 'en' if not specified.
22
+ */
23
+ primaryLanguage?: SupportedLanguage;
24
+ }
25
+ export interface PluginOptions extends I18nBaseOptions {
10
26
  /**
11
27
  * Each translation string will be stored in a separate field, you can remap it to existing columns using this option
12
28
  * By default it will assume field are named like `${lang_code}_string` (e.g. 'en_string', 'uk_string', 'ja_string', 'fr_string')
@@ -18,10 +34,6 @@ export interface PluginOptions extends PluginsCommonOptions {
18
34
  * We recommend to put index on this column
19
35
  */
20
36
  categoryFieldName: string;
21
- /**
22
- * Language selector on login page order
23
- */
24
- loginPageLanguageSelectorOrder?: number;
25
37
  /**
26
38
  * Optional source field to store e.g. file name where it first was captured
27
39
  */
@@ -46,13 +58,6 @@ export interface PluginOptions extends PluginsCommonOptions {
46
58
  * it should be a JSON field (underlyng database type should be TEXT or JSON)
47
59
  */
48
60
  reviewedCheckboxesFieldName?: string;
49
- /**
50
- * Primary language for the application. This is the default language shown to users.
51
- * English is always used as the source language for translations, even if primaryLanguage is different.
52
- * When a translation is missing for the primaryLanguage, English will be shown as fallback.
53
- * Defaults to 'en' if not specified.
54
- */
55
- primaryLanguage?: SupportedLanguage;
56
61
  /**
57
62
  * Ask translator to treat some code from supportedLanguages as exact BCP47 tag. Read docs for details.
58
63
  * key - one of the values form supportedLanguages, value -BCP47 tag
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, getPrimaryLanguageCode, requiresSlavicPlurals, SLAVIC_PLURAL_EXAMPLES } 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,20 +89,15 @@ 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
103
  async computeCompletedFieldValue(record: any) {
@@ -201,13 +139,6 @@ export default class I18nPlugin extends AdminForthPlugin {
201
139
  }
202
140
  );
203
141
 
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
142
  if (this.options.translateLangAsBCP47Code) {
212
143
  for (const [lang, bcp47] of Object.entries(this.options.translateLangAsBCP47Code)) {
213
144
  if (!this.options.supportedLanguages.includes(lang as SupportedLanguage)) {
@@ -341,35 +272,9 @@ export default class I18nPlugin extends AdminForthPlugin {
341
272
  });
342
273
  }
343
274
 
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
275
  // add underLogin component
358
276
  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
- });
277
+ this.injectLanguageSelectors(adminforth);
373
278
  }
374
279
 
375
280
  // disable create allowedActions for translations
@@ -746,14 +651,14 @@ export default class I18nPlugin extends AdminForthPlugin {
746
651
  const lang = langIsoCode;
747
652
  const primaryLang = getPrimaryLanguageCode(lang);
748
653
  const langName = iso6391.getName(primaryLang);
749
- const requestSlavicPlurals = Object.keys(SLAVIC_PLURAL_EXAMPLES).includes(primaryLang) && plurals;
654
+ const requestSlavicPlurals = requiresSlavicPlurals(lang) && plurals;
750
655
  const region = langSchema.region;
751
656
  const basePrompt = `
752
657
  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
658
 
754
659
 
755
660
  ${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]}"` : ''}
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]}"` : ''}
757
662
  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
663
  \`\`\`json
759
664
  {
@@ -1102,13 +1007,7 @@ export default class I18nPlugin extends AdminForthPlugin {
1102
1007
  }
1103
1008
  // console.log('🪲tr', msg, category, lang);
1104
1009
 
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
- }
1010
+ lang = this.resolveLanguage(lang);
1112
1011
 
1113
1012
  let result;
1114
1013
  // try to get translation from cache
@@ -1143,59 +1042,11 @@ export default class I18nPlugin extends AdminForthPlugin {
1143
1042
  // cache so even if key does not exist, we will not hit database
1144
1043
  await this.cache.set(cacheKey, result);
1145
1044
  }
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);
1149
- }
1150
-
1151
- if (params) {
1152
- for (const [key, value] of Object.entries(params)) {
1153
- if (result) {
1154
- result = result.replace(`{${key}}`, value);
1155
- }
1156
- }
1157
- }
1158
- return result;
1045
+ return this.formatTranslation(msg, result, lang, params, pluralizationNumber);
1159
1046
  }
1160
1047
  }
1161
1048
  }
1162
1049
 
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
1189
- }
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
- }
1198
-
1199
1050
  async getCategoryTranslations(category: string, lang: string): Promise<Record<string, string>> {
1200
1051
  const resource = this.adminforth.resource(this.resourceConfig.resourceId);
1201
1052
  const cacheKey = `${this.resourceConfig.resourceId}:${category}:${lang}`;
@@ -1213,30 +1064,6 @@ export default class I18nPlugin extends AdminForthPlugin {
1213
1064
  return translations;
1214
1065
  }
1215
1066
 
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
1067
  async feedCategoryTranslations(messages: {
1241
1068
  en_string: string;
1242
1069
  source: string;
@@ -1279,19 +1106,7 @@ export default class I18nPlugin extends AdminForthPlugin {
1279
1106
 
1280
1107
 
1281
1108
  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
- });
1109
+ super.setupEndpoints(server);
1295
1110
 
1296
1111
  const lock = new RAMLock();
1297
1112
 
@@ -1452,3 +1267,6 @@ export default class I18nPlugin extends AdminForthPlugin {
1452
1267
  }
1453
1268
 
1454
1269
  }
1270
+
1271
+ export { I18nBase, getCountryCodeFromLangCode, getPrimaryLanguageCode, isValidSupportedLanguageTag, requiresSlavicPlurals, SOURCE_LANGUAGE, PLURAL_SEPARATOR, SLAVIC_PLURAL_FORMS } from './base.js';
1272
+ 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.4.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