Internationalization (i18n)
This plugin allows you translate your AdminForth application to multiple languages. Main features:
- Stores all translation strings in your application in a single AdminForth resource. You can set allowed actions only to Developers/Translators role if you don't want other users to see/edit the translations.
- Supports AI completion adapters to help with translations. For example, you can use OpenAI ChatGPT to generate translations. Supports correct pluralization, even for Slavic languages.
- Supports any number of languages.
Under the hood it uses vue-i18n library and provides several additional facilities to make the translation process easier.
Installation
To install the plugin:
npm install @adminforth/i18n --save
npm install @adminforth/completion-adapter-open-ai-chat-gpt --save
For example lets add translations to next 4 languages: Ukrainian, Japanese, French, Spanish. Also we will support basic translation for English.
Add a model for translations, if you are using prisma, add something like this:
model translations {
id String @id
en_string String
created_at DateTime
uk_string String? // translation for Ukrainian language
ja_string String? // translation for Japanese language
fr_string String? // translation for French language
es_string String? // translation for Spanish language
category String
source String?
completedLangs String?
// we need both indexes on en_string+category and separately on category
@@index([en_string, category])
@@index([category])
@@index([completedLangs])
}
If you want more languages, just add more fields like uk_string
, ja_string
, fr_string
, es_string
to the model.
Next, add resource for translations:
import AdminForth, { AdminForthDataTypes, AdminForthResourceInput } from "adminforth";
import CompletionAdapterOpenAIChatGPT from "@adminforth/completion-adapter-open-ai-chat-gpt";
import I18nPlugin from "@adminforth/i18n";
import { v1 as uuid } from "uuid";
export default {
dataSource: "maindb",
table: "translations",
resourceId: "translations",
label: "Translations",
recordLabel: (r: any) => `✍️ ${r.en_string}`,
plugins: [
new I18nPlugin({
supportedLanguages: ['en', 'uk', 'ja', 'fr'],
// names of the fields in the resource which will store translations
translationFieldNames: {
en: 'en_string',
uk: 'uk_string',
ja: 'ja_string',
fr: 'fr_string',
},
// name of the field which will store the category of the string
// this helps to categorize strings and deliver them efficiently
categoryFieldName: 'category',
// optional field to store the source (e.g. source file name)
sourceFieldName: 'source',
// optional field store list of completed translations
// will hel to filter out incomplete translations
completedFieldName: 'completedLangs',
completeAdapter: new CompletionAdapterOpenAIChatGPT({
openAiApiKey: process.env.OPENAI_API_KEY as string,
model: 'gpt-4o-mini',
expert: {
// for UI translation it is better to lower down the temperature from default 0.7. Less creative and more accurate
temperature: 0.5,
},
}),
}),
],
options: {
listPageSize: 30,
},
columns: [
{
name: "id",
fillOnCreate: ({ initialRecord, adminUser }: any) => uuid(),
primaryKey: true,
showIn: { all: false },
},
{
name: "en_string",
type: AdminForthDataTypes.STRING,
label: 'English',
},
{
name: "created_at",
fillOnCreate: ({ initialRecord, adminUser }: any) => new Date().toISOString(),
showIn: {
all: false,
show: true,
edit: false,
},
},
{
name: "uk_string",
type: AdminForthDataTypes.STRING,
label: 'Ukrainian',
},
{
name: "ja_string",
type: AdminForthDataTypes.STRING,
label: 'Japanese',
},
{
name: "fr_string",
type: AdminForthDataTypes.STRING,
label: 'French',
},
{
name: "completedLangs",
},
{
name: "source",
showIn: {
list: false,
edit: false,
create: false,
},
type: AdminForthDataTypes.STRING,
},
{
name: "category",
showIn: {
edit: false,
create: false,
},
type: AdminForthDataTypes.STRING,
}
],
} as AdminForthResourceInput;
Add OPENAI_API_KEY
to your .env
file:
OPENAI_API_KEY=your_openai_api_key
Also add the resource to main file and add menu item in ./index.ts
:
import translations from "./resources/translations";
...
const adminForth = new AdminForth({
...
resources: [
...
translations,
],
menu: [
...
{
label: 'Translations',
icon: 'material-symbols:translate',
resourceId: 'translations',
},
],
...
});
This is it, now you should restart your app and see the translations resource in the menu.
You can add translations for each language manually or use Bulk actions to generate translations with AI completion adapter.
For simplicity you can also use filter to get only untranslated strings and complete them one by one (filter name "Fully translated" in the filter).
Translation for custom components
To translate custom components, you should simply wrap all strings in $t function. For example:
Now create file CustomLoginFooter.vue
in the custom
folder of your project:
<template>
<div class="text-center text-gray-500 text-sm mt-4">
By logging in, you agree to our <a href="#" class="text-blue-500">Terms of Service</a> and <a href="#" class="text-blue-500">Privacy Policy</a>
{{$t('By logging in, you agree to our')}} <a href="#" class="text-blue-500">{{$t('Terms of Service')}}</a> {{$t('and')}} <a href="#" class="text-blue-500">{{$t('Privacy Policy')}}</a>
</div>
</template>
Variables in frontend translations
You can use variables in translations in same way like you would do it with vue-i18n library.
This is generally helps to understand the context of the translation for AI completion adapters and simplifies the translation process, even if done manually.
For example if you have string "Showing 1 to 10 of 100 entries" you can of course simply do
{{ $t('Showing')}} {{from}} {{$t('to')}} {{to}} {{$t('of')}} {{total}} {{$t('entries') }}
And it will form 4 translation strings. But it is much better to have it as single string with variables like this:
{{ $t('Showing {from} to {to} of {total} entries', { from, to, total } ) }}
For example, let's add user greeting to the header.
<template>
<div class="flex items-center justify-between p-4 bg-white shadow-md">
<div class="text-lg font-semibold text-gray-800">
{{ $t('Welcome, {name}', { name: adminUser.username }) }}
</div>
</div>
</template>
<script setup lang="ts">
import type { AdminForthResourceColumnCommon, AdminForthResourceCommon, AdminUser } from '@/types/Common';
const props = defineProps<{
column: AdminForthResourceColumnCommon;
record: any;
meta: any;
resource: AdminForthResourceCommon;
adminUser: AdminUser
}>();
</script>
How to use such component
const adminForth = new AdminForth({
...
customization{
globalInjections: {
header: {
file: '@@/Header.vue',
},
}
},
...
});