@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 +239 -0
- package/build.log +1 -1
- package/dist/base.d.ts +51 -0
- package/dist/base.js +203 -0
- package/dist/index.d.ts +4 -16
- package/dist/index.js +11 -164
- package/dist/types.d.ts +17 -12
- package/index.ts +13 -195
- package/package.json +1 -1
- package/types.ts +20 -14
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
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
|
|
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 {
|
|
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
|
|
74
|
+
export default class I18nPlugin extends I18nBase {
|
|
125
75
|
constructor(options) {
|
|
126
|
-
super(options
|
|
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
|
-
|
|
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 =
|
|
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[
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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, {
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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[
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
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
|
-
|
|
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
|