Skip to content

Commit 130e0e1

Browse files
committed
i 18n progress
1 parent 8636a03 commit 130e0e1

File tree

14 files changed

+166
-73
lines changed

14 files changed

+166
-73
lines changed

adminforth/dataConnectors/sqlite.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,16 @@ class SQLiteConnector extends AdminForthBaseConnector implements IAdminForthData
158158
placeholder = `LOWER(?)`;
159159
field = `LOWER(${f.field})`;
160160
operator = 'LIKE';
161+
} else if (f.operator == AdminForthFilterOperators.NE) {
162+
if (f.value === null) {
163+
operator = 'IS NOT';
164+
placeholder = 'NULL';
165+
} else {
166+
// for not equal, we need to add a null check
167+
// because nullish field will not match != value
168+
placeholder = `${placeholder} OR ${field} IS NULL)`;
169+
field = `(${field}`;
170+
}
161171
}
162172
163173
return `${field} ${operator} ${placeholder}`

adminforth/modules/restApi.ts

Lines changed: 64 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
206206
noAuth: true,
207207
method: 'GET',
208208
path: '/get_public_config',
209-
handler: async ({ body }) => {
209+
handler: async ({ tr }) => {
210210

211211
// TODO we need to remove this method and make get_config to return public and private parts for logged in user and only public for not logged in
212212

@@ -225,7 +225,7 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
225225
loginBackgroundPosition: this.adminforth.config.auth.loginBackgroundPosition,
226226
title: this.adminforth.config.customization?.title,
227227
demoCredentials: this.adminforth.config.auth.demoCredentials,
228-
loginPromptHTML: this.adminforth.config.auth.loginPromptHTML,
228+
loginPromptHTML: await tr(this.adminforth.config.auth.loginPromptHTML, 'system.loginPromptHTML'),
229229
loginPageInjections: this.adminforth.config.customization.loginPageInjections,
230230
rememberMeDays: this.adminforth.config.auth.rememberMeDays,
231231
};
@@ -236,7 +236,7 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
236236
server.endpoint({
237237
method: 'GET',
238238
path: '/get_base_config',
239-
handler: async ({input, adminUser, cookies}): Promise<GetBaseConfigResponse>=> {
239+
handler: async ({input, adminUser, cookies, tr}): Promise<GetBaseConfigResponse>=> {
240240
let username = ''
241241
let userFullName = ''
242242

@@ -323,6 +323,25 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
323323
userFullnameField: this.adminforth.config.auth.userFullNameField,
324324
}
325325

326+
// translate menu labels
327+
const translateRoutines: Promise<void>[] = [];
328+
newMenu.forEach((menuItem) => {
329+
translateRoutines.push((async () => {
330+
if (menuItem.label) {
331+
menuItem.label = await tr(menuItem.label, `menu.${menuItem._itemId}`);
332+
}
333+
})())
334+
if (menuItem.children) {
335+
menuItem.children.forEach((child) => {
336+
translateRoutines.push((async () => {
337+
if (child.label) {
338+
child.label = await tr(child.label, `menu.${child._itemId}`);
339+
}
340+
})())
341+
})
342+
}
343+
});
344+
await Promise.all(translateRoutines);
326345

327346
// strip all backendOnly fields or not described in adminForth fields from dbUser
328347
// (when user defines column and does not set backendOnly, we assume it is not backendOnly)
@@ -423,19 +442,51 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
423442
})
424443
);
425444

445+
// translate
446+
const translateRoutines: Record<string, Promise<string>> = {};
447+
translateRoutines.resLabel = tr(resource.label, `resource.${resource.resourceId}`);
448+
resource.columns.forEach((col, i) => {
449+
translateRoutines[`resCol${i}`] = tr(col.label, `resource.${resource.resourceId}`);
450+
})
451+
allowedBulkActions.forEach((action, i) => {
452+
if (action.label) {
453+
translateRoutines[`bulkAction${i}`] = tr(action.label, `resource.${resource.resourceId}`);
454+
}
455+
if (action.confirm) {
456+
translateRoutines[`bulkActionConfirm${i}`] = tr(action.confirm, `resource.${resource.resourceId}`);
457+
}
458+
});
459+
460+
const translated: Record<string, string> = {};
461+
await Promise.all(
462+
Object.entries(translateRoutines).map(async ([key, value]) => {
463+
translated[key] = await value;
464+
})
465+
);
466+
467+
console.log('🗣️translated', translated);
468+
469+
426470
const toReturn = {
427471
...resource,
428-
columns: await Promise.all(
429-
resource.columns.map(async (col) => {
472+
label: translated.resLabel,
473+
columns: resource.columns.map(
474+
(col, i) => {
430475
return {
431476
...col,
432-
label: await tr(col.label, `resource.${resource.resourceId}`),
477+
label: translated[`resCol${i}`],
433478
}
434-
})
479+
}
435480
),
436481
options: {
437482
...resource.options,
438-
bulkActions: allowedBulkActions,
483+
bulkActions: allowedBulkActions.map(
484+
(action, i) => ({
485+
...action,
486+
label: action.label ? translated[`bulkAction${i}`] : action.label,
487+
confirm: action.confirm ? translated[`bulkActionConfirm${i}`] : action.confirm,
488+
})
489+
),
439490
allowedActions,
440491
}
441492
}
@@ -865,11 +916,11 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
865916
server.endpoint({
866917
method: 'POST',
867918
path: '/start_bulk_action',
868-
handler: async ({ body, adminUser }) => {
919+
handler: async ({ body, adminUser, tr }) => {
869920
const { resourceId, actionId, recordIds } = body;
870921
const resource = this.adminforth.config.resources.find((res) => res.resourceId == resourceId);
871922
if (!resource) {
872-
return { error: `Resource '${resourceId}' not found` };
923+
return { error: await tr(`Resource {resourceId} not found`, 'errors', { resourceId }) };
873924
}
874925
const { allowedActions } = await interpretResource(
875926
adminUser,
@@ -881,16 +932,16 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
881932

882933
const action = resource.options.bulkActions.find((act) => act.id == actionId);
883934
if (!action) {
884-
return { error: `Action '${actionId}' not found` };
935+
return { error: await tr(`Action {actionId} not found`, 'errors', { actionId }) };
885936
}
886937

887938
if (action.allowed) {
888939
const execAllowed = await action.allowed({ adminUser, resource, selectedIds: recordIds, allowedActions });
889940
if (!execAllowed) {
890-
return { error: `Action '${actionId}' is not allowed` };
941+
return { error: await tr(`Action {actionId} not allowed`, 'errors', { actionId }) };
891942
}
892943
}
893-
const response = await action.action({selectedIds: recordIds, adminUser, resource});
944+
const response = await action.action({selectedIds: recordIds, adminUser, resource, tr});
894945

895946
return {
896947
actionId,

adminforth/plugins/i18n/custom/LanguageInUserMenu.vue

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
v-model="selectedLanguage"
55
:options="options"
66
:placeholder="$t('Select language')"
7-
@change="changeLanguage"
87
>
98
<template #item="{ option }">
109
<span class="mr-1">

adminforth/plugins/i18n/custom/LanguageUnderLogin.vue

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
v-model="selectedLanguage"
55
:options="options"
66
:placeholder="$t('Select language')"
7-
@change="changeLanguage"
87
>
98
<template #item="{ option }">
109
<span class="mr-1">

adminforth/plugins/i18n/custom/langCommon.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ setInterval(() => {
2323
// i18n is vue-i18n instance
2424
export async function setLang({ setLocaleMessage, locale }: any, pluginInstanceId: string, langIso: string) {
2525

26-
2726
if (!messagesCache[langIso]) {
2827
const messages = await callAdminForthApi({
2928
path: `/plugin/${pluginInstanceId}/frontend_messages?lang=${langIso}`,

adminforth/plugins/i18n/index.ts

Lines changed: 40 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ export default class I18N extends AdminForthPlugin {
229229
resourceConfig.hooks.edit.beforeSave.push(async ({ record, oldRecord }: { record: any, oldRecord: any }): Promise<{ ok: boolean, error?: string }> => {
230230
const futureRecord = { ...oldRecord, ...record };
231231

232-
const futureCompletedFieldValue = this.computeCompletedFieldValue(futureRecord);
232+
const futureCompletedFieldValue = await this.computeCompletedFieldValue(futureRecord);
233233

234234
record[this.options.completedFieldName] = futureCompletedFieldValue;
235235
return { ok: true };
@@ -246,7 +246,7 @@ export default class I18N extends AdminForthPlugin {
246246
if (fullyTranslatedFilter) {
247247
// remove it from filters because it is virtual field
248248
query.filters = query.filters.filter((f: any) => f.field !== 'fully_translated');
249-
if (fullyTranslatedFilter.value) {
249+
if (fullyTranslatedFilter.value[0]) {
250250
query.filters.push({
251251
field: this.options.completedFieldName,
252252
value: this.fullCompleatedFieldValue,
@@ -278,16 +278,19 @@ export default class I18N extends AdminForthPlugin {
278278
// if optional `confirm` is provided, user will be asked to confirm action
279279
confirm: 'Are you sure you want to translate selected items?',
280280
state: 'selected',
281-
action: async ({ selectedIds }) => {
281+
action: async ({ selectedIds, tr }) => {
282282
try {
283-
const data = await this.bulkTranslate({ selectedIds });
283+
await this.bulkTranslate({ selectedIds });
284284
} catch (e) {
285285
if (e instanceof AiTranslateError) {
286286
return { ok: false, error: e.message };
287287
}
288288
}
289-
return { ok: true, error: undefined, successMessage: `Translated ${selectedIds.length} items` };
290-
},
289+
return {
290+
ok: true, error: undefined,
291+
successMessage: await tr(`Translated {count} items`, 'frontend', {count: selectedIds.length}),
292+
};
293+
}
291294
}
292295
);
293296
};
@@ -355,7 +358,6 @@ ${
355358
}
356359
\`\`\`
357360
`;
358-
console.log('🪲prompt', prompt);
359361
// call OpenAI
360362
const resp = await this.options.completeAdapter.complete(
361363
prompt,
@@ -385,7 +387,7 @@ ${
385387
// might be several with same en_string
386388
for (const translation of translationsTargeted) {
387389
translation[this.trFieldNames[lang]] = translatedStr;
388-
process.env.HEAVY_DEBUG && console.log(`🪲translated to ${lang} ${translation.en_string}, ${translatedStr}`)
390+
// process.env.HEAVY_DEBUG && console.log(`🪲translated to ${lang} ${translation.en_string}, ${translatedStr}`)
389391
if (!updateStrings[enStr]) {
390392
updateStrings[enStr] = {
391393
updates: {},
@@ -405,7 +407,6 @@ ${
405407
await translateToLang(lang, strings);
406408
}));
407409

408-
console.log('🪲updateStrings', updateStrings);
409410
await Promise.all(
410411
Object.entries(updateStrings).map(
411412
async ([_, { updates, strId }]: [string, { updates: any, category: string, strId: string }]) => {
@@ -427,12 +428,6 @@ ${
427428
}
428429
}
429430

430-
return {
431-
ok: true,
432-
error: undefined,
433-
successMessage: `Translated ${selectedIds.length} items`,
434-
}
435-
436431
}
437432

438433
async processExtractedMessages(adminforth: IAdminForth, filePath: string) {
@@ -521,42 +516,50 @@ ${
521516
// in this plugin we will use plugin to fill the database with missing language messages
522517
this.tryProcessAndWatch(adminforth);
523518

524-
adminforth.tr = async (msg: string, category: string, lang: string): Promise<string> => {
525-
console.log('🪲tr', msg, category, lang);
519+
adminforth.tr = async (msg: string, category: string, lang: string, params): Promise<string> => {
520+
// console.log('🪲tr', msg, category, lang);
526521

527522
// if lang is not supported , throw
528523
if (!this.options.supportedLanguages.includes(lang as LanguageCode)) {
529524
throw new Error(`Language ${lang} is not entered to be supported by requested by browser in request headers accept-language`);
530525
}
531526

527+
let result;
532528
// try to get translation from cache
533529
const cacheKey = `${resourceConfig.resourceId}:${category}:${lang}:${msg}`;
534530
const cached = await this.cache.get(cacheKey);
535531
if (cached) {
536-
return cached;
537-
}
538-
const resource = adminforth.resource(resourceConfig.resourceId);
539-
const translation = await resource.get([Filters.EQ(this.enFieldName, msg), Filters.EQ(this.options.categoryFieldName, category)]);
540-
if (!translation) {
541-
await resource.create({
542-
[this.enFieldName]: msg,
543-
[this.options.categoryFieldName]: category,
544-
});
545-
return msg;
532+
result = cached;
546533
}
534+
if (!result) {
535+
const resource = adminforth.resource(resourceConfig.resourceId);
536+
const translation = await resource.get([Filters.EQ(this.enFieldName, msg), Filters.EQ(this.options.categoryFieldName, category)]);
537+
if (!translation) {
538+
await resource.create({
539+
[this.enFieldName]: msg,
540+
[this.options.categoryFieldName]: category,
541+
});
542+
}
547543

548-
// do this check here, to faster register missing translations
549-
// also not cache it - no sense to cache english strings
550-
if (lang === 'en') {
551-
return msg;
544+
// do this check here, to faster register missing translations
545+
// also not cache it - no sense to cache english strings
546+
if (lang === 'en') {
547+
// set to cache to return faster next time
548+
result = msg;
549+
} else {
550+
result = translation?.[this.trFieldNames[lang]];
551+
if (!result) {
552+
// return english
553+
result = msg;
554+
}
555+
}
556+
await this.cache.set(cacheKey, result);
552557
}
553-
554-
const result = translation[this.trFieldNames[lang]];
555-
if (!result) {
556-
// return english
557-
return msg;
558+
if (params) {
559+
for (const [key, value] of Object.entries(params)) {
560+
result = result.replace(`{${key}}`, value);
561+
}
558562
}
559-
await this.cache.set(cacheKey, result);
560563
return result;
561564
}
562565
}

adminforth/servers/express.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ class ExpressServer implements IExpressHttpServer {
292292
};
293293

294294
const acceptLang = headers['accept-language'];
295-
const tr = (msg: string, category: string): Promise<string> => this.adminforth.tr(msg, category, acceptLang);
295+
const tr = (msg: string, category: string, params: any): Promise<string> => this.adminforth.tr(msg, category, acceptLang, params);
296296
const input = { body, query, headers, cookies, adminUser, response, _raw_express_req: req, _raw_express_res: res, tr};
297297

298298
let output;

adminforth/spa/src/components/ResourceListTable.vue

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -233,13 +233,18 @@
233233
<!-- Help text -->
234234
<span class="text-sm text-gray-700 dark:text-gray-400">
235235
<span v-if="((page || 1) - 1) * pageSize + 1 > totalRows">{{ $t('Wrong Page') }} </span>
236-
<span v-else>{{ $t('Showing') }}&nbsp;</span>
237-
<span class="font-semibold text-gray-900 dark:text-white">
238-
{{ ((page || 1) - 1) * pageSize + 1 }}
239-
</span> {{ $t('to') }} <span class="font-semibold text-gray-900 dark:text-white">
240-
{{ Math.min((page || 1) * pageSize, totalRows) }}
241-
</span> {{ $t('of') }} <span class="font-semibold text-gray-900 dark:text-white">{{
242-
totalRows }}</span> <span class="hidden sm:inline">{{ $t('Entries') }}</span>
236+
<template v-else>
237+
<span class="hidden sm:inline"
238+
v-html="$t('Showing {from} to {to} of {total} Entries', {from: ((page || 1) - 1) * pageSize + 1, to: Math.min((page || 1) * pageSize, totalRows), total: totalRows})
239+
.replace(/\d+/g, '<strong>$&</strong>')">
240+
</span>
241+
<span class="sm:hidden"
242+
v-html="$t('{from} - {to} of {total}', {from: ((page || 1) - 1) * pageSize + 1, to: Math.min((page || 1) * pageSize, totalRows), total: totalRows})
243+
.replace(/\d+/g, '<strong>$&</strong>')
244+
">
245+
</span>
246+
247+
</template>
243248
</span>
244249
</div>
245250
</template>

adminforth/spa/src/i18n.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { createI18n } from 'vue-i18n';
2+
import { createApp } from 'vue';
3+
4+
5+
export function initI18n(app: ReturnType<typeof createApp>) {
6+
const i18n = createI18n({
7+
missing: (locale, key) => {
8+
// very very dirty hack to make work $t("a {key} b", { key: "c" }) as "a c b" when translation is missing
9+
// e.g. relevant for "Showing {from} to {to} of {total} entries" on list page
10+
return key + ' ';
11+
},
12+
})
13+
14+
app.use(i18n);
15+
16+
}

0 commit comments

Comments
 (0)