This commit is contained in:
Brandon Thomas 2026-10-09 20:56:55 -04:00
commit 0fc81747cb
7 changed files with 1961 additions and 3 deletions

View File

@ -355,7 +355,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

@ -0,0 +1,210 @@
{
" ({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é",
" · {failed} couldn't be rendered": " · {failed} impossible(s) à afficher",
" · {skipped} skipped (file exists)": " · {skipped} ignoré(s) (le fichier existe)",
" · {} duplicate{} skipped": " · {} doublon{} ignoré(s)",
" · {} failed: {}": " · {} échec(s) : {}",
" · {} moved": " · {} déplacé(s)",
" · {} not readable": " · {} illisible(s)",
" · {} original{} left at the source": " · {} original/originaux{} conservé(s) à la source",
" · {} 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{} 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{}",
"Add {} Photo{}, restore {}": "Ajouter {} photo{}, restaurer {}",
"Add “{}” to Favorites": "Ajouter « {} » aux favoris",
"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é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{} 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})",
"Build Smart Previews ({scope})": "Créer les aperçus dynamiques ({scope})",
"Build Standard-Sized Previews ({scope})": "Créer les aperçus de taille standard ({scope})",
"Building previews for {} photos…": "Création des aperçus pour {} photos…",
"Click {}:1": "Cliquer sur {}:1",
"Couldn't open the editor: {e}": "Impossible d’ouvrir l’éditeur : {e}",
"Create {n} Virtual Copies": "Créer {n} copies virtuelles",
"Creates {} stacks from {} of {scope}": "Crée {} piles à partir de {} parmi {scope}",
"Decoder: {reason}": "Décodeur : {reason}",
"Delete \"{label}\"": "Supprimer « {label} »",
"Delete \"{label}\" (and the mask)": "Supprimer « {label} » (et le masque)",
"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{plural}",
"Duplicate \"{label}\"": "Dupliquer « {label} »",
"Example: {example}": "Exemple : {example}",
"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{} 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{} 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{} 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}",
"LightCraft could not open its window: {e}\n\nThe log file has the details: {path}": "LightCraft n’a pas pu ouvrir sa fenêtre : {e}\n\nConsultez le fichier journal pour plus de détails : {path}",
"Lightroom catalog inspected: {photos} photos, {collections} collections, {missing} missing": "Catalogue Lightroom analysé : {photos} photos, {collections} collections, {missing} manquante(s)",
"Lightroom import complete: {imported} photos added": "Importation Lightroom terminée : {imported} photos ajoutées",
"Lightroom import complete: {imported} photos added, {failed} failed": "Importation Lightroom terminée : {imported} photos ajoutées, {failed} échec(s)",
"Lightroom import complete: {imported} photos added; index warning: {warning}": "Importation Lightroom terminée : {imported} photos ajoutées ; avertissement d’index : {warning}",
"Lightroom import failed: {e}{warning}": "Échec de l’importation Lightroom : {e}{warning}",
"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}%",
"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{} — 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{plural}",
"Profile: {}": "Profil : {}",
"Proof Copy ({})": "Copie d’épreuve ({})",
"Proof Preview · {}": "Aperçu d’épreuve · {}",
"Quick Develop · {n} photos": "Développement rapide · {n} photos",
"Quick Develop: {name}": "Développement rapide : {name}",
"Rated {}": "Noté {}",
"Reading Lightroom catalog… {done} of {total}": "Lecture du catalogue Lightroom… {done} sur {total}",
"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{} 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{} (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{}",
"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{} masqué(s)",
"Showing {n} of {}": "Affichage de {n} sur {}",
"Smart album: {}": "Album dynamique : {}",
"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)",
"Updated “{name}”": "« {name} » mis à jour",
"Version {} — a clean-room, pure-Rust photo library and raw developer.": "Version {} — bibliothèque photo et développeur RAW en Rust pur, développés indépendamment.",
"Version: {}": "Version : {}",
"Virtual copy “{name}” of {of}": "Copie virtuelle « {name} » de {of}",
"Working on {what} for {} photos…": "Traitement de {what} pour {} photos…",
"none (CPU rendering): {why}": "aucun (rendu sur le processeur) : {why}",
"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{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",
"{label} {index} of {n}": "{label} {index} sur {n}",
"{label}, {n} photos": "{label}, {n} photos",
"{matching} of {t} photos": "{matching} sur {t} photos",
"{matching} photos": "{matching} photos",
"{mode} spot {} of {}": "Tache {mode} {} sur {}",
"{month}": "{month:02}",
"{month} {year}": "{month:02}/{year}",
"{msg} ({failed} failed)": "{msg} ({failed} échec(s))",
"{name} Label": "Étiquette {name}",
"{name} Off": "{name} : désactivé",
"{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{} 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{}",
"{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{}",
"{weekday}, {day}": "{weekday} {day}",
"{weekday}, {day} {month} {year}": "{weekday} {day} {month} {year}",
"{what} merge added": "Fusion de {what} ajoutée",
"{what} merge failed: {e}": "Échec de la fusion de {what} : {e}",
"{year}": "{year}",
"{{date:…}} directives: {}": "Directives {{date:…}} : {}",
"{} and higher": "{} et plus",
"{} contributors · {} commits": "{} contributeurs · {} commits",
"{} failed": "{} échec(s)",
"{} files": "{} fichiers",
"{} 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{} 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{}",
"… 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{} 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{}",
", {} 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{}",
"{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… {}%",
"{name} · {terms}": "{name} · {terms}",
"{n} {noun}": "{n} {noun}",
"Trained on: {provenance}": "Entraîné sur : {provenance}",
" · {queued} waiting": " · {queued} en attente",
"Shows the model's terms, then downloads {} from {host}, installs it and starts using it. Nothing else is sent.": "Affiche les conditions du modèle, puis télécharge {} depuis {host}, l’installe et commence à l’utiliser. Aucune autre donnée n’est envoyée.",
"In line for AI Denoise ({ahead} ahead)…": "En attente de la réduction du bruit par IA ({ahead} devant)…",
"Running on {}.": "En cours d’exécution sur {}.",
"{file} · {size}": "{file} · {size}",
"• {assumption}": "• {assumption}"
}

File diff suppressed because it is too large Load Diff

View File

@ -124,6 +124,7 @@ language_table! {
Es, "es", "Español", "Latn", include_str!("../locales/es.json");
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");
}
// The settings file stores the BCP-47 code (`"zh-hans"`), never the Rust variant name, so a
@ -508,6 +509,9 @@ mod tests {
assert_eq!(Locale::parse_tag("pt-BR"), Some(Locale::PtBr));
assert_eq!(Locale::parse_tag("pt_BR.UTF-8"), Some(Locale::PtBr));
assert_eq!(Locale::parse_tag("pt"), Some(Locale::PtBr));
assert_eq!(Locale::parse_tag("fr"), Some(Locale::Fr));
assert_eq!(Locale::parse_tag("fr-FR"), Some(Locale::Fr));
assert_eq!(Locale::parse_tag("fr_FR.UTF-8"), Some(Locale::Fr));
for tag in ["de", "de-DE", "de_AT.UTF-8", "de-CH"] {
assert_eq!(Locale::parse_tag(tag), Some(Locale::De), "{tag}");
}
@ -552,6 +556,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);
@ -654,6 +661,7 @@ mod tests {
("app.language.simplifiedChinese", Locale::ZhHans),
("app.language.japanese", Locale::Ja),
("app.language.portuguese", Locale::PtBr),
("app.language.french", Locale::Fr),
("app.language.spanish", Locale::Es),
("app.language.german", Locale::De),
("app.language.russian", Locale::Ru),

View File

@ -19,6 +19,7 @@ pub const LANGUAGE_COMMANDS: &[UiCommand] = &[
("app.language.traditionalChinese", crate::i18n::Locale::ZhHant.name(), None, "Edit>Language"),
("app.language.japanese", crate::i18n::Locale::Ja.name(), None, "Edit>Language"),
("app.language.portuguese", crate::i18n::Locale::PtBr.name(), None, "Edit>Language"),
("app.language.french", crate::i18n::Locale::Fr.name(), None, "Edit>Language"),
("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"),
@ -39,6 +40,7 @@ pub fn language_from_command(id: &str) -> Option<crate::i18n::Locale> {
"app.language.traditionalChinese" => Some(crate::i18n::Locale::ZhHant),
"app.language.japanese" => Some(crate::i18n::Locale::Ja),
"app.language.portuguese" => Some(crate::i18n::Locale::PtBr),
"app.language.french" => Some(crate::i18n::Locale::Fr),
"app.language.spanish" => Some(crate::i18n::Locale::Es),
"app.language.german" => Some(crate::i18n::Locale::De),
"app.language.russian" => Some(crate::i18n::Locale::Ru),

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

@ -528,7 +528,7 @@ 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.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 |