Skip to content

Commit 37f768d

Browse files
authored
Merge pull request #737 from devforth/feature/AdminForth/1609/add-few-languages-support-out-
docs: add i18n-static plugin page and connect the plugin in dev-demo AdminForth/1609/add-few-languages-support-out-
2 parents f70d980 + 7dd47c1 commit 37f768d

10 files changed

Lines changed: 2870 additions & 3 deletions

File tree

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
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).
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
name: adminforth-i18n-static
3+
description: "Use when asked to translate AdminForth admin panel UI, fill missing translations, or add a new admin language with @adminforth/i18n-static plugin (JSON files per language)."
4+
user-invocable: true
5+
---
6+
7+
# AdminForth Static Translations Workflow
8+
9+
## How Translations Work
10+
11+
- Every supported language has its own flat JSON file `<translationsDir>/<language code>.json` (`langs/` by default), e.g. `langs/uk.json`, `langs/pt-BR.json`.
12+
- Keys are original English strings from admin UI, values are translations: `{ "Save": "Зберегти" }`.
13+
- Empty value means "not translated yet", admin shows the original English key instead.
14+
- Files are read on every request, so edits are visible after page reload without restart.
15+
16+
## Workflow
17+
18+
1. Sync keys first, so every language file contains all actual keys:
19+
`pnpm exec adminforth-i18n-static sync` (or `npx adminforth-i18n-static sync` for npm).
20+
It prints every language file with counts: `total`, `added`, `stale` and `untranslated`.
21+
2. Fill empty values in language files the user asked for (all non-English files if not specified).
22+
3. Run sync again and make sure `untranslated` is 0 for translated files and there is no `wrong plural` warning. Fix every listed plural string.
23+
24+
## Translation Rules
25+
26+
- Never change, add or remove keys, only write values. Keep keys order.
27+
- Do not translate `en.json`: sync fills its values with the keys (original English strings). Change its values only if the user asks to rephrase English texts.
28+
- Do not touch non-empty values unless the user asks to fix them.
29+
- Do not remove stale keys (reported as `stale`), the user decides about them.
30+
- Keep variables in curly brackets exactly as in the key and do not translate them: `"Hello {name}"` → `"Привіт {name}"`, never `{ім'я}`.
31+
- Keys with `|` are plural forms. Keep the same number of forms separated by `|`, except Slavic languages
32+
(uk, ru, be, pl, cs, sk, bg, sr, hr, mk, sl) which need 4 forms "zero | singular | 2-4 | 5+",
33+
e.g. `"apple | apples"` in `uk.json` becomes `"яблук | яблуко | яблука | яблук"`.
34+
- For codes with region (e.g. `pt-BR`) use regional spelling, punctuation and formatting.
35+
- Keep translations short: they are UI labels, buttons and messages of an admin panel.
36+
- If meaning of a key is not clear (short words like `Show`, `of`, `to`, `Your` or sentence fragments), find where it is used:
37+
search for the key in `$t('...')` / `t('...')` calls in project custom components and in AdminForth sources
38+
(`node_modules/adminforth/dist/spa/src`, `node_modules/@adminforth/*/dist/custom`). Translate by context: a button, a label
39+
or a part of a sentence glued with other text in the template, then the translation must fit the whole phrase.
40+
- Keep JSON valid with 2 spaces indentation.

‎dev-demo/Taskfile.yaml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ vars:
1111
- "adminforth-email-password-reset"
1212
- "adminforth-foreign-inline-list"
1313
- "adminforth-i18n"
14+
- "adminforth-i18n-static"
1415
- "adminforth-import-export"
1516
- "adminforth-text-complete"
1617
- "adminforth-open-signup"

‎dev-demo/globalPlugins.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import OpenAIAudioAdapter from '../adapters/adminforth-audio-adapter-openai/inde
55
import DashboardPlugin from '../plugins/adminforth-dashboard/index.js';
66
import AdminForthMcpPlugin from '../plugins/adminforth-mcp/index.js';
77
import UserSessionsPlugin from '../plugins/adminforth-user-sessions/index.js';
8+
import I18nStaticPlugin from '../plugins/adminforth-i18n-static/index.js';
89
import { levelDbAdapter } from './utils.js';
910

1011
const OVH_AI_ENDPOINTS_BASE_URL = 'https://oai.endpoints.kepler.ai.cloud.ovh.net/v1';
@@ -38,6 +39,9 @@ function createAgentCompletionAdapter(
3839
}
3940

4041
export const globalPlugins = [
42+
new I18nStaticPlugin({
43+
supportedLanguages: ['en', 'uk', 'de', 'fr', 'ja'],
44+
}),
4145
new UserSessionsPlugin({
4246
keyValueAdapter: levelDbAdapter,
4347
canManageOtherUsersSessions: async (adminUser) => adminUser.dbUser.role === 'superadmin',

0 commit comments

Comments
 (0)