Merge PR #1198 (rebased)

# Conflicts:
#	crates/ui-egui/src/panels/keyword_list.rs
This commit is contained in:
Brandon Thomas committed 2026-10-15 00:52:53 -04:00
commit 02000d98a4
9 files changed
+2409 -10

No files matched your search

+2 -1
View File
@@ -96,7 +96,7 @@ while lower-layer errors stay English. Detail: [localization-parity.md](docs/loc
| Korean | ko | 0% | none (no Hangul face) |
| Vietnamese | vi | 0% | none |
Also shipped: Traditional Chinese (87%), Russian (74%), Ukrainian (99%).
Also shipped: Traditional Chinese (87%), Russian (74%), Ukrainian (99%), Italian (92%).
## Upcoming
@@ -180,6 +180,7 @@ Ranked toward beta; detail in [docs/roadmap.md](docs/roadmap.md), gaps in [docs/
| Date | Change | Summary |
|---|---|---|
| 2026-10-12 | minor | Italian interface (`it`, #1192): 2,201 of 2,398 messages, locale, menu command and tests; headline numbers unchanged |
| 2026-10-13 | minor | Sony ILCA-99M2 camera colour profile in the log; numbers unchanged |
| 2026-10-14 | minor | Localization ~52% → ~55% (mean of the 12 key languages): de, es, fr, ja and uk catalogs filled |
| 2026-10-14 | minor | 21 Sony and Leica models verified (258 models, 18%); headline numbers unchanged |
+1
View File
@@ -29,6 +29,7 @@ covers a whole directory), or if a licence file referenced in the Licence column
| 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, Noto Sans Arabic Regular, Noto Sans Thai Regular | The Noto Project Authors (Arabic and Thai); 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 Noto Sans Thai 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 |
| `crates/ui-egui/locales/cs.json`, `crates/ui-egui/locales/cs-formats.json` | Czech interface catalogs | @Losminek with AI assistance (Codex) | original work: translations of English source phrases, reviewed against the app UI conventions; no external translation material | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`) | 2026-10-11 | catalog coverage, Czech dates and count-neutral wording; independent scope/terminology review; native-speaker proofreading pending |
| `crates/ui-egui/locales/it.json`, `crates/ui-egui/locales/it-formats.json` | Italian interface catalogs | @GabrielePecchioli with Claude Code; PhotoCraft, VectorCraft, PdfCraft and WordCraft translation contributors for reused labels | original translations from English source; 410 initial English-key matches reviewed from [PhotoCraft da3da09](https://github.com/storytold/photocraft/blob/da3da0942c14c1159f40c652afcf33584332c733/crates/ui-egui/src/i18n/it.tsv), [VectorCraft 5432821](https://github.com/storytold/vectorcraft/blob/543282148007bcb5633c493a5374cd26b0339c8d/crates/ui-egui/src/i18n/it.tsv), [PdfCraft e8e275c](https://github.com/storytold/pdfcraft/blob/e8e275cbd40ccd0dc6b56f82b16a575571043702/crates/ui-egui/src/i18n/it.tsv) and the Italian WordCraft catalog ([storytold/wordcraft#598](https://github.com/storytold/wordcraft/pull/598)); 46 of them retranslated where the sibling's meaning didn't fit a photo app | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`, also the source repositories' licences) | 2026-10-12 | adapted to LightCraft photo-development context, count-neutral labels for Italian plural agreement, reviewed placeholders |
Notes
- Fonts authored or published by Adobe (Source Sans/Serif/Code, Source Han, …) are not used, even though some are
+293
View File
@@ -0,0 +1,293 @@
{
" ({unsure} with several look-alike files: use Locate)": " ({unsure} con diversi file simili: usa Trova)",
" ({} in Recently Deleted)": " ({} in Eliminate di recente)",
" · {:.0}% clipped": " · {:.0}% tagliato",
" · {failed} couldn't be rendered": " · impossibili da elaborare: {failed}",
" · {skipped} skipped (file exists)": " · saltate: {skipped} (il file esiste)",
" · {} duplicate{} skipped": " · duplicati saltati: {}{:.0}",
" · {} failed: {}": " · {} con errori: {}",
" · {} moved": " · spostate: {}",
" · {} not readable": " · illeggibili: {}",
" · {} original{} left at the source": " · originali lasciati nell'origine: {}{:.0}",
" · {} restored from Recently Deleted": " · ripristinate da Eliminate di recente: {}",
" — not carried over: {}{}": " · non trasferito: {}{}",
"+{} more": "+{} altre",
", {failed} file{} not readable": ", file illeggibili: {failed}{:.0}",
"; {kept} already had a location": "; avevano già una posizione: {kept}",
"; {outside} outside its time range": "; fuori dal loro intervallo di tempo: {outside}",
"@{}: {} PRs, {} commits, +{} / −{} lines (Δ {}), +{} / −{} binary assets, {} – {}": "@{}: {} PR, {} commit, +{} / −{} righe (Δ {}), +{} / −{} risorse binarie, {} - {}",
"Add {local_n} to My Photos": "Aggiungi {local_n} a Le mie foto",
"Add {} Photo{}": "Aggiungi {} foto{:.0}",
"Add {} Photo{}, restore {}": "Aggiungi {} foto{:.0}, ripristina {}",
"Add “{}” to Favorites": "Aggiungi “{}” ai preferiti",
"Added {n} photo{} to My Photos": "Foto aggiunte a Le mie foto: {n}{:.0}",
"Added {n} photo{} to “{}”": "Foto aggiunte a “{1}”: {n}{0:.0}",
"Added {what} to {name}": "Aggiunto a {name}: {what}",
"Adding photos… {} of {}": "Aggiunta delle foto… {} di {}",
"All {n} selected photos move by the same amount.": "Tutte le {n} foto selezionate si spostano della stessa quantità.",
"Already in the library, in Recently Deleted: {} photo{} selected · right-click ▸ Restore, or Delete Permanently to import afresh": "Già nella libreria, in Eliminate di recente: foto selezionate {}{:.0} · clic destro → Ripristina, oppure Elimina definitivamente per importarle da capo",
"Already in the library: {} photo{} selected in All Photos": "Già nella libreria (selezionate in Tutte le foto): {}{:.0}",
"Auto ({})": "Automatico ({})",
"Auto Import: added {} photo{}": "Importazione automatica · foto aggiunte: {}{:.0}",
"Auto Sync: edits apply to {n} photos": "Sincronizzazione automatica: le modifiche si applicano a {n} foto",
"Automatic ({} MB)": "Automatico ({} MB)",
"Build 1:1 Previews ({scope})": "Genera anteprime 1:1 ({scope})",
"Build Smart Previews ({scope})": "Genera anteprime avanzate ({scope})",
"Build Standard-Sized Previews ({scope})": "Genera anteprime di dimensione standard ({scope})",
"Building previews for {} photos…": "Generazione delle anteprime per {} foto…",
"Click {}:1": "Clic {}:1",
"Couldn't open the editor: {e}": "Impossibile aprire l'editor: {e}",
"Create {n} Virtual Copies": "Crea {n} copie virtuali",
"Creates {} stacks from {} of {scope}": "Crea pile: {} (da {} di {scope})",
"Decoder: {reason}": "Decodificatore: {reason}",
"Delete \"{label}\"": "Elimina \"{label}\"",
"Delete \"{label}\" (and the mask)": "Elimina \"{label}\" (e la maschera)",
"Delete {n} Photos": "Elimina {n} foto",
"Delete “{current}”": "Elimina “{current}”",
"Discard Smart Previews ({scope})": "Elimina anteprime avanzate ({scope})",
"Done ({what}): {done} photo{plural}": "Fatto ({what}): {done} foto{plural:.0}",
"Duplicate \"{label}\"": "Duplica \"{label}\"",
"Example: {example}": "Esempio: {example}",
"Exported {n} photo{}": "Foto esportate: {n}{:.0}",
"Exported {ok} of {total} photo{}": "Foto esportate: {ok} di {total}{:.0}",
"Exported {} point curve preset{}": "Preset di curva a punti esportati: {}{:.0}",
"Exported {} preset{}": "Preset esportati: {}{:.0}",
"Exporting {} of {total}": "Esportazione {} di {total}",
"Filter bar — {active} active filter{}": "Barra dei filtri · filtri attivi: {active}{:.0}",
"Focus {:.0}": "Messa a fuoco {:.0}",
"Folder “{}” (and its subfolders)": "Cartella “{}” (e sottocartelle)",
"Found {n} missing photo{}; {left} still missing{unsure}": "Foto mancanti ritrovate: {n}{:.0}; ancora mancanti: {left}{unsure}",
"Healed {n} dust spot{}": "Macchie di polvere corrette: {n}{:.0}",
"Import cancelled · {} photo{} added": "Importazione annullata · foto aggiunte: {}{:.0}",
"Import failed: {e}": "Importazione non riuscita: {e}",
"Import warnings ({n}):\n{warnings}": "Avvisi di importazione ({n}):\n{warnings}",
"XMP profile \"{profile}\" was not applied for {path}; colors may differ from Lightroom.": "Il profilo XMP \"{profile}\" non è stato applicato a {path}; i colori possono differire da Lightroom.",
"Lightroom geometry was not preserved for {path}; {mode} will be recalculated.": "La geometria di Lightroom non è stata conservata per {path}; {mode} verrà ricalcolato.",
"Lightroom geometry for {path} was not preserved; saved transform is ignored because Upright is Off.": "La geometria di Lightroom per {path} non è stata conservata; la trasformazione salvata viene ignorata perché il raddrizzamento è disattivato.",
"Lightroom geometry for {path} was not preserved; Guided Upright saved transform is unsupported.": "La geometria di Lightroom per {path} non è stata conservata; la trasformazione salvata del raddrizzamento guidato non è supportata.",
"Lightroom geometry for {path} was not preserved; saved Upright transform is unsupported.": "La geometria di Lightroom per {path} non è stata conservata; la trasformazione salvata del raddrizzamento non è supportata.",
"XMP adjustments were not imported for {path}: {fields}.": "Le regolazioni XMP non sono state importate per {path}: {fields}.",
"Imported {n} point curve preset{}": "Preset di curva a punti importati: {n}{:.0}",
"Imported {n} preset{}": "Preset importati: {n}{:.0}",
"Imported {n} preset{}, {f} file{} not readable": "Preset importati: {n}{:.0}, file illeggibili: {f}{:.0}",
"Imported {profiles} profile{} (Profile browser ▸ their groups)": "Profili importati: {profiles}{:.0} (Browser dei profili → i loro gruppi)",
"Imported {} photo{}": "Foto importate: {}{:.0}",
"Insert at the cursor (also written {})": "Inserisci al cursore (si scrive anche {})",
"Label Set: {name}": "Set di etichette: {name}",
"LightCraft could not open its window: {e}": "LightCraft non è riuscito ad aprire la sua finestra: {e}",
"LightCraft could not open its window: {e}\n\nThe log file has the details: {path}": "LightCraft non è riuscito ad aprire la sua finestra: {e}\n\nI dettagli sono nel file di log: {path}",
"Lightroom catalog inspected: {photos} photos, {collections} collections, {missing} missing": "Catalogo di Lightroom esaminato: foto {photos}, raccolte {collections}, mancanti {missing}",
"Lightroom import complete: {imported} photos added": "Importazione da Lightroom completata: foto aggiunte {imported}",
"Lightroom import complete: {imported} photos added, {failed} failed": "Importazione da Lightroom completata: foto aggiunte {imported}, con errori {failed}",
"Lightroom import complete: {imported} photos added; index warning: {warning}": "Importazione da Lightroom completata: foto aggiunte {imported}; avviso sull'indice: {warning}",
"Lightroom import failed: {e}{warning}": "Importazione da Lightroom non riuscita: {e}{warning}",
"Lightroom inspection failed: {e}{warning}": "Analisi di Lightroom non riuscita: {e}{warning}",
"MIT OR Apache-2.0. Fonts: {} (OFL). Icons: original.": "MIT OR Apache-2.0. Caratteri: {} (OFL). Icone: originali.",
"Matches: {}": "Corrispondenze: {}",
"Merging… {stage} {:.0}%": "Unione in corso… {stage} {:.0}%",
"Move {what} to Recently Deleted?": "Spostare {what} in Eliminate di recente?",
"Named ({named_n})": "Con nome ({named_n})",
"Painting “{k}”: click photos to add or remove it · Esc stops": "Pennello “{k}”: fai clic sulle foto per aggiungerla o toglierla · Esc interrompe",
"Paste into {n} photo{} — only settings that were copied are pasted": "Incolla in {n} foto{:.0}: vengono incollate solo le impostazioni copiate",
"Preset: {name}": "Preset: {name}",
"Preview only — editing the camera's embedded JPEG ({})": "Solo anteprima: si modifica il JPEG incorporato dalla fotocamera ({})",
"Preview only — {}": "Solo anteprima: {}",
"Previews ready for {done} photo{plural}": "Anteprime pronte per {done} foto{plural:.0}",
"Profile: {}": "Profilo: {}",
"Proof Copy ({})": "Copia di prova ({})",
"Proof Preview · {}": "Anteprima di prova · {}",
"Quick Develop · {n} photos": "Sviluppo rapido · {n} foto",
"Quick Develop: {name}": "Sviluppo rapido: {name}",
"Rated {}": "Valutata {}",
"Reading Lightroom catalog… {done} of {total}": "Lettura del catalogo Lightroom… {done} di {total}",
"Reading photos… {done} of {total}": "Lettura delle foto… {done} di {total}",
"Reading photos… {} of {}": "Lettura delle foto… {} di {}",
"Redo {}": "Ripeti {}",
"Remove “{name}” and its {count} photo{} from the library?": "Togliere dalla libreria “{name}” e le sue foto ({count}{:.0})?",
"Remove “{}” from Favorites": "Rimuovi “{}” dai preferiti",
"Removed from {}": "Rimosso da {}",
"Removed {what} from {name}": "Rimosso da {name}: {what}",
"Rename {n} Photos…": "Rinomina {n} foto…",
"Rename “{name}”": "Rinomina “{name}”",
"Renames “{from}” on {n} photo{} (keywords below it too). Use | for levels, e.g. Travel|Italy. An existing name merges the two.": "Rinomina “{from}” in {n} foto{:.0} (anche le parole chiave sotto di essa). Usa | per i livelli, es. Viaggi|Italia. Un nome esistente unisce le due.",
"Rendering on the CPU: {why}": "Rendering sulla CPU: {why}",
"Replace {} with:": "Sostituisci {} con:",
"Reset {name}": "Reimposta {name}",
"Reset {} Channel": "Reimposta canale {}",
"Restore {}": "Ripristina {}",
"Restore {} Photo{}": "Ripristina {} foto{:.0}",
"Restored {} photo{} from Recently Deleted": "Foto ripristinate da Eliminate di recente: {}{:.0}",
"Saved export preset “{}”": "Preset di esportazione “{}” salvato",
"Scores {scope} for focus and exposure and finds similar shots taken within seconds of each other (bursts).": "Valuta messa a fuoco ed esposizione di {scope} e trova scatti simili fatti a pochi secondi di distanza (raffiche).",
"Show {hidden} hidden location{}": "Mostra le posizioni nascoste: {hidden}{:.0}",
"Showing {n} of {}": "Mostrate {n} di {}",
"Smart album: {}": "Album smart: {}",
"Tagged {n} photo{} from the tracklog": "Foto geolocalizzate dal tracciato: {n}{:.0}",
"The SAM 3 download failed: {e}": "Download di SAM 3 non riuscito: {e}",
"Undo {}": "Annulla {}",
"Unknown tag{s} {} stay{} as typed (see Tags)": "Tag sconosciuti{s:.0}: {} (restano come li hai scritti{:.0}; vedi Tag)",
"Updated “{name}”": "“{name}” aggiornato",
"Version {} — a clean-room, pure-Rust photo library and raw developer.": "Versione {}: libreria fotografica e sviluppo raw clean-room, in puro Rust.",
"Version: {}": "Versione: {}",
"Virtual copy “{name}” of {of}": "Copia virtuale “{name}” di {of}",
"Working on {what} for {} photos…": "Elaborazione di {what} per {} foto…",
"none (CPU rendering): {why}": "nessuna (rendering sulla CPU): {why}",
"the {n} selected photos": "le {n} foto selezionate",
"the {} photos in view": "le {} foto nella vista",
"these {count} photos": "queste {count} foto",
"{count} smart preview{} · {:.1} MB": "Anteprime avanzate: {count}{0:.0} · {1:.1} MB",
"{f} folders (and their subfolders)": "{f} cartelle (e le loro sottocartelle)",
"{label} Grading": "Gradazione di {label}",
"{label} {d:+} on every selected photo": "{label} {d:+} su ogni foto selezionata",
"{label} {index} of {n}": "{label} {index} di {n}",
"{label}, {n} photos": "{label}, {n} foto",
"{matching} of {t} photos": "{matching} di {t} foto",
"{matching} photos": "{matching} foto",
"{mode} spot {} of {}": "{mode}: macchia {} di {}",
"{month}": "{month:02}",
"{month} {year}": "{month:02}/{year}",
"{msg} ({failed} failed)": "{msg} ({failed} con errori)",
"{name} Label": "Etichetta {name}",
"{name} Off": "{name}: disattivato",
"{name} On": "{name}: attivato",
"{name} opened for editing; it is stacked with the original": "{name} aperta per la modifica; è in pila con l'originale",
"{name} — double-click to reset it": "{name}: doppio clic per reimpostarlo",
"{n} change{} couldn't be written to disk: {e}\nIf you quit now, {} lost.": "Modifiche non scritte sul disco: {n}{0:.0}: {e}\nSe esci ora, andranno perse.{1:.0}",
"{n} change{} saved in memory but not written to disk: {e}\nLightCraft retries automatically; quitting now would lose {}.": "Modifiche salvate in memoria ma non scritte sul disco: {n}{0:.0}: {e}\nLightCraft riprova da solo; se esci ora, andranno perse.{1:.0}",
"{n} change{} saved in memory but not written to disk: {e} — LightCraft will retry": "Modifiche salvate solo in memoria: {n}{:.0}: {e}; LightCraft riproverà",
"{n} found · {sel} selected": "Trovate: {n} · selezionate: {sel}",
"{n} photos": "{n} foto",
"{n} photos in {} are hidden by the filters above": "I filtri qui sopra nascondono {n} foto in {}",
"{n} photo{}": "{n} foto{:.0}",
"{n} photo{} match · updates automatically as photos change": "Foto corrispondenti: {n}{:.0} · si aggiorna da solo quando le foto cambiano",
"{n} photo{} now · updates automatically as photos change": "{n} foto{:.0} ora · si aggiorna da solo quando le foto cambiano",
"{pid} (missing)": "{pid} (mancante)",
"{sel_n} selected · {counted}": "Selezionate: {sel_n} · {counted}",
"{stage}… {:.0}%": "{stage}… {:.0}%",
"{verb} {n} Photo{}": "{verb} {n} foto{:.0}",
"{weekday}, {day}": "{weekday} {day}",
"{weekday}, {day} {month} {year}": "{weekday} {day}/{month:02}/{year}",
"{what} merge added": "Unione {what} aggiunta",
"{what} merge failed: {e}": "Unione {what} non riuscita: {e}",
"{year}": "{year}",
"{{date:…}} directives: {}": "Direttive {{date:…}}: {}",
"{} and higher": "{} e superiori",
"{} contributors · {} commits": "Collaboratori: {} · commit: {}",
"{} failed": "Con errori: {}",
"{} files": "{} file",
"{} files and {f} folder{}": "{} file, cartelle: {f}{:.0}",
"{} item{}": "Elementi: {}{:.0}",
"{} of 4 guides — drag along lines that should be vertical or horizontal.": "{} di 4 guide: trascina lungo le linee che dovrebbero essere verticali o orizzontali.",
"{} photos": "{} foto",
"{} photos · exposures {} EV": "{} foto · esposizioni {} EV",
"{} photos · {} · {:.0}° × {:.0}°": "{} foto · {} · {:.0}° × {:.0}°",
"{} photo{} not readable": "Foto illeggibili: {}{:.0}",
"{} sample{}": "Campioni: {}{:.0}",
"{} sec": "{} s",
"{} spot(s) on this photo": "Macchie in questa foto: {}",
"{} — show their photos": "{}: mostra le loro foto",
"· {dups} already in the library": "· già nella libreria: {dups}",
"· {n} photo{}": "· {n} foto{:.0}",
"· {trashed} in Recently Deleted": "· {trashed} in Eliminate di recente",
"… and {more} more": "… e altre {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 non sa ancora decodificare questa variante raw ({}). Stai modificando l'anteprima JPEG incorporata dalla fotocamera, che contiene già lo stile immagine della fotocamera (ad es. Monocromatico) e il bilanciamento del bianco.",
"Filtered: {label}": "Filtrate: {label}",
"{n} similar photo{} · View ▸ Clear Filters to see all": "Foto simili: {n}{:.0} · Visualizza → Cancella filtri per vederle tutte",
"Moved; {} photo(s) relinked": "Spostata; foto ricollegate: {}",
"{} label (click more labels to show any of them)": "Etichetta {} (fai clic su altre etichette per mostrarne una qualsiasi)",
", {} star{}": ", stelle: {}{:.0}",
", {} label": ", etichetta {}",
"Downloads from {host}. Once it has arrived and checked out, LightCraft installs it, {after}.": "Scaricato da {host}. Quando è arrivato ed è stato verificato, LightCraft lo installa e {after}.",
"{:.1} MB": "{:.1} MB",
"Running on the processor: {}.": "In esecuzione sul processore: {}.",
"{} · {}. Kept in the library's “denoise” folder; the oldest ones that no photo uses go first when the limit is reached.": "{} · {}. Conservate nella cartella “denoise” della libreria; al raggiungimento del limite si eliminano per prime le più vecchie che nessuna foto usa.",
"Denoising {name}: {done} of {total} tiles{}": "Riduzione disturbo di {name}: {done} di {total} riquadri{}",
"{size} GB": "{size} GB",
"{} of {} from {host}": "{} di {} da {host}",
"{} KB": "{} KB",
"{} MB": "{} MB",
"Making the denoised picture… {}%": "Generazione dell'immagine con il disturbo ridotto… {}%",
"{name} · {terms}": "{name} · {terms}",
"{n} {noun}": "{n} {noun}",
"Trained on: {provenance}": "Addestrato su: {provenance}",
" · {queued} waiting": " · in attesa: {queued}",
"Shows the model's terms, then downloads {} from {host}, installs it and starts using it. Nothing else is sent.": "Mostra le condizioni del modello, poi scarica {} da {host}, lo installa e comincia a usarlo. Non viene inviato nient'altro.",
"In line for AI Denoise ({ahead} ahead)…": "In coda per la riduzione disturbo IA ({ahead} prima)…",
"Running on {}.": "In esecuzione su {}.",
"{file} · {size}": "{file} · {size}",
"• {assumption}": "• {assumption}",
"Put inside “{parent}”": "Metti dentro “{parent}”",
"Delete the keyword “{name}”?": "Eliminare la parola chiave “{name}”?",
"It is taken off {count} photo{}, with the keywords below it. Undo brings it back.": "Viene tolta da {count} foto{:.0}, insieme alle parole chiave sotto di essa. Annulla la riporta.",
"Create Keyword Tag Inside “{name}”…": "Crea tag parola chiave dentro “{name}”…",
"“{parent}” has a “{name}” already. Merge the two?": "“{parent}” ha già “{name}”. Unire le due?",
"There is a “{name}” at the top level already. Merge the two?": "Al livello principale c'è già “{name}”. Unire le due?",
"Added “{keyword}” to {n} photo{}": "Aggiunta “{keyword}” a {n} foto{:.0}",
"On {have} of {of} selected photos": "Su {have} di {of} foto selezionate",
"Remove “{name}”": "Rimuovi “{name}”",
"There is a keyword set “{name}” already": "Esiste già un set di parole chiave “{name}”",
"Added {n} keyword{}": "Parole chiave aggiunte: {n}{:.0}",
"; {n} gained synonyms": "; con nuovi sinonimi: {n}",
"Exported {n} keyword{}": "Parole chiave esportate: {n}{:.0}",
" — Capture One won't import a list with ; , < or > in a keyword: {names}": " · Capture One non importa un elenco con ; , < o > in una parola chiave: {names}",
" — left out, as a keyword list can't hold them: {names}": " · escluse, perché un elenco di parole chiave non può contenerle: {names}",
"{done} of {total}": "{done} di {total}",
"{done} of {total} MB": "{done} di {total} MB",
"{pct} %": "{pct}%",
"+{n} more": "+{n} altre",
"1 task is still running: {names}. Quitting stops it.": "1 attività è ancora in corso: {names}. Uscendo si interrompe.",
"{n} tasks are still running: {names}. Quitting stops them.": "{n} attività sono ancora in corso: {names}. Uscendo si interrompono.",
"{what} merge cancelled": "Unione {what} annullata",
"{n} Local photo{} skipped: albums hold library photos (Add to My Photos first)": "Foto locali saltate: {n}{0:.0}; gli album contengono foto della libreria (prima aggiungile a Le mie foto)",
"New Folder in “{name}”": "Nuova cartella in “{name}”",
"Moved {} photo(s)": "Foto spostate: {}",
"not added to Apple Photos: {e}": "non aggiunte a Foto di Apple: {e}",
"{n} added to Apple Photos album “{album}”": "Aggiunte all'album “{album}” di Foto di Apple: {n}",
"{n} added to Apple Photos": "Aggiunte a Foto di Apple: {n}",
"{n} photos opened for editing; each is stacked with its original": "{n} foto aperte per la modifica; ciascuna è in pila con il suo originale",
"{missing} originals are missing. Their catalog records are available; use Find Missing Photos to relink them.": "{missing} originals are missing. Their catalog records are available; use Find Missing Photos to relink them.",
"{name} opened in the other editor": "{name} opened in the other editor",
"{s} s of AV1 video": "{s} s of AV1 video",
"Delete {n} effect preset{}? This can't be undone.": "Delete {n} effect preset{}? This can't be undone.",
"{count} more warnings.": "{count} more warnings.",
"· {in_source} identical copies": "· {in_source} identical copies",
"{what} merge added at {mp}, the largest a panorama can be (its full size is {full})": "{what} merge added at {mp}, the largest a panorama can be (its full size is {full})",
"{n} preset{}": "{n} preset{}",
" — kept but not applied: {}": " — kept but not applied: {}",
"{what} merge added at {mp}: its full size, {full}, needs more memory than this computer has": "{what} merge added at {mp}: its full size, {full}, needs more memory than this computer has",
"Saved template {name}": "Saved template {name}",
"LightCraft stopped unexpectedly: {e}": "LightCraft stopped unexpectedly: {e}",
"Renames {n} face{} across the library. The files are not changed.": "Renames {n} face{} across the library. The files are not changed.",
" · {n} skipped (could not be rendered)": " · {n} skipped (could not be rendered)",
"Delete “{name}”?": "Delete “{name}”?",
"Keywords of {n} selected photos": "Keywords of {n} selected photos",
"new, inside {parent}": "new, inside {parent}",
"Delete these {n} effect presets?": "Delete these {n} effect presets?",
"Imported {e} mask effect{} (Masking ▸ Effect)": "Imported {e} mask effect{} (Masking → Effect)",
"{photos} catalog records processed; {collections} collections.": "{photos} catalog records processed; {collections} collections.",
"Skipped {problem}": "Skipped {problem}",
"Updated {name}": "Updated {name}",
"The XMP sidecars of {n} photos couldn't be written (is their folder offline?). LightCraft keeps trying.": "The XMP sidecars of {n} photos couldn't be written (is their folder offline?). LightCraft keeps trying.",
"Apply to “{mask}” on Selected Photos": "Apply to “{mask}” on Selected Photos",
"Update “{name}” with Current Settings": "Update “{name}” with Current Settings",
"Slideshow exported: {what}": "Slideshow exported: {what}",
"Exported to {path}": "Exported to {path}",
"…and {more} more": "…and {more} more",
"Create the keyword “{name}”": "Create the keyword “{name}”",
"{n} JPEG files": "{n} JPEG files",
"Exported {} effect preset{}": "Exported {} effect preset{}",
"Imported {name}": "Imported {name}",
"{n} pages": "{n} pages",
" · kept, not applied: {}": " · kept, not applied: {}",
"Delete the effect preset “{name}”?": "Delete the effect preset “{name}”?",
"{n} raws can now be developed from their raw data": "{n} raws can now be developed from their raw data",
"Removed “{name}” from {n} photos. Edit ▸ Undo puts it back.": "Removed “{name}” from {n} photos. Edit → Undo puts it back.",
"Template preview: {name}": "Template preview: {name}",
"{n} photos in view · {slides} slides · {time}": "{n} photos in view · {slides} slides · {time}",
"Applied {name}": "Applied {name}",
"“{to}” is already a person. Combining them makes one person; Undo separates them again.": "“{to}” is already a person. Combining them makes one person; Undo separates them again.",
"{n} originals opened in the other editor": "{n} originals opened in the other editor",
"LightCraft stopped unexpectedly: {e}\n\nThe log file has the details: {path}": "LightCraft stopped unexpectedly: {e}\n\nThe log file has the details: {path}",
"Effect: {name}": "Effect: {name}",
" in {g} group{}": " in {g} group{}"
}
File diff suppressed because it is too large. Load diff
+107 -2
View File
@@ -128,6 +128,7 @@ language_table! {
Uk, "uk", "Українська", "Cyrl", include_str!("../locales/uk.json");
Pl, "pl", "Polski", "Latn", include_str!("../locales/pl.json");
Cs, "cs", "Čeština", "Latn", include_str!("../locales/cs.json");
It, "it", "Italiano", "Latn", include_str!("../locales/it.json");
}
// The settings file stores the BCP-47 code (`"zh-hans"`), never the Rust variant name, so a
@@ -753,6 +754,7 @@ mod tests {
("app.language.ukrainian", Locale::Uk),
("app.language.polish", Locale::Pl),
("app.language.czech", Locale::Cs),
("app.language.italian", Locale::It),
];
// One command per language, and every command reachable from the menu table.
assert_eq!(commands.len(), Locale::ALL.len());
@@ -1007,6 +1009,109 @@ mod tests {
set_language(Locale::En);
}
/// Italian's coverage of every catalog message and display label, reported like Ukrainian's.
#[test]
fn italian_coverage_of_catalogs_commands_controls_and_rules_is_reported() {
let catalog = Locale::It.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!("it 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!("it lacks {} display label(s) (shown in English): {missing:?}", missing.len());
}
}
#[test]
fn italian_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.italian", serde_json::json!({})).unwrap();
assert_eq!(app.ui.language, Locale::It);
assert_eq!(crate::menubar::checked(&app, "app.language.italian"), Some(true));
assert_eq!(tr("Light"), "Luce");
assert_eq!(tr("Landscape"), "Paesaggio");
assert_eq!(tr("Home"), "Cartella personale");
assert_eq!(date_group_label("2026-09-20", false), "Domenica 20/09/2026");
assert_eq!(date_group_label("2026-09", false), "09/2026");
assert_eq!(display_time("2026-09-20T16:04:05"), "Domenica 20/09/2026 16:04:05");
// Italian agrees adjectives with the count ("1 foto importata", "2 foto importate"), so the
// count follows a label instead, which reads right for every number; the English plural
// suffix is consumed and never shows.
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!("Foto importate: {n}"));
assert_eq!(tr_format!("{} files and {f} folder{}", n, "s", f = n), format!("{n} file, cartelle: {n}"));
let noun = tr(if n == 1 { "cached picture" } else { "cached pictures" });
let expected = if n == 1 { "1 immagine in cache".to_string() } else { format!("{n} immagini in cache") };
assert_eq!(tr_format!("{n} {noun}", n = n, noun = noun), expected);
}
// 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),
"Riduzione disturbo di IMG.CR3: 1 di 4 riquadri · in attesa: 5"
);
let name = "Color {title} 日本語";
assert_eq!(tr_format!("Added {n} photo{} to “{}”", "s", name, n = 22), format!("Foto aggiunte a “{name}”: 22"));
assert_eq!(builtin_label("Warm Glow", false), "Warm Glow");
assert_eq!(builtin_label("Warm Glow", true), "Bagliore caldo");
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":"it""#));
assert_eq!(serde_json::from_str::<crate::state::UiState>(&saved).unwrap().language, Locale::It);
let ctx = egui::Context::default();
for code in ["it", "it-IT", "it_IT.UTF-8", "it-Latn-IT", "it-CH"] {
assert_eq!(Locale::parse_tag(code), Some(Locale::It));
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::It);
}
set_language(Locale::En);
}
#[test]
fn italian_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::It);
assert!(text.contains("Le mie foto") && text.contains("Tutte le foto"), "{text}");
for (command, title) in
[("app.about", "Informazioni su LightCraft"), ("app.shortcuts", "Scorciatoie da tastiera"), ("app.settings", "Impostazioni")]
{
app.ui.dialog = None;
app.run(command, serde_json::json!({})).unwrap();
let text = painted_text(&ctx, &mut app, Locale::It);
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::It.catalog().values().flat_map(|text| text.chars()).filter(|ch| ('\u{00c0}'..='\u{017f}').contains(ch)))
{
assert!(fonts.has_glyph(&font, ch), "missing Italian 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
@@ -1148,7 +1253,7 @@ mod tests {
}
#[test]
fn german_and_ukrainian_buttons_fit_the_bottom_bar_and_export_dialog() {
fn german_ukrainian_and_italian_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) => {
@@ -1158,7 +1263,7 @@ mod tests {
_ => {}
}
}
for language in [Locale::De, Locale::Uk] {
for language in [Locale::De, Locale::Uk, Locale::It] {
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);
+2
View File
@@ -26,6 +26,7 @@ pub const LANGUAGE_COMMANDS: &[UiCommand] = &[
("app.language.ukrainian", crate::i18n::Locale::Uk.name(), None, "Edit>Language"),
("app.language.polish", crate::i18n::Locale::Pl.name(), None, "Edit>Language"),
("app.language.czech", crate::i18n::Locale::Cs.name(), None, "Edit>Language"),
("app.language.italian", crate::i18n::Locale::It.name(), None, "Edit>Language"),
];
/// Every UI command: the languages, then everything else. `xtask parity` reads both tables from
@@ -50,6 +51,7 @@ pub fn language_from_command(id: &str) -> Option<crate::i18n::Locale> {
"app.language.ukrainian" => Some(crate::i18n::Locale::Uk),
"app.language.polish" => Some(crate::i18n::Locale::Pl),
"app.language.czech" => Some(crate::i18n::Locale::Cs),
"app.language.italian" => Some(crate::i18n::Locale::It),
_ => None,
}
}
+2 -2
View File
@@ -621,13 +621,13 @@ mod tests {
"Export Keywords…",
];
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("locales");
for code in ["de", "es", "fr", "pt-br", "ru", "uk", "ja", "zh-hans", "zh-hant", "cs", "pl"] {
for code in ["de", "es", "fr", "pt-br", "ru", "uk", "it", "ja", "zh-hans", "zh-hant", "pl", "cs"] {
let text = std::fs::read_to_string(dir.join(format!("{code}.json"))).unwrap();
let catalog: std::collections::HashMap<String, String> = serde_json::from_str(&text).unwrap();
for label in labels {
assert!(catalog.contains_key(label), "{code}: “{label}”");
}
}
assert_eq!(Locale::ALL.len(), 12, "a language added: list its file above");
assert_eq!(Locale::ALL.len(), 13, "a language added: list its file above");
}
}
+7 -5
View File
@@ -1,6 +1,6 @@
# Localization parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (de, es, fr, ja, uk catalogs filled; re-measured) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (de, es, fr, ja, uk catalogs filled; re-measured; Italian interface catalog added) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
How far each interface language is from `full`. How to add or maintain a language is in
[localization.md](localization.md); per-language notes are in `localization-<code>.md`
@@ -58,15 +58,16 @@ that is promising but is not recorded as review, so the column says `no (communi
| 11 | Korean | `ko` | 0 (0%) | none | **no Korean (Hangul) face**: needs a non-Adobe OFL face in craft-fonts (Noto CJK is allowed only for Chinese) | no | none | 14–22 h |
| 12 | Vietnamese | `vi` | 0 (0%) | none | Latin with stacked diacritics (Inter covers it; check combining marks) | no | none (target: not shipped) | 10–16 h |
## Other languages LightCraft ships (3)
## Other languages LightCraft ships (4)
| Language | Code | UI strings translated | Script | Native review | Status | To `full` |
|---|---|---:|---|---|---|---:|
| Traditional Chinese (Taiwan) | `zh-hant` | 1,993 / 2,304 (87%) | Hant; uses the Simplified Chinese face until craft-fonts has a TC face | no (community edits) | partial | 8–14 h (incl. the font) |
| Russian | `ru` | 1,704 / 2,304 (74%) | Cyrillic (Inter) | no (community edits) | partial | 8–12 h |
| Ukrainian | `uk` | 2,383 / 2,398 (99%; 2026-10-12) | Cyrillic (Inter) | no (community edits) | partial (lower-layer errors English) | 2–4 h |
| Italian | `it` | 2,201 / 2,398 (92%, measured 2026-10-12; the rest is What's New) | Latin (Inter) | no (written with an agent; native-speaker pass pending) | partial (lower-layer errors English) | 3–5 h |
Target languages LightCraft lacks: Italian, Korean, Dutch, Polish, Swedish, Norwegian Bokmål, Thai
Target languages LightCraft lacks: Korean, Dutch, Polish, Swedish, Norwegian Bokmål, Thai
(Thai needs shaping and line breaking without spaces). Each Latin-script one is ~10–16 h; Thai
~20–35 h on top of the shaping work.
@@ -83,14 +84,15 @@ Target languages LightCraft lacks: Italian, Korean, Dutch, Polish, Swedish, Norw
## Totals
- Bring the 9 shipped non-English catalogs to `full`: ~55–90 h (incl. the shared work).
- Bring the 10 shipped non-English catalogs to `full`: ~55–90 h (incl. the shared work).
- Add the 5 missing key languages: ~145–255 h, most of it RTL and complex shaping in egui.
- Reach the target's 16 languages: + ~60–100 h (Italian, Dutch, Polish, Swedish, Norwegian, Thai).
- Reach the target's 16 languages: + ~50–85 h (Dutch, Polish, Swedish, Norwegian, Thai).
## Revision history
| Date | Change | Summary |
|---|---|---|
| 2026-10-12 | minor | Italian interface catalog (`it`): 2,201 of 2,398 messages, What's New left in English |
| 2026-10-11 | minor | Shared work: the interface decides on error kinds, not English error text (#1046) |
| 2026-10-10 | major | Created: per-language counts measured from the catalogs, the twelve key languages, script support, estimates |
| 2026-10-12 | minor | de, es, fr, ja and uk catalogs filled to every message another catalog knows (agent translations, native review welcome); those rows re-measured against the current 2,398 messages. Other rows keep their 2026-10-10 counts (open PRs for pt-BR, ru, zh) |
+1
View File
@@ -32,6 +32,7 @@ are in [`localization-zh-hans.md`](localization-zh-hans.md), [`localization-zh-h
Ru, "ru", "Русский", "Cyrl", include_str!("../locales/ru.json");
Uk, "uk", "Українська", "Cyrl", include_str!("../locales/uk.json");
Cs, "cs", "Čeština", "Latn", include_str!("../locales/cs.json");
It, "it", "Italiano", "Latn", include_str!("../locales/it.json");
}
```