diff --git a/assets/js/5dd1e9f0.d4b1ffbf.js b/assets/js/5dd1e9f0.d4b1ffbf.js
new file mode 100644
index 000000000..dfd5be086
--- /dev/null
+++ b/assets/js/5dd1e9f0.d4b1ffbf.js
@@ -0,0 +1 @@
+"use strict";(self.webpackChunkadminforth=self.webpackChunkadminforth||[]).push([[5653],{7519:(n,e,t)=>{t.r(e),t.d(e,{assets:()=>l,contentTitle:()=>r,default:()=>u,frontMatter:()=>i,metadata:()=>o,toc:()=>d});var a=t(4848),s=t(8453);const i={},r="Internationalization (i18n)",o={id:"tutorial/Plugins/i18n",title:"Internationalization (i18n)",description:"This plugin allows you translate your AdminForth application to multiple languages.",source:"@site/docs/tutorial/05-Plugins/10-i18n.md",sourceDirName:"tutorial/05-Plugins",slug:"/tutorial/Plugins/i18n",permalink:"/docs/tutorial/Plugins/i18n",draft:!1,unlisted:!1,tags:[],version:"current",sidebarPosition:10,frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Open Signup",permalink:"/docs/tutorial/Plugins/open-signup"},next:{title:"Plugin development guide",permalink:"/docs/tutorial/Advanced/plugin-development"}},l={},d=[{value:"Installation",id:"installation",level:2},{value:"Translation for custom components",id:"translation-for-custom-components",level:2},{value:"Variables in frontend translations",id:"variables-in-frontend-translations",level:3},{value:"HTML in translations",id:"html-in-translations",level:3},{value:"Pluralization",id:"pluralization",level:3},{value:"Limiting access to translating",id:"limiting-access-to-translating",level:2},{value:"Translations in custom APIs",id:"translations-in-custom-apis",level:2},{value:"Translating messaged within bulk action",id:"translating-messaged-within-bulk-action",level:2},{value:"Translating external application",id:"translating-external-application",level:2}];function c(n){const e={a:"a",blockquote:"blockquote",code:"code",h1:"h1",h2:"h2",h3:"h3",img:"img",li:"li",p:"p",pre:"pre",ul:"ul",...(0,s.R)(),...n.components};return(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(e.h1,{id:"internationalization-i18n",children:"Internationalization (i18n)"}),"\n",(0,a.jsx)(e.p,{children:"This plugin allows you translate your AdminForth application to multiple languages.\nMain features:"}),"\n",(0,a.jsxs)(e.ul,{children:["\n",(0,a.jsxs)(e.li,{children:["Stores all translation strings in your application in a single AdminForth resource. You can set ",(0,a.jsx)(e.a,{href:"/docs/tutorial/Customization/limitingAccess/#disable-some-action-based-on-logged-in-user-record-or-role",children:"allowed actions"})," only to Developers/Translators role if you don't want other users to see/edit the translations."]}),"\n",(0,a.jsx)(e.li,{children:"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."}),"\n",(0,a.jsx)(e.li,{children:"Supports any number of languages."}),"\n"]}),"\n",(0,a.jsx)(e.p,{children:"Under the hood it uses vue-i18n library and provides several additional facilities to make the translation process easier."}),"\n",(0,a.jsx)(e.h2,{id:"installation",children:"Installation"}),"\n",(0,a.jsx)(e.p,{children:"To install the plugin:"}),"\n",(0,a.jsx)(e.pre,{children:(0,a.jsx)(e.code,{className:"language-bash",children:"npm install @adminforth/i18n --save\nnpm install @adminforth/completion-adapter-open-ai-chat-gpt --save\n"})}),"\n",(0,a.jsx)(e.p,{children:"For example lets add translations to next 4 languages: Ukrainian, Japanese, French, Spanish. Also we will support basic translation for English."}),"\n",(0,a.jsx)(e.p,{children:"Add a model for translations, if you are using prisma, add something like this:"}),"\n",(0,a.jsx)(e.pre,{children:(0,a.jsx)(e.code,{className:"language-ts",metastring:"title='./schema.prisma'",children:"model translations {\n id String @id\n en_string String\n created_at DateTime\n uk_string String? // translation for Ukrainian language\n ja_string String? // translation for Japanese language\n fr_string String? // translation for French language\n es_string String? // translation for Spanish language\n category String\n source String?\n completedLangs String?\n \n // we need both indexes on en_string+category and separately on category\n @@index([en_string, category])\n @@index([category])\n}\n"})}),"\n",(0,a.jsxs)(e.p,{children:["If you want more languages, just add more fields like ",(0,a.jsx)(e.code,{children:"uk_string"}),", ",(0,a.jsx)(e.code,{children:"ja_string"}),", ",(0,a.jsx)(e.code,{children:"fr_string"}),", ",(0,a.jsx)(e.code,{children:"es_string"})," to the model."]}),"\n",(0,a.jsx)(e.p,{children:"Next, add resource for translations:"}),"\n",(0,a.jsx)(e.pre,{children:(0,a.jsx)(e.code,{className:"language-ts",metastring:"title='./resources/translations.ts'",children:"\nimport AdminForth, { AdminForthDataTypes, AdminForthResourceInput } from \"adminforth\";\nimport CompletionAdapterOpenAIChatGPT from \"@adminforth/completion-adapter-open-ai-chat-gpt\";\nimport I18nPlugin from \"@adminforth/i18n\";\nimport { v1 as uuid } from \"uuid\";\n\n\nexport default {\n dataSource: \"maindb\",\n table: \"translations\",\n resourceId: \"translations\",\n label: \"Translations\",\n\n recordLabel: (r: any) => `\u270d\ufe0f ${r.en_string}`,\n plugins: [\n new I18nPlugin({\n supportedLanguages: ['en', 'uk', 'ja', 'fr'],\n\n // names of the fields in the resource which will store translations\n translationFieldNames: {\n en: 'en_string',\n uk: 'uk_string',\n ja: 'ja_string',\n fr: 'fr_string',\n },\n\n // name of the field which will store the category of the string\n // this helps to categorize strings and deliver them efficiently\n categoryFieldName: 'category',\n\n // optional field to store the source (e.g. source file name)\n sourceFieldName: 'source',\n\n // optional field store list of completed translations\n // will hel to filter out incomplete translations\n completedFieldName: 'completedLangs',\n\n completeAdapter: new CompletionAdapterOpenAIChatGPT({\n openAiApiKey: process.env.OPENAI_API_KEY as string,\n model: 'gpt-4o-mini',\n expert: {\n // for UI translation it is better to lower down the temperature from default 0.7. Less creative and more accurate\n temperature: 0.5,\n },\n }),\n }),\n\n ],\n options: {\n listPageSize: 30,\n },\n columns: [\n {\n name: \"id\",\n fillOnCreate: ({ initialRecord, adminUser }: any) => uuid(),\n primaryKey: true,\n showIn: [],\n },\n {\n name: \"en_string\",\n type: AdminForthDataTypes.STRING,\n label: 'English',\n },\n {\n name: \"created_at\",\n fillOnCreate: ({ initialRecord, adminUser }: any) => new Date().toISOString(),\n },\n {\n name: \"uk_string\",\n type: AdminForthDataTypes.STRING,\n label: 'Ukrainian',\n },\n {\n name: \"ja_string\",\n type: AdminForthDataTypes.STRING,\n label: 'Japanese',\n },\n {\n name: \"fr_string\",\n type: AdminForthDataTypes.STRING,\n label: 'French',\n },\n {\n name: \"completedLangs\",\n },\n {\n name: \"source\",\n showIn: ['filter', 'show'],\n type: AdminForthDataTypes.STRING,\n },\n {\n name: \"category\",\n showIn: ['filter', 'show', 'list'],\n type: AdminForthDataTypes.STRING,\n }\n ],\n} as AdminForthResourceInput;\n"})}),"\n",(0,a.jsxs)(e.p,{children:["Add ",(0,a.jsx)(e.code,{children:"OPENAI_API_KEY"})," to your ",(0,a.jsx)(e.code,{children:".env"})," file:"]}),"\n",(0,a.jsx)(e.pre,{children:(0,a.jsx)(e.code,{className:"language-bash",children:"OPENAI_API_KEY=your_openai_api_key\n"})}),"\n",(0,a.jsxs)(e.p,{children:["Also add the resource to main file and add menu item in ",(0,a.jsx)(e.code,{children:"./index.ts"}),":"]}),"\n",(0,a.jsx)(e.pre,{children:(0,a.jsx)(e.code,{className:"language-ts",metastring:"title='./index.ts'",children:"\n//diff-add\nimport translations from \"./resources/translations\";\n...\n\nconst adminForth = new AdminForth({\n ...\n resources: [\n ...\n//diff-add\n translations,\n ],\n menu: [\n ...\n//diff-add\n {\n//diff-add\n label: 'Translations',\n//diff-add\n icon: 'material-symbols:translate',\n//diff-add\n resourceId: 'translations',\n//diff-add\n },\n ],\n ...\n});\n\n"})}),"\n",(0,a.jsx)(e.p,{children:"This is it, now you should restart your app and see the translations resource in the menu."}),"\n",(0,a.jsx)(e.p,{children:"You can add translations for each language manually or use Bulk actions to generate translations with AI completion adapter."}),"\n",(0,a.jsx)(e.p,{children:'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).'}),"\n",(0,a.jsx)(e.h2,{id:"translation-for-custom-components",children:"Translation for custom components"}),"\n",(0,a.jsx)(e.p,{children:"To translate custom components, you should simply wrap all strings in $t function. For example:"}),"\n",(0,a.jsxs)(e.p,{children:["Now create file ",(0,a.jsx)(e.code,{children:"CustomLoginFooter.vue"})," in the ",(0,a.jsx)(e.code,{children:"custom"})," folder of your project:"]}),"\n",(0,a.jsx)(e.pre,{children:(0,a.jsx)(e.code,{className:"language-html",metastring:'title="./custom/CustomLoginFooter.vue"',children:'\n
\n\n'})}),"\n",(0,a.jsx)(e.h3,{id:"variables-in-frontend-translations",children:"Variables in frontend translations"}),"\n",(0,a.jsx)(e.p,{children:"You can use variables in translations in same way like you would do it with vue-i18n library."}),"\n",(0,a.jsx)(e.p,{children:"This is generally helps to understand the context of the translation for AI completion adapters and simplifies the translation process, even if done manually."}),"\n",(0,a.jsx)(e.p,{children:'For example if you have string "Showing 1 to 10 of 100 entries" you can of course simply do'}),"\n",(0,a.jsx)(e.pre,{children:(0,a.jsx)(e.code,{className:"language-html",children:"{{ $t('Showing')}} {{from}} {{$t('to')}} {{to}} {{$t('of')}} {{total}} {{$t('entries') }}\n"})}),"\n",(0,a.jsx)(e.p,{children:"And it will form 4 translation strings. But it is much better to have it as single string with variables like this:"}),"\n",(0,a.jsx)(e.pre,{children:(0,a.jsx)(e.code,{className:"language-html",children:"{{ $t('Showing {from} to {to} of {total} entries', { from, to, total } ) }}\n"})}),"\n",(0,a.jsx)(e.p,{children:"For example, let's add user greeting to the header."}),"\n",(0,a.jsx)(e.pre,{children:(0,a.jsx)(e.code,{className:"language-html",metastring:'title="./custom/Header.vue"',children:'\n
import{ AdminUser }from'adminforth'; import{ admin }from'../index'; { ... resourceId:'aparts', ... options:{ bulkActions:[ { label:'Mark as listed', icon:'flowbite:eye-solid', // if optional `confirm` is provided, user will be asked to confirm action confirm:'Are you sure you want to mark all selected apartments as listed?', action:function({selectedIds, adminUser }:{selectedIds:any[], adminUser: AdminUser }){ const stmt = admin.resource('aparts').dataConnector.db.prepare(`UPDATE apartments SET listed = 1 WHERE id IN (${selectedIds.map(()=>'?').join(',')})`); stmt.run(...selectedIds); return{ ok:true, error:false, successMessage:`Marked ${selectedIds.length} apartments as listed`}; return{ ok:true, error:false, successMessage:awaittr('Marked {count} apartments as listed','apartments',{ count: selectedIds.length })}; }, } ], } }
import{ AdminUser }from'adminforth'; import{ admin }from'../index'; { ... resourceId:'aparts', ... options:{ bulkActions:[ { label:'Mark as listed', icon:'flowbite:eye-solid', // if optional `confirm` is provided, user will be asked to confirm action confirm:'Are you sure you want to mark all selected apartments as listed?', action:function({selectedIds, adminUser }:{selectedIds:any[], adminUser: AdminUser }){ const stmt = admin.resource('aparts').dataConnector.db.prepare(`UPDATE apartments SET listed = 1 WHERE id IN (${selectedIds.map(()=>'?').join(',')})`); stmt.run(...selectedIds); return{ ok:true, error:false, successMessage:`Marked ${selectedIds.length} apartments as listed`}; return{ ok:true, error:false, successMessage:awaittr('Marked {count} apartments as listed','apartments',{ count: selectedIds.length })}; }, } ], } }
You can use this module not only to translate Admin area of your application but also to translate other services of your application.
+This will allow you to reuse the same functionality and AI completion adapters for all your translations. For example in this app we
+will consider that we have a Nuxt.js SEO-centric frontend which we want to translate with vue-i18n.
+
To do it you need to use 2 exposed methods from the plugin: feedCategoryTranslations and getCategoryTranslations.
+
First of all, at some step, e.g. CI pipeline you should get all translation strings from your external app and feed them ao an own rest API like '/feed-nuxt-strings', this API might look like this
Make sure to replace adminforth:3000 with AdminForth API URL. We are assuming it is docker container name in internal docker network.
+
So in the pipeline you should run npm run i18n:feed-to-backoffice to extract messages from your Nuxt.js app and feed them to AdminForth.
+
+
👆 The example method is just a stub, please make sure you not expose endpoint to public or add some simple authorization on it,
+otherwise someone might flood you with dump translations requests.
+
+
Then in your Nuxt.js app you should call this API and store the strings in the same.