mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-28 03:57:57 -06:00
Merge PR #564
# Conflicts: # README.md # crates/ui-egui/src/i18n.rs # docs/localization.md # docs/parity.md
This commit is contained in:
+5
-3
@@ -29,7 +29,7 @@ duplicate Local entries #22, black GPU exports on an Intel iGPU #78).
|
||||
| **Workflow & library** | ~85% (single machine) | Robust catalog (journal + snapshots, background compaction, crash-tested), 85k-photo libraries stay responsive, Local browsing with automatic cleanup, XMP interop, keywords, smart albums, Move import | Opening an 85k library takes 1.7–4.7 s; no cloud sync (out of scope), no tablet companion (#74, roadmap), shared albums, publish services, tethering |
|
||||
| **Classic modules** | ~30% | Geotagging from GPX track logs, soft proofing (partial), slideshow (basic) | **Map view, Book, Print, Slideshow module, Web, publish services**: ~40 tracker rows ⬜ |
|
||||
| **HDR & video** | 0% | | HDR edit/display/export; video play/trim/edit/export |
|
||||
| **Platform & robustness** | ~70% | macOS native; Windows/Linux builds; web via WASM; no-panic lints workspace-wide, `unsafe` confined to `crates/sysmem`; failed saves are reported; GPU errors fall back to CPU | Windows installer UI unverified on Windows (PR #79); GPU path proven only on Apple + user reports; English, Simplified/Traditional Chinese, Japanese, Brazilian Portuguese and German UI (see docs/localization.md); remaining technical errors; Traditional Chinese uses the Simplified Chinese font until craft-fonts has a TC face; accessibility partial; headless UI tests time out under machine load |
|
||||
| **Platform & robustness** | ~70% | macOS native; Windows/Linux builds; web via WASM; no-panic lints workspace-wide, `unsafe` confined to `crates/sysmem`; failed saves are reported; GPU errors fall back to CPU | Windows installer UI unverified on Windows (PR #79); GPU path proven only on Apple + user reports; English, Simplified/Traditional Chinese, Japanese, Brazilian Portuguese, Spanish, German, Russian and Ukrainian UI (see docs/localization.md); remaining technical errors; Traditional Chinese uses the Simplified Chinese font until craft-fonts has a TC face; accessibility partial; headless UI tests time out under machine load |
|
||||
|
||||
### By kind of user
|
||||
|
||||
@@ -100,7 +100,7 @@ yet guarantee original-pixel detail; visible-region rendering remains a gap (LR-
|
||||
| M13 | Merge | HDR merge (deghost), panorama (projections, boundary warp, fill edges), HDR panorama | 10–15 | ✅ |
|
||||
| M14 | Video | import/playback/trim via FilmCraft crates, global edits + presets on video, video export | 6–10 | ⬜ |
|
||||
| M15 | Classic modules | Map, Book, Slideshow, Print, Web; smart collections, stacks, virtual copies, publish services, tethering | 25–40 | 🚧 (smart albums, stacks, virtual copies, compare/survey ✅; Map/Book/Slideshow/Print/Web ⬜) |
|
||||
| M16 | 1.0 polish | preferences, shortcut editor, accessibility, localization, packaging (dmg/msi/AppImage/web), hardening | 10–20 | 🚧 (settings, keyboard shortcuts sheet, packaging basics ✅; English/Chinese (Simplified, Traditional)/Japanese/Brazilian Portuguese/German localisation 🟡; accessibility and further locales ⬜) |
|
||||
| M16 | 1.0 polish | preferences, shortcut editor, accessibility, localization, packaging (dmg/msi/AppImage/web), hardening | 10–20 | 🚧 (settings, keyboard shortcuts sheet, packaging basics ✅; English/Chinese (Simplified, Traditional)/Japanese/Brazilian Portuguese/Spanish/German/Russian/Ukrainian localisation 🟡; accessibility and further locales ⬜) |
|
||||
|
||||
## Parity estimate (feature count updated 2026-10-08; effort estimate from 2026-10-02)
|
||||
|
||||
@@ -233,7 +233,9 @@ database is read-only; LightCraft owns subsequent edits. See `docs/lightroom-cat
|
||||
English, Simplified Chinese, Traditional Chinese (Taiwan) and Japanese interface languages are persisted in UI state, and the
|
||||
language table (`crates/ui-egui/src/i18n.rs`) drives the menus, settings and fonts, so another
|
||||
language is a table entry plus two catalogs (docs/localization.md). Core menus and panels are
|
||||
translated; untranslated text retains English. CJK glyphs (Chinese: Noto Sans CJK SC; Japanese UI:
|
||||
translated; untranslated text retains English. Ukrainian covers all existing catalog keys, command/control/rule labels
|
||||
and format messages, with count-neutral labels and numeric dates; technical errors from lower layers remain English.
|
||||
CJK glyphs (Chinese: Noto Sans CJK SC; Japanese UI:
|
||||
BIZ UDPGothic; watermarks: BIZ UDMincho) come from storytold/craft-fonts, embedded by builds made
|
||||
with the optional `CRAFT_FONTS_DIR` input (all releases), so no system fonts are needed. Text watermarks now accept
|
||||
`vertical: true` in export JSON/presets and expose an orientation selector. Japanese
|
||||
|
||||
@@ -24,6 +24,7 @@ covers a whole directory), or if a licence file referenced in the Licence column
|
||||
| `docs/images/*.jpg` (UI) | LightCraft application screenshots | LightCraft contributors | original work (captured with `docs/showcase/`) | MIT OR Apache-2.0 | 2026-09-30 | n/a |
|
||||
| `docs/brand/` (artcraft-logo and artcraft-mark, SVG and PNG) | ArtCraft wordmark and mark | ArtCraft Team | original work (https://getartcraft.com/) | ArtCraft trademark, see `docs/brand/LICENSE-brand.txt` (not open source) | 2026-10-01 | none |
|
||||
| craft-fonts: optional build input, not files in this repository | BIZ UDPGothic (Regular, Bold), BIZ UDMincho Regular, Shippori Mincho Regular, Noto Sans CJK SC Regular | Morisawa Inc. / The BIZ UDGothic and BIZ UDMincho Project Authors; The Shippori Mincho Project Authors; Google and Adobe (Noto Sans CJK, maintainer-approved exception; see AGENTS.md) (per craft-fonts' attribution) | https://github.com/storytold/craft-fonts/blob/main/ATTRIBUTION.md | SIL Open Font License 1.1 (each font's OFL.txt in craft-fonts; release packages ship it as OFL-<family>.txt) | 2026-10-06 | none; native builds made with `CRAFT_FONTS_DIR` embed the manifest fonts (all official releases); the web build embeds BIZ UDPGothic Regular and ships Noto Sans CJK SC Regular as a separate font file |
|
||||
| `crates/ui-egui/locales/uk.json`, `crates/ui-egui/locales/uk-formats.json` | Ukrainian interface catalogs | @dmatviichuk with Codex; PhotoCraft, FilmCraft and VectorCraft translation contributors for reused labels | original translations from English source; 337 initial English-key matches reviewed from [PhotoCraft 69c41f3](https://github.com/storytold/photocraft/blob/69c41f3523e147eb215da8c7b4c29b18b35d1c63/crates/ui-egui/src/i18n/uk.tsv), [FilmCraft 4854c27](https://github.com/storytold/filmcraft/blob/4854c277536fe8118257362304b6a7d82d8e704b/crates/ui-egui/src/i18n/uk.tsv), [VectorCraft 4b44265](https://github.com/storytold/vectorcraft/blob/4b44265965fdd6ffedfd6182a892c1a6c360d344/crates/ui-egui/src/i18n/uk.tsv) | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`, also the source repositories' licences) | 2026-10-09 | adapted to LightCraft photo-development context, completed catalogs, reviewed spelling and placeholders |
|
||||
|
||||
Notes
|
||||
- Fonts authored or published by Adobe (Source Sans/Serif/Code, Source Han, …) are not used, even though some are
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
{
|
||||
" ({unsure} with several look-alike files: use Locate)": " ({unsure} із кількома схожими файлами: використайте «Знайти»)",
|
||||
" ({} in Recently Deleted)": " ({} у нещодавно видалених)",
|
||||
" · {:.0}% clipped": " · {:.0}% відсічено",
|
||||
" · {failed} couldn't be rendered": " · не вдалося відтворити: {failed}",
|
||||
" · {skipped} skipped (file exists)": " · пропущено: {skipped} (файл існує)",
|
||||
" · {} duplicate{} skipped": " · дублікатів пропущено: {}{:.0}",
|
||||
" · {} failed: {}": " · помилок: {}: {}",
|
||||
" · {} moved": " · переміщено: {}",
|
||||
" · {} not readable": " · не читається: {}",
|
||||
" · {} original{} left at the source": " · оригіналів залишено в джерелі: {}{:.0}",
|
||||
" · {} restored from Recently Deleted": " · відновлено з нещодавно видалених: {}",
|
||||
" — not carried over: {}{}": " — не перенесено: {}{}",
|
||||
"+{} more": "+{} ще",
|
||||
", {failed} file{} not readable": ", файлів не читається: {failed}{:.0}",
|
||||
"; {kept} already had a location": "; уже мали розташування: {kept}",
|
||||
"; {outside} outside its time range": "; поза часовим діапазоном маршруту: {outside}",
|
||||
"@{}: {} PRs, {} commits, +{} / −{} lines (Δ {}), +{} / −{} binary assets, {} – {}": "@{}: PR: {}, комітів: {}, рядків: +{} / −{} (Δ {}), двійкових ресурсів: +{} / −{}, {} – {}",
|
||||
"Add {local_n} to My Photos": "Додати до моїх фото: {local_n}",
|
||||
"Add {} Photo{}": "Додати фото: {}{:.0}",
|
||||
"Add {} Photo{}, restore {}": "Додати фото: {}{:.0}, відновити: {}",
|
||||
"Add “{}” to Favorites": "Додати «{}» до улюблених",
|
||||
"Added {n} photo{} to My Photos": "До моїх фото додано: {n}{:.0}",
|
||||
"Added {n} photo{} to “{}”": "До «{1}» додано фото: {n}{0:.0}",
|
||||
"Added {what} to {name}": "{what}: додано до {name}",
|
||||
"Adding photos… {} of {}": "Додавання фото… {} з {}",
|
||||
"All {n} selected photos move by the same amount.": "Усі вибрані фото ({n}) зміщуються на однакове значення.",
|
||||
"Already in the library, in Recently Deleted: {} photo{} selected · right-click ▸ Restore, or Delete Permanently to import afresh": "Уже в бібліотеці, у нещодавно видалених: вибрано фото: {}{:.0} · права кнопка → Відновити або Видалити назавжди, щоб імпортувати заново",
|
||||
"Already in the library: {} photo{} selected in All Photos": "Уже в бібліотеці: вибрано фото в усіх фото: {}{:.0}",
|
||||
"Auto ({})": "Автоматично ({})",
|
||||
"Auto Import: added {} photo{}": "Автоімпорт: додано фото: {}{:.0}",
|
||||
"Auto Sync: edits apply to {n} photos": "Автосинхронізація: зміни застосовуються до фото: {n}",
|
||||
"Automatic ({} MB)": "Автоматично ({} МБ)",
|
||||
"Build 1:1 Previews ({scope})": "Створити перегляди 1:1 ({scope})",
|
||||
"Build Smart Previews ({scope})": "Створити смарт-перегляди ({scope})",
|
||||
"Build Standard-Sized Previews ({scope})": "Створити перегляди стандартного розміру ({scope})",
|
||||
"Building previews for {} photos…": "Створення переглядів для фото: {}…",
|
||||
"Click {}:1": "Клацання {}:1",
|
||||
"Couldn't open the editor: {e}": "Не вдалося відкрити редактор: {e}",
|
||||
"Create {n} Virtual Copies": "Створити віртуальні копії: {n}",
|
||||
"Creates {} stacks from {} of {scope}": "Буде створено стосів: {} із {} ({scope})",
|
||||
"Decoder: {reason}": "Декодер: {reason}",
|
||||
"Delete \"{label}\"": "Видалити «{label}»",
|
||||
"Delete \"{label}\" (and the mask)": "Видалити «{label}» (і маску)",
|
||||
"Delete {n} Photos": "Видалити фото: {n}",
|
||||
"Delete “{current}”": "Видалити «{current}»",
|
||||
"Discard Smart Previews ({scope})": "Видалити смарт-перегляди ({scope})",
|
||||
"Done ({what}): {done} photo{plural}": "Готово ({what}): фото: {done}{plural:.0}",
|
||||
"Duplicate \"{label}\"": "Дублювати «{label}»",
|
||||
"Example: {example}": "Приклад: {example}",
|
||||
"Exported {n} photo{}": "Експортовано фото: {n}{:.0}",
|
||||
"Exported {ok} of {total} photo{}": "Експортовано фото: {ok} із {total}{:.0}",
|
||||
"Exported {} point curve preset{}": "Експортовано наборів налаштувань точкової кривої: {}{:.0}",
|
||||
"Exported {} preset{}": "Експортовано наборів налаштувань: {}{:.0}",
|
||||
"Exporting {} of {total}": "Експорт: {} із {total}",
|
||||
"Filter bar — {active} active filter{}": "Панель фільтрів — активних фільтрів: {active}{:.0}",
|
||||
"Focus {:.0}": "Фокус {:.0}",
|
||||
"Folder “{}” (and its subfolders)": "Тека «{}» (і вкладені теки)",
|
||||
"Found {n} missing photo{}; {left} still missing{unsure}": "Знайдено відсутніх фото: {n}{:.0}; усе ще відсутні: {left}{unsure}",
|
||||
"Healed {n} dust spot{}": "Усунено пилових плям: {n}{:.0}",
|
||||
"Import cancelled · {} photo{} added": "Імпорт скасовано · додано фото: {}{:.0}",
|
||||
"Import failed: {e}": "Помилка імпорту: {e}",
|
||||
"Imported {n} point curve preset{}": "Імпортовано наборів налаштувань точкової кривої: {n}{:.0}",
|
||||
"Imported {n} preset{}": "Імпортовано наборів налаштувань: {n}{:.0}",
|
||||
"Imported {n} preset{}, {f} file{} not readable": "Імпортовано наборів налаштувань: {n}{:.0}, файлів не читається: {f}{:.0}",
|
||||
"Imported {profiles} profile{} (Profile browser ▸ their groups)": "Імпортовано профілів: {profiles}{:.0} (Оглядач профілів → їхні групи)",
|
||||
"Imported {} photo{}": "Імпортовано фото: {}{:.0}",
|
||||
"Insert at the cursor (also written {})": "Вставити в позицію курсора (також записується як {})",
|
||||
"Label Set: {name}": "Набір міток: {name}",
|
||||
"LightCraft could not open its window: {e}": "LightCraft не вдалося відкрити вікно: {e}",
|
||||
"LightCraft could not open its window: {e}\n\nThe log file has the details: {path}": "LightCraft не вдалося відкрити вікно: {e}\n\nПодробиці у файлі журналу: {path}",
|
||||
"Lightroom catalog inspected: {photos} photos, {collections} collections, {missing} missing": "Каталог Lightroom перевірено: фото — {photos}, колекцій — {collections}, відсутні — {missing}",
|
||||
"Lightroom import complete: {imported} photos added": "Імпорт Lightroom завершено: додано фото — {imported}",
|
||||
"Lightroom import complete: {imported} photos added, {failed} failed": "Імпорт Lightroom завершено: додано фото — {imported}, помилок — {failed}",
|
||||
"Lightroom import complete: {imported} photos added; index warning: {warning}": "Імпорт Lightroom завершено: додано фото — {imported}; попередження індексу: {warning}",
|
||||
"Lightroom import failed: {e}{warning}": "Помилка імпорту Lightroom: {e}{warning}",
|
||||
"Lightroom inspection failed: {e}{warning}": "Помилка перевірки Lightroom: {e}{warning}",
|
||||
"MIT OR Apache-2.0. Fonts: {} (OFL). Icons: original.": "MIT OR Apache-2.0. Шрифти: {} (OFL). Значки: оригінальні.",
|
||||
"Matches: {}": "Збігів: {}",
|
||||
"Merging… {stage} {:.0}%": "Об’єднання… {stage} {:.0}%",
|
||||
"Move {what} to Recently Deleted?": "Перемістити {what} до нещодавно видалених?",
|
||||
"Named ({named_n})": "З іменами ({named_n})",
|
||||
"Painting “{k}”: click photos to add or remove it · Esc stops": "Призначення «{k}»: клацайте фото, щоб додати або вилучити його · Esc завершує",
|
||||
"Paste into {n} photo{} — only settings that were copied are pasted": "Вставка у фото: {n}{:.0} — вставляються лише скопійовані налаштування",
|
||||
"Preset: {name}": "Набір налаштувань: {name}",
|
||||
"Preview only — editing the camera's embedded JPEG ({})": "Лише перегляд — редагування вбудованого JPEG камери ({})",
|
||||
"Preview only — {}": "Лише перегляд — {}",
|
||||
"Previews ready for {done} photo{plural}": "Перегляди готові для фото: {done}{plural:.0}",
|
||||
"Profile: {}": "Профіль: {}",
|
||||
"Proof Copy ({})": "Пробна копія ({})",
|
||||
"Proof Preview · {}": "Пробний перегляд · {}",
|
||||
"Quick Develop · {n} photos": "Швидка обробка · фото: {n}",
|
||||
"Quick Develop: {name}": "Швидка обробка: {name}",
|
||||
"Rated {}": "Оцінено: {}",
|
||||
"Reading Lightroom catalog… {done} of {total}": "Читання каталогу Lightroom… {done} із {total}",
|
||||
"Reading photos… {done} of {total}": "Читання фото… {done} із {total}",
|
||||
"Reading photos… {} of {}": "Читання фото… {} із {}",
|
||||
"Redo {}": "Повторити {}",
|
||||
"Remove “{name}” and its {count} photo{} from the library?": "Вилучити «{name}» і фото ({count}){:.0} з бібліотеки?",
|
||||
"Remove “{}” from Favorites": "Вилучити «{}» з улюблених",
|
||||
"Removed from {}": "Вилучено з {}",
|
||||
"Removed {what} from {name}": "{what}: вилучено з {name}",
|
||||
"Rename {n} Photos…": "Перейменувати фото: {n}…",
|
||||
"Rename “{name}”": "Перейменувати «{name}»",
|
||||
"Renames “{from}” on {n} photo{} (keywords below it too). Use | for levels, e.g. Travel|Italy. An existing name merges the two.": "Перейменовує «{from}» на фото ({n}){:.0} (також вкладені ключові слова). Використовуйте | для рівнів, наприклад Подорожі|Італія. Наявна назва об’єднує обидва слова.",
|
||||
"Rendering on the CPU: {why}": "Рендеринг на ЦП: {why}",
|
||||
"Replace {} with:": "Замінити {} на:",
|
||||
"Reset {name}": "Скинути: {name}",
|
||||
"Reset {} Channel": "Скинути канал: {}",
|
||||
"Restore {}": "Відновити {}",
|
||||
"Restore {} Photo{}": "Відновити фото: {}{:.0}",
|
||||
"Saved export preset “{}”": "Набір налаштувань експорту «{}» збережено",
|
||||
"Scores {scope} for focus and exposure and finds similar shots taken within seconds of each other (bursts).": "Оцінює фокус і експозицію ({scope}) та знаходить подібні знімки з різницею в кілька секунд (серії).",
|
||||
"Show {hidden} hidden location{}": "Показати приховані розташування: {hidden}{:.0}",
|
||||
"Showing {n} of {}": "Показано {n} із {}",
|
||||
"Smart album: {}": "Смарт-альбом: {}",
|
||||
"Tagged {n} photo{} from the tracklog": "Позначено за журналом маршруту фото: {n}{:.0}",
|
||||
"The SAM 3 download failed: {e}": "Не вдалося завантажити SAM 3: {e}",
|
||||
"Undo {}": "Скасувати {}",
|
||||
"Unknown tag{s} {} stay{} as typed (see Tags)": "Невідомі теги{s:.0} {} залишаються як введено{:.0} (див. «Теги»)",
|
||||
"Updated “{name}”": "«{name}» оновлено",
|
||||
"Version {} — a clean-room, pure-Rust photo library and raw developer.": "Версія {} — незалежно розроблена бібліотека фото й обробка RAW на чистому Rust.",
|
||||
"Version: {}": "Версія: {}",
|
||||
"Virtual copy “{name}” of {of}": "Віртуальна копія «{name}» з {of}",
|
||||
"Working on {what} for {} photos…": "{what}: обробка фото ({})…",
|
||||
"none (CPU rendering): {why}": "немає (рендеринг на ЦП): {why}",
|
||||
"the {n} selected photos": "вибрані фото: {n}",
|
||||
"the {} photos in view": "фото в поданні: {}",
|
||||
"these {count} photos": "ці фото ({count})",
|
||||
"{count} smart preview{} · {:.1} MB": "Смарт-переглядів: {count}{:.0} · {:.1} МБ",
|
||||
"{f} folders (and their subfolders)": "Тек: {f} (і їхніх вкладених тек)",
|
||||
"{label} Grading": "Колірне тонування: {label}",
|
||||
"{label} {d:+} on every selected photo": "{label} {d:+} на кожному вибраному фото",
|
||||
"{label} {index} of {n}": "{label} {index} із {n}",
|
||||
"{label}, {n} photos": "{label}, фото: {n}",
|
||||
"{matching} of {t} photos": "Фото: {matching} із {t}",
|
||||
"{matching} photos": "Фото: {matching}",
|
||||
"{mode} spot {} of {}": "{mode}: ділянка {} із {}",
|
||||
"{month}": "{month}",
|
||||
"{month} {year}": "{month:02}.{year}",
|
||||
"{msg} ({failed} failed)": "{msg} (помилок: {failed})",
|
||||
"{name} Label": "Мітка: {name}",
|
||||
"{name} Off": "{name}: вимкнено",
|
||||
"{name} On": "{name}: увімкнено",
|
||||
"{name} opened for editing; it is stacked with the original": "{name} відкрито для редагування й об’єднано в стос з оригіналом",
|
||||
"{name} — double-click to reset it": "{name} — подвійне клацання для скидання",
|
||||
"{n} change{} couldn't be written to disk: {e}\nIf you quit now, {} lost.": "Змін не вдалося записати на диск: {n}{0:.0}: {e}\nЯкщо вийти зараз, їх буде втрачено.{1:.0}",
|
||||
"{n} change{} saved in memory but not written to disk: {e}\nLightCraft retries automatically; quitting now would lose {}.": "Змін збережено в пам’яті, але не записано на диск: {n}{:.0}: {e}\nLightCraft повторює спроби автоматично; вихід зараз призведе до їхньої втрати.{:.0}",
|
||||
"{n} change{} saved in memory but not written to disk: {e} — LightCraft will retry": "Змін збережено в пам’яті, але не записано на диск: {n}{:.0}: {e} — LightCraft повторить спробу",
|
||||
"{n} found · {sel} selected": "Знайдено: {n} · вибрано: {sel}",
|
||||
"{n} photos": "Фото: {n}",
|
||||
"{n} photos in {} are hidden by the filters above": "Фільтри вище приховують фото в «{}»: {n}",
|
||||
"{n} photo{}": "Фото: {n}{:.0}",
|
||||
"{n} photo{} match · updates automatically as photos change": "Відповідних фото: {n}{:.0} · оновлюється автоматично зі зміною фото",
|
||||
"{n} photo{} now · updates automatically as photos change": "Зараз фото: {n}{:.0} · оновлюється автоматично зі зміною фото",
|
||||
"{pid} (missing)": "{pid} (відсутній)",
|
||||
"{sel_n} selected · {counted}": "Вибрано: {sel_n} · {counted}",
|
||||
"{stage}… {:.0}%": "{stage}… {:.0}%",
|
||||
"{verb} {n} Photo{}": "{verb} (фото: {n}){:.0}",
|
||||
"{weekday}, {day}": "{weekday}, {day}",
|
||||
"{weekday}, {day} {month} {year}": "{weekday}, {day:02}.{month:02}.{year}",
|
||||
"{what} merge added": "{what}: об’єднання додано",
|
||||
"{what} merge failed: {e}": "{what}: помилка об’єднання: {e}",
|
||||
"{year}": "{year}",
|
||||
"{{date:…}} directives: {}": "Директиви {{date:…}}: {}",
|
||||
"{} and higher": "{} і вище",
|
||||
"{} contributors · {} commits": "Учасників: {} · комітів: {}",
|
||||
"{} failed": "Помилок: {}",
|
||||
"{} files": "Файлів: {}",
|
||||
"{} files and {f} folder{}": "Файлів: {}, тек: {f}{:.0}",
|
||||
"{} item{}": "Елементів: {}{:.0}",
|
||||
"{} of 4 guides — drag along lines that should be vertical or horizontal.": "Напрямних: {} із 4 — проведіть уздовж ліній, які мають бути вертикальними або горизонтальними.",
|
||||
"{} photos": "Фото: {}",
|
||||
"{} photos · exposures {} EV": "Фото: {} · експозиції {} EV",
|
||||
"{} photos · {} · {:.0}° × {:.0}°": "Фото: {} · {} · {:.0}° × {:.0}°",
|
||||
"{} photo{} not readable": "Не читається фото: {}{:.0}",
|
||||
"{} sample{}": "Зразків: {}{:.0}",
|
||||
"{} sec": "{} с",
|
||||
"{} spot(s) on this photo": "Ділянок на цьому фото: {}",
|
||||
"{} — show their photos": "{} — показати фото цієї особи",
|
||||
"· {dups} already in the library": "· уже в бібліотеці: {dups}",
|
||||
"· {n} photo{}": "· фото: {n}{:.0}",
|
||||
"… and {more} more": "… і ще {more}",
|
||||
"LightCraft can't decode this raw variant yet ({}). You're editing the camera's embedded JPEG preview, which already includes the camera's picture style (e.g. Monochrome) and white balance.": "LightCraft ще не може декодувати цей варіант RAW ({}). Ви редагуєте вбудований JPEG-перегляд камери, який уже містить її стиль зображення (наприклад, монохромний) і баланс білого.",
|
||||
"Filtered: {label}": "Відфільтровано: {label}",
|
||||
"{n} similar photo{} · View ▸ Clear Filters to see all": "Подібних фото: {n}{:.0} · Вигляд → Скинути фільтри, щоб побачити всі",
|
||||
"Moved; {} photo(s) relinked": "Переміщено; повторно пов’язано фото: {}",
|
||||
"{} label (click more labels to show any of them)": "Мітка «{}» (клацніть інші мітки, щоб показати фото з будь-якою з них)",
|
||||
", {} star{}": ", зірок: {}{:.0}",
|
||||
", {} label": ", мітка: {}",
|
||||
"Downloads from {host}. Once it has arrived and checked out, LightCraft installs it, {after}.": "Завантажується з {host}. Після отримання й перевірки LightCraft установлює модель, {after}.",
|
||||
"{:.1} MB": "{:.1} МБ",
|
||||
"Running on the processor: {}.": "Працює на процесорі: {}.",
|
||||
"{} · {}. Kept in the library's “denoise” folder; the oldest ones that no photo uses go first when the limit is reached.": "{} · {}. Зберігається в теці «denoise» бібліотеки; після досягнення ліміту першими видаляються найстаріші зображення, які не використовуються жодним фото.",
|
||||
"Denoising {name}: {done} of {total} tiles{}": "Усунення шуму {name}: ділянки — {done} із {total}{}",
|
||||
"{size} GB": "{size} ГБ",
|
||||
"{} of {} from {host}": "{} із {} з {host}",
|
||||
"{} KB": "{} КБ",
|
||||
"{} MB": "{} МБ",
|
||||
"Making the denoised picture… {}%": "Створення зображення з усуненим шумом… {}%",
|
||||
"{name} · {terms}": "{name} · {terms}",
|
||||
"{n} {noun}": "{noun}: {n}",
|
||||
"Trained on: {provenance}": "Навчено на: {provenance}",
|
||||
" · {queued} waiting": " · очікують: {queued}",
|
||||
"Shows the model's terms, then downloads {} from {host}, installs it and starts using it. Nothing else is sent.": "Показує умови моделі, потім завантажує {} з {host}, установлює й починає використовувати її. Більше нічого не надсилається.",
|
||||
"In line for AI Denoise ({ahead} ahead)…": "У черзі на усунення шуму ШІ (попереду: {ahead})…",
|
||||
"Running on {}.": "Працює на {}.",
|
||||
"{file} · {size}": "{file} · {size}",
|
||||
"• {assumption}": "• {assumption}"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+138
-33
@@ -3,8 +3,8 @@
|
||||
//! A language is one entry in [`language_table!`] below: its BCP-47 code, the endonym shown in the
|
||||
//! Language menus, the ISO 15924 script its text needs (which picks the CJK font fallback), and its
|
||||
//! message catalog (`locales/<code>.json`, embedded at build time). Adding a language is that entry
|
||||
//! plus its catalogs — every menu and settings surface is built from the table, so nothing else in
|
||||
//! the UI changes.
|
||||
//! plus its catalogs and the corresponding language command in `menus.rs`. Settings read the
|
||||
//! locale table, while menus and the control channel share the command mapping.
|
||||
|
||||
use std::{cell::RefCell, collections::BTreeMap, sync::OnceLock};
|
||||
|
||||
@@ -125,6 +125,7 @@ language_table! {
|
||||
De, "de", "Deutsch", "Latn", include_str!("../locales/de.json");
|
||||
Ru, "ru", "Русский", "Cyrl", include_str!("../locales/ru.json");
|
||||
Fr, "fr", "Français", "Latn", include_str!("../locales/fr.json");
|
||||
Uk, "uk", "Українська", "Cyrl", include_str!("../locales/uk.json");
|
||||
}
|
||||
|
||||
// The settings file stores the BCP-47 code (`"zh-hans"`), never the Rust variant name, so a
|
||||
@@ -665,6 +666,7 @@ mod tests {
|
||||
("app.language.spanish", Locale::Es),
|
||||
("app.language.german", Locale::De),
|
||||
("app.language.russian", Locale::Ru),
|
||||
("app.language.ukrainian", Locale::Uk),
|
||||
];
|
||||
// One command per language, and every command reachable from the menu table.
|
||||
assert_eq!(commands.len(), Locale::ALL.len());
|
||||
@@ -818,6 +820,107 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// Ukrainian's coverage of every catalog message and display label. Gaps are reported, not failed
|
||||
/// (like `catalogs_agree_on_placeholders_and_report_gaps`): a feature PR that adds a label doesn't
|
||||
/// have to ship its Ukrainian text, which shows in English until the translation catches up.
|
||||
#[test]
|
||||
fn ukrainian_coverage_of_catalogs_commands_controls_and_rules_is_reported() {
|
||||
let catalog = Locale::Uk.catalog();
|
||||
let keys: std::collections::BTreeSet<&String> = Locale::ALL.iter().flat_map(|language| language.catalog().keys()).collect();
|
||||
let lacking: Vec<_> = keys.into_iter().filter(|key| !catalog.contains_key(key.as_str())).collect();
|
||||
if !lacking.is_empty() {
|
||||
eprintln!("uk lacks {} catalog message(s) (shown in English): {lacking:?}", lacking.len());
|
||||
}
|
||||
let mut labels: Vec<&str> = lightcraft_engine::command_specs().iter().map(|spec| spec.label).collect();
|
||||
labels.extend(crate::menus::ui_commands().map(|command| command.1).filter(|label| !Locale::ALL.iter().any(|locale| locale.name() == *label)));
|
||||
labels.extend(lightcraft_develop::CONTROLS.iter().map(|control| control.label));
|
||||
labels.extend(crate::panels::settings::TABS.iter().map(|(_, label)| *label));
|
||||
for (_, label, kind) in lightcraft_catalog::rules::FIELDS {
|
||||
labels.push(label);
|
||||
labels.extend(lightcraft_catalog::rules::ops_for(*kind).iter().map(|(_, label)| *label));
|
||||
}
|
||||
labels.extend(lightcraft_engine::rename::TOKENS.iter().map(|token| token.meaning));
|
||||
labels.extend(lightcraft_engine::rename::TEMPLATE_NOTES);
|
||||
let missing: Vec<_> = labels.into_iter().filter(|label| !catalog.contains_key(*label)).collect();
|
||||
if !missing.is_empty() {
|
||||
eprintln!("uk lacks {} display label(s) (shown in English): {missing:?}", missing.len());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ukrainian_switches_persists_formats_dates_and_preserves_values() {
|
||||
use crate::control::{ControlRequest, Outcome};
|
||||
let mut app = crate::LightcraftApp::new(lightcraft_engine::Session::with_demo(), Default::default());
|
||||
app.run("app.language.ukrainian", serde_json::json!({})).unwrap();
|
||||
assert_eq!(app.ui.language, Locale::Uk);
|
||||
assert_eq!(crate::menubar::checked(&app, "app.language.ukrainian"), Some(true));
|
||||
assert_eq!(tr("Light"), "Світло");
|
||||
assert_eq!(tr("Landscape"), "Пейзаж");
|
||||
assert_eq!(tr("Home"), "Домашня тека");
|
||||
assert_eq!(date_group_label("2026-09-20", false), "Неділя, 20.09.2026");
|
||||
assert_eq!(date_group_label("2026-09", false), "09.2026");
|
||||
assert_eq!(display_time("2026-09-20T16:04:05"), "Неділя, 20.09.2026 16:04:05");
|
||||
// Count-neutral labels work for every Ukrainian integer category, with no English suffix.
|
||||
for n in [0, 1, 2, 5, 11, 21, 22, 25, 111] {
|
||||
assert_eq!(tr_format!("Imported {} photo{}", n, if n == 1 { "" } else { "s" }), format!("Імпортовано фото: {n}"));
|
||||
assert_eq!(tr_format!("{} files and {f} folder{}", n, "s", f = n), format!("Файлів: {n}, тек: {n}"));
|
||||
let noun = tr(if n == 1 { "cached picture" } else { "cached pictures" });
|
||||
assert_eq!(tr_format!("{n} {noun}", n = n, noun = noun), format!("Кешовані зображення: {n}"));
|
||||
}
|
||||
// This trailing argument is queue progress, not an English plural suffix.
|
||||
let queued = tr_format!(" · {queued} waiting", queued = 5);
|
||||
assert_eq!(
|
||||
tr_format!("Denoising {name}: {done} of {total} tiles{}", queued, name = "IMG.CR3", done = 1, total = 4),
|
||||
"Усунення шуму IMG.CR3: ділянки — 1 із 4 · очікують: 5"
|
||||
);
|
||||
let name = "Color {title} 日本語";
|
||||
assert_eq!(tr_format!("Added {n} photo{} to “{}”", "s", name, n = 22), format!("До «{name}» додано фото: 22"));
|
||||
assert_eq!(builtin_label("Warm Glow", false), "Warm Glow");
|
||||
assert_eq!(builtin_label("Warm Glow", true), "Тепле сяйво");
|
||||
assert_eq!(tr("photo-{title}.jpg"), "photo-{title}.jpg");
|
||||
assert_eq!(tr("develop.set"), "develop.set");
|
||||
let saved = serde_json::to_string(&app.ui).unwrap();
|
||||
assert!(saved.contains(r#""language":"uk""#));
|
||||
assert_eq!(serde_json::from_str::<crate::state::UiState>(&saved).unwrap().language, Locale::Uk);
|
||||
let ctx = egui::Context::default();
|
||||
for code in ["uk", "uk-UA", "uk_UA.UTF-8", "uk-Cyrl-UA"] {
|
||||
assert_eq!(Locale::parse_tag(code), Some(Locale::Uk));
|
||||
let (request, _) = ControlRequest::new("ui.set", serde_json::json!({"language": code}));
|
||||
let Outcome::Done(reply) = crate::control::handle(&mut app, &ctx, &request) else { panic!("expected reply") };
|
||||
assert_eq!(reply["ok"], true);
|
||||
assert_eq!(language(), Locale::Uk);
|
||||
}
|
||||
set_language(Locale::En);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ukrainian_panels_dialogs_and_letters_paint_with_bundled_fonts() {
|
||||
let ctx = fonts_ctx(&[]);
|
||||
let mut app = crate::LightcraftApp::new(lightcraft_engine::Session::with_demo(), Default::default());
|
||||
app.ui.left_panel = true;
|
||||
let text = painted_text(&ctx, &mut app, Locale::Uk);
|
||||
assert!(text.contains("Мої фото") && text.contains("Усі фото"), "{text}");
|
||||
for (command, title) in [("app.about", "Про LightCraft"), ("app.shortcuts", "Клавіатурні скорочення"), ("app.settings", "Налаштування")]
|
||||
{
|
||||
app.ui.dialog = None;
|
||||
app.run(command, serde_json::json!({})).unwrap();
|
||||
let text = painted_text(&ctx, &mut app, Locale::Uk);
|
||||
assert!(text.contains(title), "{command}: {text}");
|
||||
}
|
||||
ctx.fonts_mut(|fonts| {
|
||||
for family in [egui::FontFamily::Proportional, egui::FontFamily::Name(crate::theme::FONT_SEMIBOLD.into())] {
|
||||
let font = egui::FontId::new(13.0, family);
|
||||
for ch in "ҐґЄєІіЇї’«»"
|
||||
.chars()
|
||||
.chain(Locale::Uk.catalog().values().flat_map(|text| text.chars()).filter(|ch| ('\u{0400}'..='\u{04ff}').contains(ch)))
|
||||
{
|
||||
assert!(fonts.has_glyph(&font, ch), "missing Ukrainian glyph {ch}");
|
||||
}
|
||||
}
|
||||
});
|
||||
set_language(Locale::En);
|
||||
}
|
||||
|
||||
/// Every command, control, rule and rename label, and every line of What's New, is a message a
|
||||
/// catalog can translate. What a language lacks is reported, not failed (like
|
||||
/// `catalogs_agree_on_placeholders_and_report_gaps`): a feature PR doesn't have to ship every
|
||||
@@ -959,7 +1062,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn german_buttons_fit_the_bottom_bar_and_export_dialog() {
|
||||
fn german_and_ukrainian_buttons_fit_the_bottom_bar_and_export_dialog() {
|
||||
fn collect(shape: &egui::epaint::Shape, bounds: &mut Vec<(String, egui::Rect)>) {
|
||||
match shape {
|
||||
egui::epaint::Shape::Text(shape) => {
|
||||
@@ -969,38 +1072,40 @@ mod tests {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let ctx = fonts_ctx(&[]);
|
||||
let services = crate::Services { pick_folder: Some(Box::new(|| None)), ..Default::default() };
|
||||
let mut app = crate::LightcraftApp::new(lightcraft_engine::Session::with_demo(), services);
|
||||
app.ui.language = Locale::De;
|
||||
let mut bounds = Vec::new();
|
||||
for export in [false, true] {
|
||||
if export {
|
||||
app.run("dialog.export", serde_json::json!({})).unwrap();
|
||||
}
|
||||
for frame in 0..4 {
|
||||
let input = crate::headless::HeadlessView::raw_input(egui::vec2(1600.0, 1000.0), 1.0, frame as f64 / 60.0, vec![]);
|
||||
let mut out = ctx.run_ui(input, |ui| {
|
||||
app.logic(ui.ctx());
|
||||
app.ui(ui);
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
bounds.clear();
|
||||
for shape in out.shapes {
|
||||
collect(&shape.shape, &mut bounds);
|
||||
for language in [Locale::De, Locale::Uk] {
|
||||
let ctx = fonts_ctx(&[]);
|
||||
let services = crate::Services { pick_folder: Some(Box::new(|| None)), ..Default::default() };
|
||||
let mut app = crate::LightcraftApp::new(lightcraft_engine::Session::with_demo(), services);
|
||||
app.ui.language = language;
|
||||
let mut bounds = Vec::new();
|
||||
for export in [false, true] {
|
||||
if export {
|
||||
app.run("dialog.export", serde_json::json!({})).unwrap();
|
||||
}
|
||||
}
|
||||
let rect = |id: &str| app.widgets.iter().find(|entry| entry.0 == id).unwrap().1;
|
||||
if export {
|
||||
let dialog = rect("dialog:window");
|
||||
for id in ["button:exportNamingTags", "button:exportChooseFolder", "button:exportSavePreset"] {
|
||||
assert!(dialog.contains_rect(rect(id)), "{id} spills outside {dialog:?}: {:?}", rect(id));
|
||||
for frame in 0..4 {
|
||||
let input = crate::headless::HeadlessView::raw_input(egui::vec2(1600.0, 1000.0), 1.0, frame as f64 / 60.0, vec![]);
|
||||
let mut out = ctx.run_ui(input, |ui| {
|
||||
app.logic(ui.ctx());
|
||||
app.ui(ui);
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
bounds.clear();
|
||||
for shape in out.shapes {
|
||||
collect(&shape.shape, &mut bounds);
|
||||
}
|
||||
}
|
||||
let rect = |id: &str| app.widgets.iter().find(|entry| entry.0 == id).unwrap().1;
|
||||
if export {
|
||||
let dialog = rect("dialog:window");
|
||||
for id in ["button:exportNamingTags", "button:exportChooseFolder", "button:exportSavePreset"] {
|
||||
assert!(dialog.contains_rect(rect(id)), "{id} spills outside {dialog:?}: {:?}", rect(id));
|
||||
}
|
||||
} else {
|
||||
let button = rect("button:copySettings");
|
||||
let (_, text) = bounds.iter().find(|entry| entry.0 == language.tr("Copy Edit Settings")).unwrap();
|
||||
assert!(button.contains_rect(*text), "{language:?} copy caption overflows its button: {text:?} vs {button:?}");
|
||||
assert!(!button.intersects(rect("icon:copyGear")));
|
||||
}
|
||||
} else {
|
||||
let button = rect("button:copySettings");
|
||||
let (_, text) = bounds.iter().find(|entry| entry.0 == "Bearbeitungseinstellungen kopieren").unwrap();
|
||||
assert!(button.contains_rect(*text), "German copy caption overflows its button: {text:?} vs {button:?}");
|
||||
assert!(!button.intersects(rect("icon:copyGear")));
|
||||
}
|
||||
}
|
||||
set_language(Locale::En);
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::state::{BeforeAfter, Dialog, RightPanel, ViewMode, Zoom};
|
||||
pub type UiCommand = (&'static str, &'static str, Option<&'static str>, &'static str);
|
||||
|
||||
/// The "Edit → Language" entries, one per language in [`crate::i18n::Locale::ALL`], so a language
|
||||
/// added to the table shows up in the menu (and in the control channel's command list) by itself.
|
||||
/// has a matching menu command (also listed through the control channel). Tests check coverage.
|
||||
pub const LANGUAGE_COMMANDS: &[UiCommand] = &[
|
||||
("app.language.english", crate::i18n::Locale::En.name(), None, "Edit>Language"),
|
||||
("app.language.simplifiedChinese", crate::i18n::Locale::ZhHans.name(), None, "Edit>Language"),
|
||||
@@ -23,6 +23,7 @@ pub const LANGUAGE_COMMANDS: &[UiCommand] = &[
|
||||
("app.language.spanish", crate::i18n::Locale::Es.name(), None, "Edit>Language"),
|
||||
("app.language.german", crate::i18n::Locale::De.name(), None, "Edit>Language"),
|
||||
("app.language.russian", crate::i18n::Locale::Ru.name(), None, "Edit>Language"),
|
||||
("app.language.ukrainian", crate::i18n::Locale::Uk.name(), None, "Edit>Language"),
|
||||
];
|
||||
|
||||
/// Every UI command: the languages, then everything else. `xtask parity` reads both tables from
|
||||
@@ -44,6 +45,7 @@ pub fn language_from_command(id: &str) -> Option<crate::i18n::Locale> {
|
||||
"app.language.spanish" => Some(crate::i18n::Locale::Es),
|
||||
"app.language.german" => Some(crate::i18n::Locale::De),
|
||||
"app.language.russian" => Some(crate::i18n::Locale::Ru),
|
||||
"app.language.ukrainian" => Some(crate::i18n::Locale::Uk),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,14 +29,17 @@ are in [`localization-zh-hans.md`](localization-zh-hans.md), [`localization-zh-h
|
||||
PtBr, "pt-br", "Português (Brasil)", "Latn", include_str!("../locales/pt-br.json");
|
||||
De, "de", "Deutsch", "Latn", include_str!("../locales/de.json");
|
||||
Ru, "ru", "Русский", "Cyrl", include_str!("../locales/ru.json");
|
||||
Uk, "uk", "Українська", "Cyrl", include_str!("../locales/uk.json");
|
||||
}
|
||||
```
|
||||
|
||||
The fields are the BCP-47 code (also the settings-file value), the **endonym** shown in the
|
||||
Language menu, the ISO 15924 script (`Latn`, `Jpan`, `Hans`, `Hant`, `Kore`…), and the embedded
|
||||
catalog. `build.rs` picks up `<code>-formats.json` by name, so the macro and the format lookup
|
||||
follow automatically — nothing else in the UI needs to change. The menus, the settings row, the
|
||||
keyboard-shortcut sheet and the control channel all read the table.
|
||||
follow automatically. Add the corresponding `app.language.<name>` entry to
|
||||
`LANGUAGE_COMMANDS` and `language_from_command` in `src/menus.rs`, and to the
|
||||
`language_commands_cover_every_language` test. Settings read the locale table; menus and
|
||||
the control channel share the language-command mapping.
|
||||
3. **Make sure the glyphs exist.** The script drives which craft-fonts faces the UI installs: a
|
||||
language whose script has no face in the build shows boxes (the test says so instead of failing).
|
||||
Fonts live in [storytold/craft-fonts](https://github.com/storytold/craft-fonts), never here; a
|
||||
|
||||
+2
-2
@@ -528,8 +528,8 @@ under the UI command that wraps it (a no-op error); the UI command now wins.
|
||||
| LR-BEHAV-TEXTFIELD | Text fields behave alike | P1 | 🟡 | `crates/ui-egui/src/text_field.rs`, `crates/ui-egui/src/panels/topbar.rs`, `crates/ui-egui/src/widgets.rs`, `crates/ui-egui/src/panels/right.rs` (`meta_field`), `crates/ui-egui/src/panels/dialogs.rs`, `crates/ui-egui/src/panels/unnamed.rs` | one shared field: right-click Cut / Copy / Paste / Select All (no Paste in the browser), Esc gives back the text from before the edit, Return / leaving / Esc reported to the caller, optional select-on-focus; agents use `ui.clipboard` (MCP `clipboard`) and `ui.inspect` → `copied`. Used by Search Photos, slider values, the Info panel's metadata fields (multi-line captions), the one-field dialogs (New / Rename Album, New Smart Album, Rename / Merge Keywords, text prompts) and the unnamed faces' name field so far; the other text fields (most dialogs, import, settings, masking, keywords …) still use egui's own and have no menu |
|
||||
| LR-BEHAV-HEADLESS | Headless UI snapshots | P2 | ✅ | `apps/lightcraft-cli/src/main.rs`, `crates/ui-egui/src/headless.rs`, `docs/control-protocol.md` | `snapshot` rasterizes the UI and defaults photo development to the CPU before the first frame, without adapter discovery; scaled dimensions are validated before allocation, and UI zoom preserves the requested physical viewport; JSON-lines control scripts are supported |
|
||||
| LR-BEHAV-ACCESS | Accessibility | P2 | 🟡 | `crates/ui-egui/src/widgets.rs`, `crates/ui-egui/src/panels/grid.rs` | AccessKit (VoiceOver / Narrator / AT-SPI): sliders announce control and value, buttons / icon buttons / dropdowns / section headers / sources their labels and state, grid thumbnails file name, rating, flag and label; the canvas tools (crop, masks) are pointer-only; not audited with a screen reader |
|
||||
| LR-BEHAV-LOCALE | Language options | P2 | ✅ | `cmd:app.language.english`, `cmd:app.language.simplifiedChinese`, `cmd:app.language.traditionalChinese`, `cmd:app.language.japanese`, `cmd:app.language.portuguese`, `cmd:app.language.french`, `cmd:app.language.spanish`, `cmd:app.language.german`, `cmd:app.language.russian` | Edit > Language lists every language in the table and the active one is checked; shared with Settings > General (docs/localization.md) |
|
||||
| LR-BEHAV-LOCALIZE | Localisation | P2 | 🟡 | `crates/ui-egui/src/i18n.rs`, `crates/ui-egui/locales/`, `docs/localization.md`, `docs/localization-zh-hans.md`, `docs/localization-zh-hant.md`, `docs/localization-ja.md`, `docs/localization-pt-br.md`, `docs/localization-es.md`, `docs/localization-de.md`, `docs/localization-ru.md` | One table entry per language (code, endonym, ISO 15924 script, catalog): English, Simplified Chinese, Traditional Chinese (Taiwan), Japanese, Brazilian Portuguese, Spanish, German and Russian UI with a persisted language preference and craft-fonts regular/bold faces per script; menus, edit, crop/masks, settings, import/export and primary progress messages. Adding a language is a table entry plus two catalogs. Stock presets/profiles, history steps, library headings, date headings and capture times are localised (user-named items stay verbatim); the German catalog covers every existing catalog key, command/control label and rule-editor label; the other catalogs fall back to English for messages they lack. Technical errors remain English; release notes are translated in German; Traditional Chinese borrows the Simplified Chinese face (no TC face in craft-fonts yet), and the web build embeds the Japanese face in WASM while loading the Simplified Chinese face from a separate font file. |
|
||||
| LR-BEHAV-LOCALE | Language options | P2 | ✅ | `cmd:app.language.english`, `cmd:app.language.simplifiedChinese`, `cmd:app.language.traditionalChinese`, `cmd:app.language.japanese`, `cmd:app.language.portuguese`, `cmd:app.language.french`, `cmd:app.language.spanish`, `cmd:app.language.german`, `cmd:app.language.russian`, `cmd:app.language.ukrainian` | Edit > Language lists every language in the table and the active one is checked; shared with Settings > General (docs/localization.md) |
|
||||
| LR-BEHAV-LOCALIZE | Localisation | P2 | 🟡 | `crates/ui-egui/src/i18n.rs`, `crates/ui-egui/locales/`, `docs/localization.md`, `docs/localization-zh-hans.md`, `docs/localization-zh-hant.md`, `docs/localization-ja.md`, `docs/localization-pt-br.md`, `docs/localization-es.md`, `docs/localization-de.md`, `docs/localization-ru.md` | One table entry per language (code, endonym, ISO 15924 script, catalog): English, Simplified Chinese, Traditional Chinese (Taiwan), Japanese, Brazilian Portuguese, Spanish, German, Russian and Ukrainian UI with a persisted language preference and craft-fonts regular/bold faces per script; menus, edit, crop/masks, settings, import/export and primary progress messages. Adding a language registers the locale and its language command, plus two catalogs. Stock presets/profiles, history steps, library headings, date headings and capture times are localised (user-named items stay verbatim); the Ukrainian catalog covers every existing catalog key, command/control label and rule-editor label; the German catalog reports newly added gaps; the other catalogs fall back to English for messages they lack. Technical errors remain English; release notes are translated in German and Ukrainian; Traditional Chinese borrows the Simplified Chinese face (no TC face in craft-fonts yet), and the web build embeds the Japanese face in WASM while loading the Simplified Chinese face from a separate font file. |
|
||||
| LR-BEHAV-LEARN | Tutorials | OOS | 🚫 | | |
|
||||
| LR-BEHAV-WHATSNEW | What's new | P2 | ✅ | `cmd:app.whatsNew`, `docs/whats-new.md` | Help ▸ What's New: release highlights |
|
||||
| LR-BEHAV-AI-EA | Early-access badges | P2 | ⬜ | | |
|
||||
|
||||
Reference in New Issue
Block a user