French catalog: plurals, paintable separators, percent format, two new strings

- "photo(s){}" printed "photos(s)s"-style text: the plural suffix now follows the noun ("photo{}"),
  as the Spanish catalog does; smart previews repeat it ({0}).
- Menu triangles (▸) in translations replaced by → (the shipped fonts have no ▸ glyph; test
  translated_catalogs_do_not_use_missing_menu_triangles).
- "{:.0} %" -> "{:.0}%" so the Merging progress test matches every language.
- Translate the two strings main added since the branch (Camera local tone mapping).
- README, docs/localization.md and the LR-BEHAV-LOCALE parity row list French.
- Test: French plural formatting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-10-09 12:57:36 -04:00
parent 8f239d62ec
commit 3978a0ac9a
6 changed files with 59 additions and 54 deletions

View File

@ -354,7 +354,7 @@ CRAFT_FONTS_DIR=../craft-fonts cargo run --release -p lightcraft
Without it LightCraft builds and runs the same, but Chinese and Japanese text have no glyphs. Fonts are never committed to this
repo; see [craftrules `standards/fonts.md`](https://github.com/storytold/craftrules/blob/main/standards/fonts.md).
**Interface language:** **Edit → Language** (English, 简体中文, 繁體中文(台灣), 日本語, Português (Brasil), Deutsch) or **Settings → General →
**Interface language:** **Edit → Language** (English, 简体中文, 繁體中文(台灣), 日本語, Português (Brasil), Español, Deutsch, Русский, Français) or **Settings → General →
Language**; the choice applies immediately and persists. See [docs/localization.md](docs/localization.md).
**Logs:** the desktop app writes its log to standard error and to `logs/lightcraft.log` in its settings folder

View File

@ -1,10 +1,10 @@
{
" ({unsure} with several look-alike files: use Locate)": " ({unsure} avec plusieurs fichiers similaires : utilisez Localiser)",
" ({} in Recently Deleted)": " ({} dans « Supprimés récemment »)",
" · {:.0}% clipped": " · {:.0} % écrêté",
" · {:.0}% clipped": " · {:.0}% écrêté",
" · {failed} couldn't be rendered": " · {failed} impossible(s) à afficher",
" · {skipped} skipped (file exists)": " · {skipped} ignoré(s) (le fichier existe)",
" · {} duplicate{} skipped": " · {} doublon(s){} ignoré(s)",
" · {} duplicate{} skipped": " · {} doublon{} ignoré(s)",
" · {} failed: {}": " · {} échec(s) : {}",
" · {} moved": " · {} déplacé(s)",
" · {} not readable": " · {} illisible(s)",
@ -12,23 +12,23 @@
" · {} restored from Recently Deleted": " · {} restauré(s) depuis « Supprimés récemment »",
" — not carried over: {}{}": " — non transféré : {}{}",
"+{} more": "+{} de plus",
", {failed} file{} not readable": ", {failed} fichier(s){} illisible(s)",
", {failed} file{} not readable": ", {failed} fichier{} illisible(s)",
"; {kept} already had a location": " ; {kept} possédaient déjà un emplacement",
"; {outside} outside its time range": " ; {outside} hors de leur plage horaire",
"@{}: {} PRs, {} commits, +{} / −{} lines (Δ {}), +{} / −{} binary assets, {} – {}": "@{} : {} PR, {} commits, +{} / −{} lignes (Δ {}), +{} / −{} ressources binaires, {} – {}",
"Add {local_n} to My Photos": "Ajouter {local_n} à Mes photos",
"Add {} Photo{}": "Ajouter {} photo(s){}",
"Add {} Photo{}, restore {}": "Ajouter {} photo(s){}, restaurer {}",
"Add {} Photo{}": "Ajouter {} photo{}",
"Add {} Photo{}, restore {}": "Ajouter {} photo{}, restaurer {}",
"Add “{}” to Favorites": "Ajouter « {} » aux favoris",
"Added {n} photo{} to My Photos": "{n} photo(s){} ajoutée(s) à Mes photos",
"Added {n} photo{} to “{}”": "{n} photo(s){} ajoutée(s) à « {} »",
"Added {n} photo{} to My Photos": "{n} photo{} ajoutée(s) à Mes photos",
"Added {n} photo{} to “{}”": "{n} photo{} ajoutée(s) à « {} »",
"Added {what} to {name}": "{what} ajouté à {name}",
"Adding photos… {} of {}": "Ajout des photos… {} sur {}",
"All {n} selected photos move by the same amount.": "Les {n} photos sélectionnées se déplacent de la même valeur.",
"Already in the library, in Recently Deleted: {} photo{} selected · right-click ▸ Restore, or Delete Permanently to import afresh": "Déjà dans la bibliothèque, dans « Supprimés récemment » : {} photo(s){} sélectionnée(s) · clic droit ▸ Restaurer, ou Supprimer définitivement pour importer à nouveau",
"Already in the library: {} photo{} selected in All Photos": "Déjà dans la bibliothèque : {} photo(s){} sélectionnée(s) dans Toutes les photos",
"Already in the library, in Recently Deleted: {} photo{} selected · right-click ▸ Restore, or Delete Permanently to import afresh": "Déjà dans la bibliothèque, dans « Supprimés récemment » : {} photo{} sélectionnée(s) · clic droit → Restaurer, ou Supprimer définitivement pour importer à nouveau",
"Already in the library: {} photo{} selected in All Photos": "Déjà dans la bibliothèque : {} photo{} sélectionnée(s) dans Toutes les photos",
"Auto ({})": "Automatique ({})",
"Auto Import: added {} photo{}": "Importation automatique : {} photo(s){} ajoutée(s)",
"Auto Import: added {} photo{}": "Importation automatique : {} photo{} ajoutée(s)",
"Auto Sync: edits apply to {n} photos": "Synchronisation automatique : les modifications s’appliquent à {n} photos",
"Automatic ({} MB)": "Automatique ({} Mo)",
"Build 1:1 Previews ({scope})": "Créer les aperçus 1:1 ({scope})",
@ -45,26 +45,26 @@
"Delete {n} Photos": "Supprimer {n} photos",
"Delete “{current}”": "Supprimer « {current} »",
"Discard Smart Previews ({scope})": "Supprimer les aperçus dynamiques ({scope})",
"Done ({what}): {done} photo{plural}": "Terminé ({what}) : {done} photo(s){plural}",
"Done ({what}): {done} photo{plural}": "Terminé ({what}) : {done} photo{plural}",
"Duplicate \"{label}\"": "Dupliquer « {label} »",
"Example: {example}": "Exemple : {example}",
"Exported {n} photo{}": "{n} photo(s){} exportée(s)",
"Exported {ok} of {total} photo{}": "{ok} photo(s){} exportée(s) sur {total}",
"Exported {} point curve preset{}": "{} préréglage(s){} de courbe de points exporté(s)",
"Exported {} preset{}": "{} préréglage(s){} exporté(s)",
"Exported {n} photo{}": "{n} photo{} exportée(s)",
"Exported {ok} of {total} photo{}": "{ok} photo{} exportée(s) sur {total}",
"Exported {} point curve preset{}": "{} préréglage{} de courbe de points exporté(s)",
"Exported {} preset{}": "{} préréglage{} exporté(s)",
"Exporting {} of {total}": "Exportation : {} sur {total}",
"Filter bar — {active} active filter{}": "Barre de filtres — {active} filtre(s){} actif(s)",
"Filter bar — {active} active filter{}": "Barre de filtres — {active} filtre{} actif(s)",
"Focus {:.0}": "Mise au point {:.0}",
"Folder “{}” (and its subfolders)": "Dossier « {} » (et ses sous-dossiers)",
"Found {n} missing photo{}; {left} still missing{unsure}": "{n} photo(s){} manquante(s) retrouvée(s) ; {left} toujours manquante(s){unsure}",
"Healed {n} dust spot{}": "{n} tache(s){} de poussière corrigée(s)",
"Import cancelled · {} photo{} added": "Importation annulée · {} photo(s){} ajoutée(s)",
"Found {n} missing photo{}; {left} still missing{unsure}": "{n} photo{} manquante(s) retrouvée(s) ; {left} toujours manquante(s){unsure}",
"Healed {n} dust spot{}": "{n} tache{} de poussière corrigée(s)",
"Import cancelled · {} photo{} added": "Importation annulée · {} photo{} ajoutée(s)",
"Import failed: {e}": "Échec de l’importation : {e}",
"Imported {n} point curve preset{}": "{n} préréglage(s){} de courbe de points importé(s)",
"Imported {n} preset{}": "{n} préréglage(s){} importé(s)",
"Imported {n} preset{}, {f} file{} not readable": "{n} préréglage(s){} importé(s), {f} fichier(s){} illisible(s)",
"Imported {profiles} profile{} (Profile browser ▸ their groups)": "{profiles} profil(s){} importé(s) (Navigateur de profils ▸ leurs groupes)",
"Imported {} photo{}": "{} photo(s){} importée(s)",
"Imported {n} point curve preset{}": "{n} préréglage{} de courbe de points importé(s)",
"Imported {n} preset{}": "{n} préréglage{} importé(s)",
"Imported {n} preset{}, {f} file{} not readable": "{n} préréglage{} importé(s), {f} fichier{} illisible(s)",
"Imported {profiles} profile{} (Profile browser ▸ their groups)": "{profiles} profil{} importé(s) (Navigateur de profils → leurs groupes)",
"Imported {} photo{}": "{} photo{} importée(s)",
"Insert at the cursor (also written {})": "Insérer à la position du curseur (également écrit {})",
"Label Set: {name}": "Ensemble d’étiquettes : {name}",
"LightCraft could not open its window: {e}": "LightCraft n’a pas pu ouvrir sa fenêtre : {e}",
@ -77,15 +77,15 @@
"Lightroom inspection failed: {e}{warning}": "Échec de l’analyse Lightroom : {e}{warning}",
"MIT OR Apache-2.0. Fonts: {} (OFL). Icons: original.": "MIT ou Apache-2.0. Polices : {} (OFL). Icônes : originales.",
"Matches: {}": "Correspondances : {}",
"Merging… {stage} {:.0}%": "Fusion… {stage} {:.0} %",
"Merging… {stage} {:.0}%": "Fusion… {stage} {:.0}%",
"Move {what} to Recently Deleted?": "Déplacer {what} vers « Supprimés récemment » ?",
"Named ({named_n})": "Nommés ({named_n})",
"Painting “{k}”: click photos to add or remove it · Esc stops": "Application de « {k} » : cliquez sur les photos pour l’ajouter ou le retirer · Échap pour arrêter",
"Paste into {n} photo{} — only settings that were copied are pasted": "Coller dans {n} photo(s){} — seuls les réglages copiés seront collés",
"Paste into {n} photo{} — only settings that were copied are pasted": "Coller dans {n} photo{} — seuls les réglages copiés seront collés",
"Preset: {name}": "Préréglage : {name}",
"Preview only — editing the camera's embedded JPEG ({})": "Aperçu uniquement — modification du JPEG intégré de l’appareil photo ({})",
"Preview only — {}": "Aperçu uniquement — {}",
"Previews ready for {done} photo{plural}": "Aperçus prêts pour {done} photo(s){plural}",
"Previews ready for {done} photo{plural}": "Aperçus prêts pour {done} photo{plural}",
"Profile: {}": "Profil : {}",
"Proof Copy ({})": "Copie d’épreuve ({})",
"Proof Preview · {}": "Aperçu d’épreuve · {}",
@ -96,25 +96,25 @@
"Reading photos… {done} of {total}": "Lecture des photos… {done} sur {total}",
"Reading photos… {} of {}": "Lecture des photos… {} sur {}",
"Redo {}": "Rétablir {}",
"Remove “{name}” and its {count} photo{} from the library?": "Retirer « {name} » et ses {count} photo(s){} de la bibliothèque ?",
"Remove “{name}” and its {count} photo{} from the library?": "Retirer « {name} » et ses {count} photo{} de la bibliothèque ?",
"Remove “{}” from Favorites": "Retirer « {} » des favoris",
"Removed from {}": "Retiré de {}",
"Removed {what} from {name}": "{what} retiré de {name}",
"Rename {n} Photos…": "Renommer {n} photos…",
"Rename “{name}”": "Renommer « {name} »",
"Renames “{from}” on {n} photo{} (keywords below it too). Use | for levels, e.g. Travel|Italy. An existing name merges the two.": "Renomme « {from} » sur {n} photo(s){} (ainsi que les mots-clés qui en dépendent). Utilisez | pour séparer les niveaux, par ex. Voyages|Italie. Si le nom existe déjà, les deux sont fusionnés.",
"Renames “{from}” on {n} photo{} (keywords below it too). Use | for levels, e.g. Travel|Italy. An existing name merges the two.": "Renomme « {from} » sur {n} photo{} (ainsi que les mots-clés qui en dépendent). Utilisez | pour séparer les niveaux, par ex. Voyages|Italie. Si le nom existe déjà, les deux sont fusionnés.",
"Rendering on the CPU: {why}": "Rendu sur le processeur : {why}",
"Replace {} with:": "Remplacer {} par :",
"Reset {name}": "Réinitialiser {name}",
"Reset {} Channel": "Réinitialiser le canal {}",
"Restore {}": "Restaurer {}",
"Restore {} Photo{}": "Restaurer {} photo(s){}",
"Restore {} Photo{}": "Restaurer {} photo{}",
"Saved export preset “{}”": "Préréglage d’exportation « {} » enregistré",
"Scores {scope} for focus and exposure and finds similar shots taken within seconds of each other (bursts).": "Évalue la mise au point et l’exposition de {scope}, puis repère les prises similaires réalisées à quelques secondes d’intervalle (rafales).",
"Show {hidden} hidden location{}": "Afficher {hidden} emplacement(s){} masqué(s)",
"Show {hidden} hidden location{}": "Afficher {hidden} emplacement{} masqué(s)",
"Showing {n} of {}": "Affichage de {n} sur {}",
"Smart album: {}": "Album dynamique : {}",
"Tagged {n} photo{} from the tracklog": "{n} photo(s){} géolocalisée(s) à partir du journal de trajet",
"Tagged {n} photo{} from the tracklog": "{n} photo{} géolocalisée(s) à partir du journal de trajet",
"The SAM 3 download failed: {e}": "Échec du téléchargement de SAM 3 : {e}",
"Undo {}": "Annuler {}",
"Unknown tag{s} {} stay{} as typed (see Tags)": "Les étiquettes inconnues{s} {} restent{} telles quelles (voir Étiquettes)",
@ -127,7 +127,7 @@
"the {n} selected photos": "les {n} photos sélectionnées",
"the {} photos in view": "les {} photos affichées",
"these {count} photos": "ces {count} photos",
"{count} smart preview{} · {:.1} MB": "{count} aperçu(s) dynamique(s){} · {:.1} Mo",
"{count} smart preview{} · {:.1} MB": "{count} aperçu{0} dynamique{0} · {1:.1} Mo",
"{f} folders (and their subfolders)": "{f} dossier(s) (et leurs sous-dossiers)",
"{label} Grading": "Étalonnage de {label}",
"{label} {d:+} on every selected photo": "{label} {d:+} sur chaque photo sélectionnée",
@ -144,19 +144,19 @@
"{name} On": "{name} : activé",
"{name} opened for editing; it is stacked with the original": "{name} ouvert pour modification ; il est empilé avec l’original",
"{name} — double-click to reset it": "{name} — double-cliquez pour réinitialiser",
"{n} change{} couldn't be written to disk: {e}\nIf you quit now, {} lost.": "Impossible d’écrire {n} modification(s){} sur le disque : {e}\nSi vous quittez maintenant, {} seront perdues.",
"{n} change{} saved in memory but not written to disk: {e}\nLightCraft retries automatically; quitting now would lose {}.": "{n} modification(s){} enregistrée(s) en mémoire mais non écrite(s) sur le disque : {e}\nLightCraft réessaiera automatiquement ; quitter maintenant entraînerait la perte de {}.",
"{n} change{} saved in memory but not written to disk: {e} — LightCraft will retry": "{n} modification(s){} enregistrée(s) en mémoire mais non écrite(s) sur le disque : {e} — LightCraft réessaiera.",
"{n} change{} couldn't be written to disk: {e}\nIf you quit now, {} lost.": "Impossible d’écrire {n} modification{} sur le disque : {e}\nSi vous quittez maintenant, {} seront perdues.",
"{n} change{} saved in memory but not written to disk: {e}\nLightCraft retries automatically; quitting now would lose {}.": "{n} modification{} enregistrée(s) en mémoire mais non écrite(s) sur le disque : {e}\nLightCraft réessaiera automatiquement ; quitter maintenant entraînerait la perte de {}.",
"{n} change{} saved in memory but not written to disk: {e} — LightCraft will retry": "{n} modification{} enregistrée(s) en mémoire mais non écrite(s) sur le disque : {e} — LightCraft réessaiera.",
"{n} found · {sel} selected": "{n} trouvé(s) · {sel} sélectionné(s)",
"{n} photos": "{n} photos",
"{n} photos in {} are hidden by the filters above": "Les filtres ci-dessus masquent {n} photos dans {}",
"{n} photo{}": "{n} photo(s){}",
"{n} photo{} match · updates automatically as photos change": "{n} photo(s){} correspondante(s) · mise à jour automatique lorsque les photos changent",
"{n} photo{} now · updates automatically as photos change": "{n} photo(s){} actuellement · mise à jour automatique lorsque les photos changent",
"{n} photo{}": "{n} photo{}",
"{n} photo{} match · updates automatically as photos change": "{n} photo{} correspondante(s) · mise à jour automatique lorsque les photos changent",
"{n} photo{} now · updates automatically as photos change": "{n} photo{} actuellement · mise à jour automatique lorsque les photos changent",
"{pid} (missing)": "{pid} (manquant)",
"{sel_n} selected · {counted}": "{sel_n} sélectionné(s) · {counted}",
"{stage}… {:.0}%": "{stage}… {:.0} %",
"{verb} {n} Photo{}": "{verb} {n} photo(s){}",
"{stage}… {:.0}%": "{stage}… {:.0}%",
"{verb} {n} Photo{}": "{verb} {n} photo{}",
"{weekday}, {day}": "{weekday} {day}",
"{weekday}, {day} {month} {year}": "{weekday} {day} {month} {year}",
"{what} merge added": "Fusion de {what} ajoutée",
@ -167,37 +167,37 @@
"{} contributors · {} commits": "{} contributeurs · {} commits",
"{} failed": "{} échec(s)",
"{} files": "{} fichiers",
"{} files and {f} folder{}": "{} fichiers et {f} dossier(s){}",
"{} item{}": "{} élément(s){}",
"{} files and {f} folder{}": "{} fichiers et {f} dossier{}",
"{} item{}": "{} élément{}",
"{} of 4 guides — drag along lines that should be vertical or horizontal.": "{} guides sur 4 — faites-les glisser le long des lignes qui doivent être verticales ou horizontales.",
"{} photos": "{} photos",
"{} photos · exposures {} EV": "{} photos · expositions {} EV",
"{} photos · {} · {:.0}° × {:.0}°": "{} photos · {} · {:.0}° × {:.0}°",
"{} photo{} not readable": "{} photo(s){} illisible(s)",
"{} sample{}": "{} échantillon(s){}",
"{} photo{} not readable": "{} photo{} illisible(s)",
"{} sample{}": "{} échantillon{}",
"{} sec": "{} s",
"{} spot(s) on this photo": "{} tache(s) sur cette photo",
"{} — show their photos": "{} — afficher leurs photos",
"· {dups} already in the library": "· déjà dans la bibliothèque : {dups}",
"· {n} photo{}": "· {n} photo(s){}",
"· {n} photo{}": "· {n} photo{}",
"… and {more} more": "… et {more} de plus",
"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 ne peut pas encore décoder cette variante RAW ({}). Vous modifiez l’aperçu JPEG intégré de l’appareil photo, qui inclut déjà le style d’image (par ex. monochrome) et la balance des blancs de l’appareil.",
"Filtered: {label}": "Filtré : {label}",
"{n} similar photo{} · View ▸ Clear Filters to see all": "{n} photo(s){} similaire(s) · Affichage ▸ Effacer les filtres pour tout voir",
"{n} similar photo{} · View ▸ Clear Filters to see all": "{n} photo{} similaire(s) · Affichage → Effacer les filtres pour tout voir",
"Moved; {} photo(s) relinked": "Déplacé ; {} photo(s) de nouveau associée(s)",
"{} label (click more labels to show any of them)": "Étiquette {} (cliquez sur « autres étiquettes » pour afficher celles de votre choix)",
", {} star{}": ", {} étoile(s){}",
", {} star{}": ", {} étoile{}",
", {} label": ", étiquette {}",
"Downloads from {host}. Once it has arrived and checked out, LightCraft installs it, {after}.": "Téléchargement depuis {host}. Une fois le téléchargement terminé et vérifié, LightCraft l’installe, {after}.",
"{:.1} MB": "{:.1} Mo",
"Running on the processor: {}.": "Exécution sur le processeur : {}.",
"{} · {}. Kept in the library's “denoise” folder; the oldest ones that no photo uses go first when the limit is reached.": "{} · {}. Conservé dans le dossier « denoise » de la bibliothèque ; lorsque la limite est atteinte, les fichiers les plus anciens inutilisés par les photos sont supprimés en premier.",
"Denoising {name}: {done} of {total} tiles{}": "Réduction du bruit de {name} : {done} sur {total} tuile(s){}",
"Denoising {name}: {done} of {total} tiles{}": "Réduction du bruit de {name} : {done} sur {total} tuile{}",
"{size} GB": "{size} Go",
"{} of {} from {host}": "{} sur {} depuis {host}",
"{} KB": "{} Ko",
"{} MB": "{} Mo",
"Making the denoised picture… {}%": "Création de l’image débruitée… {} %",
"Making the denoised picture… {}%": "Création de l’image débruitée… {}%",
"{name} · {terms}": "{name} · {terms}",
"{n} {noun}": "{n} {noun}",
"Trained on: {provenance}": "Entraîné sur : {provenance}",

View File

@ -1732,5 +1732,7 @@
"Pending photo action cancelled": "Action de photo en attente annulée",
"Pending photo action cancelled because its library or photo changed": "Action de photo en attente annulée car sa bibliothèque ou sa photo a changé",
"ONNX model": "Modèle ONNX",
"commercial use subject to GPL conditions": "usage commercial soumis aux conditions de la GPL"
"commercial use subject to GPL conditions": "usage commercial soumis aux conditions de la GPL",
"Camera local tone mapping": "Mappage local des tons de l’appareil",
"Apply the local tone mapping stored in the raw (Apple ProRAW), as the camera renders it": "Appliquer le mappage local des tons enregistré dans le fichier raw (Apple ProRAW), comme le rend l’appareil"
}

View File

@ -492,6 +492,9 @@ mod tests {
set_language(Locale::Ja);
assert_eq!(tr_format!("Imported {} photo{}", 12, "s"), "12枚を読み込みました");
assert_eq!(tr_format!("Exported {ok} of {total} photo{}", "s", ok = 4, total = 12), "12枚中4枚を書き出しました");
set_language(Locale::Fr);
assert_eq!(tr_format!("Imported {} photo{}", 12, "s"), "12 photos importée(s)");
assert_eq!(tr_format!("Imported {} photo{}", 1, ""), "1 photo importée(s)");
// Format specs survive translation: precision, sign, and values a language reorders.
for language in Locale::ALL {
set_language(*language);

View File

@ -2,7 +2,7 @@
The interface ships in the language it is written in (English) plus every language in the table in
`crates/ui-egui/src/i18n.rs`. Today that is English, Simplified Chinese (`zh-hans`), Traditional
Chinese (`zh-hant`, Taiwan), Japanese (`ja`), Brazilian Portuguese (`pt-br`), Spanish (`es`), German (`de`) and Russian (`ru`). This file is the reference for **adding or maintaining a language**; the per-language notes
Chinese (`zh-hant`, Taiwan), Japanese (`ja`), Brazilian Portuguese (`pt-br`), Spanish (`es`), German (`de`), Russian (`ru`) and French (`fr`). This file is the reference for **adding or maintaining a language**; the per-language notes
are in [`localization-zh-hans.md`](localization-zh-hans.md), [`localization-zh-hant.md`](localization-zh-hant.md),
[`localization-ja.md`](localization-ja.md), [`localization-pt-br.md`](localization-pt-br.md), [`localization-es.md`](localization-es.md),
[`localization-de.md`](localization-de.md) and [`localization-ru.md`](localization-ru.md).

View File

@ -527,7 +527,7 @@ under the UI command that wraps it (a no-op error); the UI command now wins.
| LR-BEHAV-TOOLTIPS | Tooltips with shortcuts | P0 | ✅ | `crates/ui-egui/src/panels/bottombar.rs` | |
| 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.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-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-LEARN | Tutorials | OOS | 🚫 | | |
| LR-BEHAV-WHATSNEW | What's new | P2 | ✅ | `cmd:app.whatsNew`, `docs/whats-new.md` | Help ▸ What's New: release highlights |