Skip to content

Commit 303ec84

Browse files
committed
feat: add diff.py for version comparison, update README with limitations
- Add diff.py: compare template versions to find added/removed/modified strings - Update README: fix 'zero flash overhead' claim, add Known Limitations section - Update README: add diff.py usage, fix JS string count (45→716) - Remove AI slop, simplify language
1 parent 15f0f9e commit 303ec84

2 files changed

Lines changed: 208 additions & 30 deletions

File tree

tools/i18n/README.md

Lines changed: 75 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# WLED i18n Toolchain
22

3-
Build-time internationalization for WLED Web UI. Translates HTML/JS strings at compile time with **zero runtime overhead** and **zero flash overhead** (replaces, not adds).
3+
Build-time internationalization for WLED Web UI. Translates HTML/JS strings at compile time — replaces English text, does not add to it.
44

55
## How It Works
66

@@ -43,7 +43,7 @@ WLED-translations/
4343
├── library.json # PlatformIO dependency manifest
4444
├── zh_CN/
4545
│ ├── static.json # Layer 1: static HTML (429 entries)
46-
│ ├── js.json # Layer 2: JS strings (45 entries)
46+
│ ├── js.json # Layer 2: JS strings (716 entries)
4747
│ ├── effects.json # Layer 3: effect names (216 entries)
4848
│ ├── palettes.json # Layer 4: palette names (72 entries)
4949
│ └── metadata.json
@@ -61,7 +61,7 @@ cd WLED-translations
6161

6262
# 2. Generate English template (from WLED source)
6363
python3 /path/to/WLED/tools/i18n/extract.py --stats
64-
cp /path/to/WLED/tools/i18n/locales/en_template.json en_template/
64+
cp -r /path/to/WLED/tools/i18n/locales/* en_template/
6565

6666
# 3. Create your locale
6767
mkdir de_DE
@@ -98,6 +98,24 @@ npm ci && npm run build
9898
pio run -e esp32dev
9999
```
100100

101+
### Version updates (diff)
102+
103+
When WLED releases a new version, compare templates to find changes:
104+
105+
```bash
106+
# Generate old and new templates
107+
python3 tools/i18n/extract.py --stats # on old version
108+
cp tools/i18n/locales/en_template.json en_template_old.json
109+
110+
python3 tools/i18n/extract.py --stats # on new version
111+
cp tools/i18n/locales/en_template.json en_template_new.json
112+
113+
# Compare
114+
python3 tools/i18n/diff.py --old en_template_old.json --new en_template_new.json
115+
```
116+
117+
Output shows added/removed/modified strings. Translators update only changed entries.
118+
101119
## Translation Search Order
102120

103121
`build.py` searches for translations in this order:
@@ -106,37 +124,64 @@ pio run -e esp32dev
106124
2. `.pio/libdeps/*/WLED-translations/<locale>/` (PlatformIO out-of-tree)
107125
3. `tools/i18n/locales/<locale>.json` (local fallback)
108126

109-
## Translation JSON Format
110-
111-
```json
112-
{
113-
"index.htm": {
114-
"html:body > div#btns > a:nth-of-type(1):text": {
115-
"en": "Power",
116-
"translation": "电源",
117-
"context": "index.htm: (html_text)"
118-
},
119-
"js:index.htm:45:a1b2c3d4": {
120-
"en": "Loading...",
121-
"translation": "加载中...",
122-
"context": "index.htm:45 (js_innerHTML)"
123-
}
124-
}
125-
}
126-
```
127-
128127
## Coverage
129128

130129
| Layer | Content | Method | Count |
131130
|-------|---------|--------|-------|
132131
| 1. Static HTML | Labels, buttons, placeholders | DOM text matching | 429 |
133-
| 2. JS strings | `alert()`, `innerHTML`, `innerText` | Script block regex | 45 |
134-
| 3. Effect names | FX names in `colors.cpp` | PROGMEM replacement | 216 |
135-
| 4. Palette names | Palette names in `colors.cpp` | PROGMEM replacement | 72 |
132+
| 2. JS strings | `alert()`, `innerHTML`, `innerText` | Script block regex | 716 |
133+
| 3. Effect names | FX names in `FX.cpp` | PROGMEM replacement | 216 |
134+
| 4. Palette names | Palette names in `FX_fcn.cpp` | PROGMEM replacement | 72 |
135+
136+
## Known Limitations
137+
138+
### Cannot translate (technical)
136139

137-
## Limitations
140+
- **Dynamic runtime text** — OTA update errors, Info page content, usermod settings, Pin Info page
141+
- **External tools** — PixelForge add-ons (always English, downloaded on-the-fly)
142+
- **JavaScript template literals** — strings with `${...}` interpolation
143+
- **C++ server-side strings**~12 strings in `xml.cpp` need `#ifdef WLED_LOCALE_*`
138144

139-
1. **No runtime language switching** — language is fixed at build time
140-
2. **JS template literals with `${...}`** — partial strings can't be safely replaced
141-
3. **C++ server-side strings**~12 strings in `xml.cpp` need `#ifdef WLED_LOCALE_*`
142-
4. **External tools** (pixelforge, pixelmagic) — always English, downloaded on-the-fly
145+
### Language-specific issues (acknowledged)
146+
147+
- Word order differences (e.g., "X of Y" patterns)
148+
- Number formats (decimal point vs comma)
149+
- Grammar rules (singular/plural, countable/uncountable)
150+
- Date formats
151+
152+
These are known limitations. The tool handles short labels and UI fragments, not full sentences.
153+
154+
## Layer 3/4: Effect & Palette Names
155+
156+
Effect names (216) and palette names (72) are translated via C++ PROGMEM replacement:
157+
158+
```c
159+
// locale_effects.h (auto-generated)
160+
#pragma once
161+
#ifdef WLED_LOCALE
162+
#undef _data_FX_MODE_STATIC
163+
static const char _data_FX_MODE_STATIC[] PROGMEM = "常亮";
164+
// ...
165+
#endif
166+
```
167+
168+
The `.h` files are generated in the translation repo. Users copy them to their local build.
169+
170+
## Architecture
171+
172+
```
173+
WLED (core repo)
174+
└── tools/i18n/
175+
├── extract.py # Extract strings from HTML/JS
176+
├── build.py # Apply translations (pre-build script)
177+
├── diff.py # Compare template versions
178+
└── README.md
179+
180+
WLED-translations (community repo)
181+
├── zh_CN/
182+
│ ├── static.json # Layer 1: HTML text
183+
│ ├── js.json # Layer 2: JS strings
184+
│ ├── effects.json # Layer 3: effect names
185+
│ └── palettes.json # Layer 4: palette names
186+
└── en_template/ # English template
187+
```

tools/i18n/diff.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
#!/usr/bin/env python3
2+
"""
3+
WLED i18n Translation Diff Tool
4+
Compares two template versions and reports changes.
5+
6+
Usage:
7+
python3 diff.py --old v0.15.0.json --new v0.16.0.json
8+
python3 diff.py --old en_template_old/ --new en_template/ --locale zh_CN
9+
10+
Output:
11+
JSON with added/removed/modified strings and stats.
12+
"""
13+
14+
import json
15+
import sys
16+
from pathlib import Path
17+
18+
19+
def load_template(path):
20+
"""Load a single JSON template file."""
21+
with open(path, encoding='utf-8') as f:
22+
return json.load(f)
23+
24+
25+
def load_templates_from_dir(dir_path):
26+
"""Load all JSON files from a directory and merge into single dict."""
27+
result = {}
28+
dir_path = Path(dir_path)
29+
for json_file in sorted(dir_path.glob('*.json')):
30+
if json_file.name == 'metadata.json':
31+
continue
32+
data = load_template(json_file)
33+
result.update(data)
34+
return result
35+
36+
37+
def diff_templates(old, new):
38+
"""Compare two template dicts and return differences."""
39+
old_keys = set(old.keys())
40+
new_keys = set(new.keys())
41+
42+
added = new_keys - old_keys
43+
removed = old_keys - new_keys
44+
common = old_keys & new_keys
45+
46+
modified = []
47+
for k in sorted(common):
48+
old_en = old[k].get('en', '')
49+
new_en = new[k].get('en', '')
50+
if old_en != new_en:
51+
modified.append({
52+
'key': k,
53+
'old': old_en,
54+
'new': new_en,
55+
})
56+
57+
return {
58+
'added': sorted(added),
59+
'removed': sorted(removed),
60+
'modified': modified,
61+
'stats': {
62+
'old_count': len(old_keys),
63+
'new_count': len(new_keys),
64+
'added': len(added),
65+
'removed': len(removed),
66+
'modified': len(modified),
67+
}
68+
}
69+
70+
71+
def main():
72+
import argparse
73+
parser = argparse.ArgumentParser(
74+
description='Diff WLED i18n templates between versions'
75+
)
76+
parser.add_argument(
77+
'--old', required=True,
78+
help='Old template JSON file or directory'
79+
)
80+
parser.add_argument(
81+
'--new', required=True,
82+
help='New template JSON file or directory'
83+
)
84+
parser.add_argument(
85+
'--locale',
86+
help='Locale to compare (e.g., zh_CN). Only used with directory mode.'
87+
)
88+
args = parser.parse_args()
89+
90+
old_path = Path(args.old)
91+
new_path = Path(args.new)
92+
93+
# Load old templates
94+
if old_path.is_dir():
95+
if args.locale:
96+
locale_dir = old_path / args.locale
97+
if locale_dir.exists():
98+
old = load_templates_from_dir(locale_dir)
99+
else:
100+
old = load_templates_from_dir(old_path)
101+
else:
102+
old = load_templates_from_dir(old_path)
103+
else:
104+
old = load_template(old_path)
105+
106+
# Load new templates
107+
if new_path.is_dir():
108+
if args.locale:
109+
locale_dir = new_path / args.locale
110+
if locale_dir.exists():
111+
new = load_templates_from_dir(locale_dir)
112+
else:
113+
new = load_templates_from_dir(new_path)
114+
else:
115+
new = load_templates_from_dir(new_path)
116+
else:
117+
new = load_template(new_path)
118+
119+
# Compute diff
120+
result = diff_templates(old, new)
121+
122+
# Output
123+
print(json.dumps(result, indent=2, ensure_ascii=False))
124+
125+
# Exit code: 0 if no changes, 1 if changes found
126+
stats = result['stats']
127+
if stats['added'] or stats['removed'] or stats['modified']:
128+
return 1
129+
return 0
130+
131+
132+
if __name__ == '__main__':
133+
sys.exit(main())

0 commit comments

Comments
 (0)