mirror of
https://github.com/logicog/RTLPlayground.git
synced 2026-10-29 17:54:14 +08:00
doc: describe the web UI files and translation table after the rewrite
This commit is contained in:
1 parent
3e91b05a42
commit
75f2ade1a1
2 files changed
+78
-104
No files matched your search
@@ -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
@@ -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.
|
||||||
|
|||||||
Reference in new issue
Block a user