doc: describe the web UI files and translation table after the rewrite

This commit is contained in:
bloqaudio committed 2026-09-14 08:03:36 -05:00
1 parent 3e91b05a42
commit 75f2ade1a1
2 files changed
+78 -104

No files matched your search

+66 -94
View File
@@ -1,135 +1,107 @@
# Supporting Multiple Languages in the Web UI # Supporting Multiple Languages in the Web UI
The firmware uses a client-side i18n approach The firmware uses a client-side i18n approach: all translations are stored
all translations are stored in a single JavaScript dictionary embedded in the firmware. in one JavaScript dictionary embedded in the firmware. No server-side
No server-side changes are needed. changes are needed.
## Architecture ## Architecture
All translation logic lives in `html/i18n.js`. The file contains: All translation logic lives at the top of `html/app.js`:
- A `LANG` object with one sub-object per language (`en`, `ja`, ...) - A `LANG` object with one sub-object per language (`en`, `ja`, `zh`)
- Language auto-detection (browser language → `localStorage` override) - Language auto-detection (browser language, overridden by the
- `t(key)` — look up a translated string `rtl_lang` key in `localStorage`, which the language selector in the
- `setLang(lang)` — switch language and update the page page header writes)
- `applyTranslation(el)` — apply translation to one DOM element - `t(key, vars)` looks up a translated string; `{name}` placeholders in
the string are replaced from `vars`, e.g. `t("v_del_q", {n: vid})`
- `i18nApply()` applies the dictionary to the static markup once at load
Translation keys are **flat strings** (no nesting). The English keys in `LANG.en` also serve as the fallback when a key is missing in another language. Translation keys are flat strings. The English entry is the fallback when
a key is missing in another language, and the key itself is the fallback
when it is missing everywhere.
Changing the language stores the choice and reloads the page, so every
dynamically built table is rebuilt in the new language.
`login.html` is served before authentication and does not load `app.js`;
it carries its own five strings per language in an inline table.
## How to Add a New Language ## How to Add a New Language
### 1. Add a dictionary entry in `html/i18n.js` ### 1. Add a dictionary entry in `html/app.js`
Append a new sub-object to the `LANG` object. Every key from `LANG.en` must be present: Append a sub-object to `LANG`. Every key from `LANG.en` should be present:
```js ```js
var LANG = { var LANG={
en: { en:{
nav_overview: 'Overview', nav_dash:"Dashboard",
nav_port_config: 'Port Configuration', ...
// ... all keys for English },
}, LANGCODE:{ // add your language here
ja: { nav_dash:"...",
nav_overview: '概要', ...
nav_port_config: 'ポート設定', }
// ... all keys for Japanese
},
LANGCODE: { // ← add your language here
nav_overview: '...',
nav_port_config: '...',
// ... translate every key
},
}; };
``` ```
### 2. Add the language to the navigation sidebar ### 2. Add the language to the selector in `html/index.html`
In `html/navigation.js`, add an `<option>` to the language selector: ```html
<select class="ctl" id="langSel" data-i18n-t="sy_lang">
```js <option value="en">English</option>
+ "<option value='en'>English</option><option value='ja'>日本語</option>" <option value="ja">日本語</option>
<option value="zh">中文</option>
<option value="LANGCODE">Native name</option>
</select>
``` ```
Replace with: ### 3. Add the login strings in `html/login.html`
```js The inline table there holds, per language: page title, subtitle,
+ "<option value='en'>English</option><option value='ja'>日本語</option><option value='LANGCODE'>Native Name</option>" password label, button label, wrong-password message.
```
### 3. Verify auto-detection ### 4. Verify auto-detection
The language detection code in `i18n.js` reads `navigator.language` and normalises it to the first two characters: The detection normalises `navigator.language` to its first two
characters and selects that language if `LANG` has it, otherwise English.
```js
var browser = (navigator.language || navigator.userLanguage || 'en').substring(0, 2);
return LANG[browser] ? browser : 'en';
```
If the two-letter code matches a key in `LANG`, it will be auto-selected. No changes needed here.
## Two Translation Mechanisms ## Two Translation Mechanisms
### (A) `data-i18n` attribute (declarative — for HTML) ### (A) `data-i18n` attributes (declarative, for HTML)
Add `data-i18n="key_name"` to any HTML element. The English text goes in the element content as a fallback:
```html ```html
<h1 data-i18n="port_heading">Port Configuration</h1> <h2 data-i18n="pt_title">Port configuration</h2>
<input type="button" data-i18n="port_apply" value="Apply"> <button class="ctl" data-i18n="c_apply" data-i18n-t="sy_dhcp_t">Apply</button>
<option data-i18n="port_auto">Auto</option> <input class="in" data-i18n-p="l2_filter">
<title data-i18n="port_title">Port Configuration</title>
``` ```
On page load, `applyTranslation()` sets: - `data-i18n` sets the element's text content
- `data-i18n-t` sets the `title` attribute (tooltip)
- `data-i18n-p` sets the `placeholder` attribute
- `el.value` for `<input type="submit|button">` The English text stays in the markup as the fallback and as the source
- `el.textContent` for `<option>`, `<title>` of truth for what the key means.
- `el.innerHTML` for everything else
### (B) `t('key')` call (imperative — for JavaScript strings) ### (B) `t("key")` calls (imperative, for JavaScript)
When generating HTML or text in JavaScript, wrap translatable strings with `t()`:
```js ```js
td.appendChild(document.createTextNode(t('common_port') + i)); tr.insertCell().textContent=t("c_port")+" "+p;
td.innerHTML = t('port_auto'); toast(t("v_loaded",{n:vid}),"ok");
iHTML += "<tr><td>" + t('port_vendor') + "</td></tr>";
``` ```
### Special Case: Link Speed Display Strings that are identical in every language (`10M`, `2.5G`, `MAC`,
`VLAN`, `CPU`, `RSTP`) are literals, not dictionary entries.
The `linkS` array in `html/main.js` maps numeric link states to display strings. The first two entries (`speed_disabled`, `speed_down`) use `t()` for translation; the remaining entries are static literals (they are the same in all languages):
```js
const linkS = [
function(){return t('speed_disabled')},
function(){return t('speed_down')},
"10M", "100M", "1000M", "500M", "10G", "2.5G", "5G"
];
function linkText(idx) { var v = linkS[idx]; return typeof v === 'function' ? v() : v; }
```
Always use `linkText(idx)` (not `linkS[idx]`) to read these values.
## Size Considerations ## Size Considerations
- `html/i18n.js` is embedded in the firmware filesystem (~14 KB for two languages) - Each language adds roughly 3 KB to `app.js` before compression; the
- Each new language adds roughly the same number of bytes as the English dictionary (~3–4 KB) build gzips the file, so the flash cost of a language is closer to 1 KB
- The firmware binary is padded to 512 KiB, so a few extra KB do not change the flash footprint - The largest embedded file must stay below 64 KB after compression
- Values that are identical in all languages should be inlined as literals rather than added to the dictionary (e.g., `"10M"`, `"2.5G"`, `"MAC"`, `"VLAN"`, `"CPU"`) (`uint16_t` length in the file table); `app.js` with three languages
is about 29 KB compressed
## Build ## Build
No special flags are needed. The `html/` directory is embedded by `fileadder` during the build. No special flags are needed. `html/` is minified and embedded by the
normal build (see `webui-compression.md`).
## Script Load Order
`i18n.js` must be loaded after `main.js` (which defines `t()`'s dependencies like `LANG`) but before any page-specific JS that calls `t()`:
```html
<script src="/main.js"></script>
<script src="/i18n.js"></script>
<script src="/eee.js"></script> <!-- uses t() -->
```
The `navigation.js` script is loaded last (bottom of `<body>`).
+12 -10
View File
@@ -19,16 +19,17 @@ html/ --(minify.py)--> output/html_min/ --(fileadder -z)--> flash image
served bytes, so minification alone already reduces the transfer size served bytes, so minification alone already reduces the transfer size
from 109,814 to 97,730 bytes, independent of gzip. from 109,814 to 97,730 bytes, independent of gzip.
- The Web UI is a single-page application: `index.html` holds all pages - The Web UI is a single-page application: `index.html` holds all pages
as sections (`#/ports`, `#/vlan`, ...) that the sidebar switches as sections (`#ports`, `#vlan`, ...) that the sidebar switches
between via the URL hash. `login.html` stays separate because it is between via the URL hash. `login.html` stays separate because it is
the authentication gate. Only these two pages plus `main.js`, the authentication gate and is self-contained (its own styles and
`style.css`, the three SVG port images and the favicon are embedded. strings), so nothing else is served before login. Only these two
- All JavaScript (i18n, shared helpers, navigation and the per-section pages plus `app.js` and the favicon are embedded; the stylesheet and
scripts) lives in one `html/main.js` bundle that the pages load as the theme bootstrap are inline in `index.html`.
their only script, so the browser fetches it once and gzip can - All JavaScript (translation table, shared helpers, navigation and the
compress across all of it. A section's initialisation runs when the per-section code) lives in one `html/app.js` that the page loads as
section is shown, its polling intervals run only while it is visible, its only script, so the browser fetches it once and gzip can compress
and re-entering a section refetches the data. across all of it. A section starts its pollers when it is shown and
stops them when it is left; re-entering a section refetches the data.
- `fileadder -z` gzip-compresses every file (zlib, gzip format, - `fileadder -z` gzip-compresses every file (zlib, gzip format,
`Z_BEST_COMPRESSION`) when it generates the file table `Z_BEST_COMPRESSION`) when it generates the file table
(`html_data.c`/`html_data.h`) and when it embeds the files into the (`html_data.c`/`html_data.h`) and when it embeds the files into the
@@ -71,7 +72,8 @@ JSON polling of the visible section).
## Notes ## Notes
- The files stay well below the `uint16_t` size limit of the file table - The files stay well below the `uint16_t` size limit of the file table
(largest gzip output: 17.2 KB for the merged `main.js`). (largest gzip output: about 29 KB for `app.js`, three languages
included).
- `fileadder` terminates the embedded files with a NUL directly after the - `fileadder` terminates the embedded files with a NUL directly after the
content (previously at `data_read + 1`), so the `strlen()`-based size content (previously at `data_read + 1`), so the `strlen()`-based size
computation no longer depends on uninitialised buffer content. computation no longer depends on uninitialised buffer content.