|
| 1 | +--- |
| 2 | +title: Static Translations (i18n Static) |
| 3 | +description: "Guide to the i18n Static plugin: translate the AdminForth admin panel using JSON files kept in git, sync translation keys from sources with a CLI command and fill them with a coding agent." |
| 4 | +slug: /tutorial/Plugins/i18n-static |
| 5 | +--- |
| 6 | + |
| 7 | +# Static Translations (i18n Static) |
| 8 | + |
| 9 | +This plugin translates the AdminForth admin panel using plain JSON files, one file per language. No database resource is needed: |
| 10 | +translations live in your repository next to the code, go through code review and are deployed together with the app. |
| 11 | + |
| 12 | +Main features: |
| 13 | +- One flat `<language code>.json` file per language, e.g. `langs/uk.json`. |
| 14 | +- A CLI command finds every translation key in the admin SPA sources and adds the new keys to all language files. It does not need to build the SPA. |
| 15 | +- Edits are applied after a page reload. No restart is needed. |
| 16 | +- Ships an agent skill, so a coding agent can fill in missing translations for you. |
| 17 | +- Adds a language selector to the login page and the user menu. |
| 18 | +- Supports ISO 639-1 codes and BCP-47 tags with a region (e.g. `en-GB`, `pt-BR`), plus pluralization, Slavic forms included. |
| 19 | + |
| 20 | +:::tip i18n Static or i18n? |
| 21 | +Use this plugin if developers maintain the translations and you want them in git. |
| 22 | +If translators edit strings in the admin panel itself, or you want built-in LLM translation and translations for an external app, |
| 23 | +use the [i18n plugin](/docs/tutorial/Plugins/i18n/) instead. |
| 24 | +::: |
| 25 | + |
| 26 | +## Installation |
| 27 | + |
| 28 | +```bash |
| 29 | +pnpm add @adminforth/i18n-static |
| 30 | +``` |
| 31 | + |
| 32 | +The plugin is global, so register it in `globalPlugins` rather than on a resource: |
| 33 | + |
| 34 | +```ts title='./index.ts' |
| 35 | +import I18nStaticPlugin from '@adminforth/i18n-static'; |
| 36 | + |
| 37 | +export const admin = new AdminForth({ |
| 38 | + ... |
| 39 | + globalPlugins: [ |
| 40 | + new I18nStaticPlugin({ |
| 41 | + supportedLanguages: ['en', 'uk', 'ja', 'fr', 'es'], |
| 42 | + }), |
| 43 | + ], |
| 44 | +}); |
| 45 | +``` |
| 46 | + |
| 47 | +Now create the language files: |
| 48 | + |
| 49 | +```bash |
| 50 | +pnpm exec adminforth-i18n-static sync |
| 51 | +``` |
| 52 | + |
| 53 | +You will get one file per language in the `langs` folder, with all admin panel strings as keys and empty values. In `en.json` values are filled with the keys. |
| 54 | +The language selector appears on the login page and in the user menu. Until you translate the strings, they are shown in English. |
| 55 | + |
| 56 | +## Options |
| 57 | + |
| 58 | +| Option | Default | Description | |
| 59 | +|--------|---------|-------------| |
| 60 | +| `supportedLanguages` | – | Required. Language codes to support: ISO 639-1 (`uk`) or BCP-47 with a region (`pt-BR`). The app does not start if a code is invalid. | |
| 61 | +| `primaryLanguage` | `'en'` | Language shown by default. See [Primary language](#primary-language). | |
| 62 | +| `translationsDir` | `'langs'` | Folder with language files. A relative path is resolved from the current working directory. Created by the `sync` command. | |
| 63 | +| `loginPageLanguageSelectorOrder` | `0` | Order of the language selector among other components under the login button. | |
| 64 | + |
| 65 | +## Language files |
| 66 | + |
| 67 | +Every supported language has its own flat JSON file. Keys are the original English strings, values are translations: |
| 68 | + |
| 69 | +```json title='./langs/uk.json' |
| 70 | +{ |
| 71 | + "Sign out": "Вийти", |
| 72 | + "Save": "Зберегти", |
| 73 | + "Showing {from} to {to} of {total} entries": "Показано з {from} по {to} з {total} записів", |
| 74 | + "Apartment | Apartments": "квартир | квартира | квартири | квартир", |
| 75 | + "Open sidebar": "" |
| 76 | +} |
| 77 | +``` |
| 78 | + |
| 79 | +- An empty value means "not translated yet", and the original English key is shown instead. |
| 80 | +- Keep the variables in curly brackets exactly as they are in the key, and do not translate them. |
| 81 | +- `sync` fills `en.json` values with the keys, so English is complete right away. You can edit a value there to rephrase an English text without changing the source code. |
| 82 | +- Files are read on every request, so you can edit a file and reload the page to see the result. |
| 83 | + |
| 84 | +The plugin checks all language files on startup. If a file is not valid JSON, or is not a flat object with string values, |
| 85 | +the app does not start and prints the file path and the position of the error: |
| 86 | + |
| 87 | +``` |
| 88 | +Error: Translations file /app/langs/uk.json is not valid JSON: Expected double-quoted property name in JSON at position 20 (line 1 column 21) |
| 89 | +``` |
| 90 | + |
| 91 | +## Syncing keys |
| 92 | + |
| 93 | +Run `sync` every time you add or change `$t(...)` strings in custom components, add a plugin or upgrade AdminForth: |
| 94 | + |
| 95 | +```bash |
| 96 | +pnpm exec adminforth-i18n-static sync |
| 97 | +``` |
| 98 | + |
| 99 | +The command loads your admin instance from the `.ts` file in the current folder that contains `export const admin` |
| 100 | +(it tries `index.ts` first), and reads `.env.local` and `.env`. Then it collects `$t()` keys from the same sources the AdminForth bundler uses: |
| 101 | +the core SPA, the custom folders of all plugins and your `customComponentsDir`. |
| 102 | + |
| 103 | +``` |
| 104 | +🌐 Extracting translation keys... |
| 105 | +📄 langs/en.json 515 total +2 added 0 stale ✔ all translated |
| 106 | +📄 langs/uk.json 515 total +2 added 0 stale 2 untranslated |
| 107 | +✨ langs/es.json created 515 total +515 added 0 stale 515 untranslated |
| 108 | +``` |
| 109 | + |
| 110 | +- **added**: new keys, appended to the end of the file with an empty value (in `en.json`, with the key itself). Existing keys and their order are kept. |
| 111 | +- **stale**: keys that are no longer found in the sources. They are only reported, never removed, so you decide whether to delete them. |
| 112 | +- **untranslated**: keys with an empty value, stale ones included. |
| 113 | +- **wrong plural**: plural translations with a wrong number of forms, listed below the file line. They are shown only when there are some. |
| 114 | + |
| 115 | +:::note |
| 116 | +The CLI imports your admin `index.ts`. Make sure importing it does not start the HTTP server: keep `admin.express.serve(app)` |
| 117 | +behind the `if (fileURLToPath(import.meta.url) === path.resolve(process.argv[1]))` check, as the AdminForth app template does. |
| 118 | +::: |
| 119 | + |
| 120 | +## Translating with a coding agent |
| 121 | + |
| 122 | +On the first `sync` the plugin copies its skill into `.agents/skills/adminforth-i18n-static/SKILL.md`. |
| 123 | +An existing file is never overwritten, so you can adjust the skill to your project. |
| 124 | + |
| 125 | +The skill teaches an agent the translation workflow: run `sync`, fill only the empty values, keep variables and plural forms, look up unclear short keys |
| 126 | +in the sources, and run `sync` again until `untranslated` is 0. Ask your agent something like: |
| 127 | + |
| 128 | +``` |
| 129 | +Translate the admin panel to Japanese and French |
| 130 | +``` |
| 131 | + |
| 132 | +Review the result like any other code change before you commit it. |
| 133 | + |
| 134 | +## Translation for custom components |
| 135 | + |
| 136 | +Wrap strings in custom components with `$t`, then run `sync`: |
| 137 | + |
| 138 | +```html title='./custom/CustomLoginFooter.vue' |
| 139 | +<template> |
| 140 | + <div class="text-center text-gray-500 text-sm mt-4"> |
| 141 | + {{ $t('By logging in, you agree to our') }} <a href="#" class="text-blue-500">{{ $t('Terms of Service') }}</a> |
| 142 | + </div> |
| 143 | +</template> |
| 144 | +``` |
| 145 | + |
| 146 | +Prefer whole sentences with variables over sentences glued together from fragments, because translators (and agents) need the context: |
| 147 | + |
| 148 | +```html |
| 149 | +{{ $t('Showing {from} to {to} of {total} entries', { from, to, total }) }} |
| 150 | +``` |
| 151 | + |
| 152 | +Keys built from template literals with variables, like `` $t(`Hello ${name}`) ``, are skipped by `sync`, because they produce a different key every time. |
| 153 | + |
| 154 | +### Pluralization |
| 155 | + |
| 156 | +Pluralization uses the vue-i18n rules. Separate the forms with `|`: |
| 157 | + |
| 158 | +```html |
| 159 | +{{ $t('Apartment last 7 days | Apartments last 7 days', data.totalAparts) }} |
| 160 | +``` |
| 161 | + |
| 162 | +Most languages use the same number of forms as English. Slavic languages (`uk`, `ru`, `be`, `pl`, `cs`, `sk`, `bg`, `sr`, `hr`, `mk`, `sl`) need 4 forms: |
| 163 | +zero, one, 2-4 and 5+, e.g. `"квартир | квартира | квартири | квартир"`. |
| 164 | + |
| 165 | +The plugin checks plural translations on startup: |
| 166 | + |
| 167 | +- A translation with a single form (no `|`) breaks the page when it is rendered, so the app does not start: |
| 168 | + |
| 169 | + ``` |
| 170 | + Error: Translations file /app/langs/uk.json has plural translations which break the page when rendered: |
| 171 | + "Apartment | Apartments": "Квартир" — expected 4 forms (zero | singular | 2-4 | 5+), found 1 |
| 172 | + ``` |
| 173 | + |
| 174 | +- A translation with a different wrong number of forms, e.g. 2 forms for Ukrainian, does not break the page, but some numbers are shown |
| 175 | + with the wrong word form. The app starts with a warning in the console, and `sync` reports such strings as `wrong plural`. |
| 176 | + |
| 177 | +Untranslated plural strings (empty values) are fine: they are shown with the English forms. |
| 178 | + |
| 179 | +## Primary language |
| 180 | + |
| 181 | +`primaryLanguage` sets the language shown to users by default. If a user requests a language that is not in `supportedLanguages`, |
| 182 | +the plugin falls back to `primaryLanguage`, then to English. |
| 183 | + |
| 184 | +```ts |
| 185 | +new I18nStaticPlugin({ |
| 186 | + primaryLanguage: 'pt-BR', |
| 187 | + supportedLanguages: ['pt-BR', 'en', 'es'], |
| 188 | +}), |
| 189 | +``` |
| 190 | + |
| 191 | +English stays the source language: keys in all files are English strings, even if `primaryLanguage` is different. |
| 192 | + |
| 193 | +## Deployment |
| 194 | + |
| 195 | +Language files are read at runtime from `translationsDir`, so commit them and make sure they are included in your Docker image or deployment artifact. |
| 196 | +If you run the app from a different working directory, pass an absolute path in `translationsDir`. |
| 197 | + |
| 198 | +## Limitations |
| 199 | + |
| 200 | +The plugin translates the frontend: strings from `$t()` in the admin SPA, plugins and your custom components. |
| 201 | +Backend translations made with `tr()` (menu labels, backend error messages, `tr` in custom APIs) are not supported yet and are shown as they are written in the config. |
| 202 | +If you need them, use the [i18n plugin](/docs/tutorial/Plugins/i18n/#translations-in-custom-apis). |
0 commit comments