mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-15 03:36:48 -04:00
Improve Korean terminology and dynamic UI translation coverage (#604)
* Improve Korean Photoshop terminology and dynamic UI translations
* Keep untranslated form labels in sentence case; add the new strings to de and pt-br
label_of fell back to the Title Case filter label whenever the sentence-case label had no
translation, which changed English form labels ("Use antialias" -> "Use Antialias"). It now
uses the Title Case translation only when one exists. German and Brazilian Portuguese (merged
since this branch was opened) get "No patterns" and "Enable {name}".
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: kijoong <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
25 files changed
+1337
-539
No files matched your search
@@ -582,7 +582,7 @@ pub fn windows(app: &mut PhotocraftApp, ctx: &egui::Context) {
|
||||
egui::Grid::new("mlog-grid").striped(true).spacing(vec2(10.0, 2.0)).show(ui, |ui| {
|
||||
for c in &cols {
|
||||
let name = photocraft_engine::analysis_cmds::COLUMNS.iter().find(|x| x.0 == *c).map_or(*c, |x| x.1);
|
||||
ui.label(RichText::new(name).color(t.text_dim).size(10.5).strong());
|
||||
ui.label(RichText::new(tl!(name)).color(t.text_dim).size(10.5).strong());
|
||||
}
|
||||
ui.end_row();
|
||||
for r in &rows {
|
||||
|
||||
@@ -231,7 +231,7 @@ fn section_list(app: &mut PhotocraftApp, ui: &mut egui::Ui, b: &mut BrushSetting
|
||||
let sel = app.ui.brush_section == i;
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(150.0, 23.0), Sense::click());
|
||||
// Named for accessibility and for tests and agents that look sections up by name.
|
||||
resp.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::SelectableLabel, true, sel, *name));
|
||||
resp.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::SelectableLabel, true, sel, tl!(*name)));
|
||||
if sel {
|
||||
ui.painter().rect_filled(r, t.radius_sm, t.accent_soft);
|
||||
} else if resp.hovered() {
|
||||
@@ -242,7 +242,9 @@ fn section_list(app: &mut PhotocraftApp, ui: &mut egui::Ui, b: &mut BrushSetting
|
||||
let br = egui::Rect::from_center_size(egui::pos2(x + 6.0, r.center().y), vec2(13.0, 13.0));
|
||||
let box_resp = ui.interact(br, ui.id().with(("brush-sec-box", i)), Sense::click());
|
||||
let checked = *flag;
|
||||
box_resp.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Checkbox, true, checked, format!("Enable {name}")));
|
||||
box_resp.widget_info(|| {
|
||||
egui::WidgetInfo::selected(egui::WidgetType::Checkbox, true, checked, crate::i18n::fmt(tl!("Enable {name}"), &[("name", tl!(*name))]))
|
||||
});
|
||||
if *flag {
|
||||
ui.painter().rect_filled(br, 2.0, t.accent);
|
||||
let (a, m, c) = (br.left_center() + vec2(3.0, 0.5), br.center_bottom() + vec2(-1.0, -3.5), br.right_top() + vec2(-3.0, 3.5));
|
||||
|
||||
@@ -99,7 +99,7 @@ fn dynamic(ui: &mut egui::Ui, id: &str, label: &str, d: &mut Dynamic, max: f32,
|
||||
fn size_row(ui: &mut egui::Ui, label: &str, size: &mut f32, max: f32, restore: Option<f32>) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(RichText::new(label).color(t.text_dim));
|
||||
ui.label(RichText::new(tl!(label)).color(t.text_dim));
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
let mut s = *size;
|
||||
if widgets::value_field(ui, &mut s, 1.0..=max, "px", 74.0).changed() {
|
||||
|
||||
@@ -124,6 +124,7 @@ pub(crate) fn draw_readout(ctx: &egui::Context, id: &str, cursor: Pos2, labels:
|
||||
let t = crate::theme::Tokens::get(ctx);
|
||||
let font = egui::FontId::proportional(11.5);
|
||||
let width = |text: &str| ctx.fonts_mut(|f| f.layout_no_wrap(text.to_owned(), font.clone(), t.text).size().x);
|
||||
let labels = labels.map(|label| tl!(label));
|
||||
let (lw, vw) = (labels.map(width), [width(&values[0]), width(&values[1])]);
|
||||
let (label_col, value_col) = (lw[0].max(lw[1]), vw[0].max(vw[1]));
|
||||
egui::Area::new(egui::Id::new(id)).order(egui::Order::Tooltip).fixed_pos(cursor + vec2(16.0, 18.0)).interactable(false).constrain(true).show(ctx, |ui| {
|
||||
|
||||
@@ -157,7 +157,7 @@ pub fn body(app: &PhotocraftApp, ui: &mut egui::Ui, f: &mut Map<String, Value>)
|
||||
let mut pat = get_str(f, "pattern", "");
|
||||
let opts: Vec<(String, &str)> = app.session.patterns.items.iter().map(|p| (p.id.clone(), p.display_name())).collect();
|
||||
if opts.is_empty() {
|
||||
ui.label(egui::RichText::new("No patterns").color(Tokens::get(ui.ctx()).text_faint));
|
||||
ui.label(egui::RichText::new(tl!("No patterns")).color(Tokens::get(ui.ctx()).text_faint));
|
||||
} else if crate::widgets::dropdown(ui, "fill-pattern", &mut pat, &opts, 170.0) {
|
||||
f.insert("pattern".into(), json!(pat));
|
||||
}
|
||||
|
||||
@@ -224,6 +224,10 @@ pub fn open_with_spec(app: &mut PhotocraftApp, command: &str, label: &str, spec:
|
||||
}
|
||||
|
||||
pub(crate) fn label(key: &str) -> String {
|
||||
tl!(&source_label(key)).to_owned()
|
||||
}
|
||||
|
||||
pub(crate) fn source_label(key: &str) -> String {
|
||||
// camelCase → "Camel Case"
|
||||
let mut s = String::new();
|
||||
for (i, ch) in key.chars().enumerate() {
|
||||
@@ -469,6 +473,41 @@ mod tests {
|
||||
assert!(!has_dialog("filter.stylize.findEdges"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn korean_covers_generated_filter_options_and_gallery_names() {
|
||||
let ko = crate::i18n::lang_from_tag("ko-KR").unwrap();
|
||||
let mut missing = std::collections::BTreeSet::new();
|
||||
let mut check = |s: String| {
|
||||
if !matches!(s.as_str(), "X" | "Y" | "A" | "B") && !crate::i18n::has(ko, &s) {
|
||||
missing.insert(s);
|
||||
}
|
||||
};
|
||||
for c in photocraft_engine::command_specs().iter().filter(|c| c.id.starts_with("filter.") || c.id.starts_with("image.adjustments.")) {
|
||||
for p in parse_spec(c.params) {
|
||||
if !p.key.chars().all(|c| c.is_ascii_alphanumeric()) || matches!(p.kind, Kind::Json) {
|
||||
continue;
|
||||
}
|
||||
check(source_label(&p.key));
|
||||
if let Kind::Choice(choices) = p.kind {
|
||||
for choice in choices {
|
||||
// Only symbolic options are UI choices; registry docs also contain
|
||||
// colour syntax and array notation, which are not translatable names.
|
||||
if choice.chars().all(|c| c.is_ascii_alphanumeric()) {
|
||||
check(source_label(&choice));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for f in photocraft_algo::GalleryFilter::ALL {
|
||||
check(f.name().to_string());
|
||||
}
|
||||
for cat in photocraft_algo::GALLERY_CATEGORIES {
|
||||
check(cat.to_string());
|
||||
}
|
||||
assert!(missing.is_empty(), "missing Korean dynamic labels: {missing:#?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preview_runs_engine_command_on_proxy() {
|
||||
let mut doc = Document::with_background(
|
||||
|
||||
@@ -357,7 +357,7 @@ pub fn show(app: &mut PhotocraftApp, ctx: &egui::Context) {
|
||||
painter.rect_filled(title, 0.0, t.dock);
|
||||
painter.line_segment([title.left_bottom(), title.right_bottom()], Stroke::new(1.0, t.separator));
|
||||
let pct = if d.zoom > 0.0 { format!("{:.0}%", d.zoom * 100.0) } else { tl!("Fit").into() };
|
||||
let name = d.effects.get(d.selected).map_or("", |e| e.filter.name());
|
||||
let name = d.effects.get(d.selected).map_or("", |e| tl!(e.filter.name()));
|
||||
painter.text(title.center(), Align2::CENTER_CENTER, format!("{name} ({}, {pct})", d.layer_name), FontId::proportional(13.0), t.text);
|
||||
let body = ERect::from_min_max(pos2(full.left(), title.bottom()), full.max);
|
||||
let right = ERect::from_min_size(pos2(body.right() - RIGHT_W, body.top()), vec2(RIGHT_W, body.height()));
|
||||
@@ -395,7 +395,7 @@ pub fn show(app: &mut PhotocraftApp, ctx: &egui::Context) {
|
||||
if widgets::dropdown(&mut zb, "gallery-zoom", &mut z, &opts, 110.0) {
|
||||
d.zoom = z;
|
||||
}
|
||||
zb.label(egui::RichText::new(format!("preview {:.0} ms", d.preview_ms)).color(t.text_faint).size(11.0));
|
||||
zb.label(egui::RichText::new(format!("{} {:.0} ms", tl!("Preview"), d.preview_ms)).color(t.text_faint).size(11.0));
|
||||
|
||||
// ---- Category folders with thumbnails ----
|
||||
painter.rect_filled(mid, 0.0, t.dock);
|
||||
@@ -408,7 +408,7 @@ pub fn show(app: &mut PhotocraftApp, ctx: &egui::Context) {
|
||||
let r = ui.horizontal(|ui| {
|
||||
let (ir, _) = ui.allocate_exact_size(vec2(16.0, 16.0), Sense::hover());
|
||||
crate::icons::paint(ui, ir, icon, 14.0, t.icon);
|
||||
ui.add(egui::Label::new(egui::RichText::new(*cat).color(t.text)).sense(Sense::click()))
|
||||
ui.add(egui::Label::new(egui::RichText::new(tl!(*cat)).color(t.text)).sense(Sense::click()))
|
||||
});
|
||||
if r.inner.clicked() {
|
||||
d.open[ci] = !d.open[ci];
|
||||
@@ -434,7 +434,7 @@ pub fn show(app: &mut PhotocraftApp, ctx: &egui::Context) {
|
||||
ui.painter().rect_stroke(r, 0.0, stroke, egui::StrokeKind::Outside);
|
||||
ui.add_sized(
|
||||
[THUMB[0] as f32, 14.0],
|
||||
egui::Label::new(egui::RichText::new(f.name()).size(10.5).color(if selected { t.text } else { t.text_dim })).truncate(),
|
||||
egui::Label::new(egui::RichText::new(tl!(f.name())).size(10.5).color(if selected { t.text } else { t.text_dim })).truncate(),
|
||||
);
|
||||
resp
|
||||
});
|
||||
@@ -539,7 +539,7 @@ pub fn show(app: &mut PhotocraftApp, ctx: &egui::Context) {
|
||||
ui.painter().text(
|
||||
row.left_center() + vec2(28.0, 0.0),
|
||||
Align2::LEFT_CENTER,
|
||||
d.effects[i].filter.name(),
|
||||
tl!(d.effects[i].filter.name()),
|
||||
FontId::proportional(12.5),
|
||||
if d.effects[i].visible { t.text } else { t.text_faint },
|
||||
);
|
||||
|
||||
@@ -2067,3 +2067,5 @@ cameraRaw Color Barvy
|
||||
Patch: Záplata:
|
||||
Lasso around an area, then drag the selection Obtáhněte oblast lasem a pak přetáhněte výběr
|
||||
Edit Type Upravit text
|
||||
No patterns Žádné vzorky
|
||||
Enable {name} Povolit {name}
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -2108,3 +2108,5 @@
|
||||
175% 175 %
|
||||
250% 250 %
|
||||
300% 300 %
|
||||
No patterns Keine Muster
|
||||
Enable {name} {name} aktivieren
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -2144,3 +2144,5 @@ cameraRaw Amount Cantidad
|
||||
Patch: Parche:
|
||||
Lasso around an area, then drag the selection Rodee un área con el lazo y arrastre la selección
|
||||
Edit Type Editar texto
|
||||
No patterns Sin motivos
|
||||
Enable {name} Activar {name}
|
||||
|
Can't render this file because it contains an unexpected character in line 4 and column 47.
|
@@ -2497,3 +2497,5 @@ cameraRaw Blending Fusion
|
||||
5 x 5 5 × 5
|
||||
Grid: Grille :
|
||||
Edit Type Modifier le texte
|
||||
No patterns Aucun motif
|
||||
Enable {name} Activer {name}
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -2484,3 +2484,5 @@ cameraRaw Blending Pembauran
|
||||
5 x 5 5 × 5
|
||||
Grid: Kisi-kisi:
|
||||
Edit Type Edit Teks
|
||||
No patterns Tidak ada pola
|
||||
Enable {name} Aktifkan {name}
|
||||
|
Can't render this file because it contains an unexpected character in line 4 and column 47.
|
@@ -2050,3 +2050,5 @@ cameraRaw Color カラー
|
||||
Patch: パッチ:
|
||||
Lasso around an area, then drag the selection 投げ縄で範囲を囲み、選択範囲をドラッグします
|
||||
Edit Type 文字を編集
|
||||
No patterns パターンなし
|
||||
Enable {name} {name}を有効にする
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
+1139
-521
File diff suppressed because it is too large.
Load diff
@@ -438,6 +438,37 @@ mod tests {
|
||||
assert_eq!(tr(zh, "no such label"), "no such label");
|
||||
}
|
||||
|
||||
/// Keep the Korean tool/menu vocabulary aligned with the Photoshop equivalents.
|
||||
/// Sources and the product-specific vocabulary policy are recorded in `ko.tsv`.
|
||||
#[test]
|
||||
fn korean_uses_photoshop_terminology() {
|
||||
let ko = Lang::from_code("ko").expect("ko registered");
|
||||
for (source, expected) in [
|
||||
("Shape", "모양"),
|
||||
("Stroke", "획"),
|
||||
("Smudge Tool", "손가락 도구"),
|
||||
("Eyedropper Tool", "스포이드 도구"),
|
||||
("Rectangular Marquee Tool", "사각형 선택 윤곽 도구"),
|
||||
("Elliptical Marquee Tool", "원형 선택 윤곽 도구"),
|
||||
("Zoom Tool", "돋보기 도구"),
|
||||
("Horizontal Type Tool", "수평 문자 도구"),
|
||||
("Puppet Warp", "퍼펫 뒤틀기"),
|
||||
("Liquify…", "픽셀 유동화…"),
|
||||
("Gaussian Blur…", "가우시안 흐림 효과…"),
|
||||
("Gaussian Blur", "가우시안 흐림 효과"),
|
||||
("Adaptive Wide Angle…", "응용 광각…"),
|
||||
("Render", "렌더"),
|
||||
("Sharpen", "선명 효과"),
|
||||
("Vibrance", "활기"),
|
||||
("Layer Comps", "레이어 구성 요소"),
|
||||
("Vivid Light", "선명한 라이트"),
|
||||
("Hard Mix", "하드 혼합"),
|
||||
] {
|
||||
assert_eq!(tr(ko, source), expected, "{source}");
|
||||
}
|
||||
assert_eq!(tr_id(ko, "filter.sharpen.sharpen", "Sharpen"), "선명하게");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spanish_resolves_and_pluralises() {
|
||||
let es = Lang::from_code("es").expect("es registered");
|
||||
|
||||
@@ -2507,3 +2507,5 @@ cameraRaw Luminance Detail Detalhe da luminância
|
||||
cameraRaw Color Detail Detalhe da cor
|
||||
cameraRaw Roughness Aspereza
|
||||
cameraRaw Whites Brancos
|
||||
No patterns Nenhum padrão
|
||||
Enable {name} Ativar {name}
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -2345,3 +2345,5 @@ cameraRaw Roughness Неровность
|
||||
Patch: Заплатка:
|
||||
Lasso around an area, then drag the selection Обведите область лассо, затем перетащите выделение
|
||||
Edit Type Редактировать текст
|
||||
No patterns Нет узоров
|
||||
Enable {name} Включить {name}
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -2076,3 +2076,5 @@ cameraRaw Blending 混合
|
||||
Details 详细信息
|
||||
Rendering mode 渲染模式
|
||||
Edit Type 编辑文字
|
||||
No patterns 无图案
|
||||
Enable {name} 启用{name}
|
||||
|
@@ -2045,3 +2045,5 @@ cameraRaw Blending 混合
|
||||
Patch: 修補:
|
||||
Lasso around an area, then drag the selection 用套索圈出區域,然後拖曳選取範圍
|
||||
Edit Type 編輯文字
|
||||
No patterns 無圖樣
|
||||
Enable {name} 啟用{name}
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -263,7 +263,7 @@ pub fn status_progress(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
|
||||
ui.label(RichText::new(percent(&j)).color(t.text_dim).size(11.5).monospace());
|
||||
let (br, _) = ui.allocate_exact_size(vec2(bar_w, bar_h), Sense::hover());
|
||||
bar(ui, br, shown_fraction(&j), &t);
|
||||
let label = if more > 0 { format!("{} (+{more})", j.label) } else { j.label.clone() };
|
||||
let label = if more > 0 { format!("{} (+{more})", tl!(&j.label)) } else { tl!(&j.label).to_owned() };
|
||||
ui.label(RichText::new(label).color(t.text_dim).size(12.0));
|
||||
if xresp.clicked() {
|
||||
cancel(app, j.id);
|
||||
@@ -283,7 +283,7 @@ pub fn dialog(app: &mut PhotocraftApp, ctx: &egui::Context) {
|
||||
// Photoshop doesn't dim the window behind its progress dialog either.
|
||||
let modal = egui::Modal::new(egui::Id::new("job-progress")).backdrop_color(Color32::from_black_alpha(36)).show(ctx, |ui| {
|
||||
ui.set_width(360.0);
|
||||
ui.label(RichText::new(&j.label).font(crate::theme::semibold(15.0)));
|
||||
ui.label(RichText::new(tl!(&j.label)).font(crate::theme::semibold(15.0)));
|
||||
ui.add_space(4.0);
|
||||
crate::widgets::hairline(ui);
|
||||
ui.add_space(10.0);
|
||||
|
||||
@@ -590,7 +590,13 @@ pub fn show(app: &mut PhotocraftApp, ctx: &egui::Context) {
|
||||
painter.rect_filled(title, 0.0, t.dock);
|
||||
painter.line_segment([title.left_bottom(), title.right_bottom()], Stroke::new(1.0, t.separator));
|
||||
let pct = if d.zoom > 0.0 { d.zoom * 100.0 } else { 100.0 };
|
||||
painter.text(title.center(), Align2::CENTER_CENTER, format!("Liquify ({}, {:.0}%)", d.layer_name, pct), FontId::proportional(13.0), t.text);
|
||||
painter.text(
|
||||
title.center(),
|
||||
Align2::CENTER_CENTER,
|
||||
format!("{} ({}, {:.0}%)", tl!("Liquify…").trim_end_matches('…'), d.layer_name, pct),
|
||||
FontId::proportional(13.0),
|
||||
t.text,
|
||||
);
|
||||
let body = ERect::from_min_max(pos2(full.left(), title.bottom()), full.max);
|
||||
// Left tool strip.
|
||||
let left = ERect::from_min_size(body.min, vec2(LEFT_W, body.height()));
|
||||
|
||||
@@ -740,7 +740,7 @@ pub fn options_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
|
||||
let mut section = |ui: &mut egui::Ui, title: &str, prefix: &str| {
|
||||
ui.label(egui::RichText::new(tl!(&title)).small().color(t.text_dim));
|
||||
for c in photocraft_engine::command_specs().iter().filter(|c| c.id.starts_with(prefix)) {
|
||||
if ui.add_enabled(app.session.is_enabled(c.id), egui::Button::new(c.label)).clicked() {
|
||||
if ui.add_enabled(app.session.is_enabled(c.id), egui::Button::new(tl!(c.label))).clicked() {
|
||||
let _ = app.run(c.id, json!({}));
|
||||
ui.close();
|
||||
}
|
||||
@@ -1462,7 +1462,7 @@ fn layers(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
|
||||
egui::Popup::menu(&adj).show(|ui| {
|
||||
ui.set_min_width(190.0);
|
||||
for c in photocraft_engine::command_specs().iter().filter(|c| c.id.starts_with("layer.newAdjustmentLayer.")) {
|
||||
if ui.button(c.label.trim_end_matches('…')).clicked() {
|
||||
if ui.button(tl!(c.label).trim_end_matches('…')).clicked() {
|
||||
actions.push((c.id.into(), json!({})));
|
||||
ui.close();
|
||||
}
|
||||
@@ -1498,7 +1498,7 @@ fn layers(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
|
||||
}
|
||||
ui.separator();
|
||||
for &(kind, label) in crate::layer_style::KINDS {
|
||||
if ui.button(format!("{label}…")).clicked() {
|
||||
if ui.button(format!("{}…", tl!(label))).clicked() {
|
||||
crate::layer_style::open(app, Some(kind));
|
||||
ui.close();
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ fn filter_label(f: &photocraft_doc::SmartFilter) -> String {
|
||||
return format!("{} (kept, not editable)", name.trim_end_matches("...").trim_end_matches('…'));
|
||||
}
|
||||
photocraft_engine::commands::find(command)
|
||||
.map_or_else(|| command.rsplit('.').next().unwrap_or(command).to_string(), |c| c.label.trim_end_matches('…').to_string())
|
||||
.map_or_else(|| command.rsplit('.').next().unwrap_or(command).to_string(), |c| tl!(c.label).trim_end_matches('…').to_string())
|
||||
}
|
||||
|
||||
/// Smart Filters header + one row per filter (top filter first, as in Photoshop) under a smart
|
||||
|
||||
@@ -704,7 +704,27 @@ fn label_of(key: &str) -> String {
|
||||
out.push(c);
|
||||
}
|
||||
}
|
||||
out
|
||||
let translated = tl!(&out);
|
||||
if translated != out {
|
||||
return translated.to_owned();
|
||||
}
|
||||
// Fall back to the Title Case filter label's translation, but keep the sentence-case
|
||||
// English when that is untranslated too (English and partial catalogs read as before).
|
||||
let title = crate::filter_dialog::label(key);
|
||||
if title == crate::filter_dialog::source_label(key) { out } else { title }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod label_tests {
|
||||
use super::label_of;
|
||||
|
||||
#[test]
|
||||
fn untranslated_form_labels_stay_sentence_case() {
|
||||
crate::i18n::with_language(crate::i18n::Lang::EN, || {
|
||||
assert_eq!(label_of("useAntialias"), "Use antialias");
|
||||
assert_eq!(label_of("radius"), "Radius");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Body of a `__form` dialog: text fields, number fields, checkboxes and `__choices` dropdowns.
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
# Korean terminology and dynamic UI labels
|
||||
|
||||
This supplements the Korean catalog merged in #582. The existing language
|
||||
registry, live language switching and Korean font loading are retained.
|
||||
Translations use the meaning of PhotoCraft's English labels and terminology
|
||||
observed in the Korean Photoshop UI in October 2026. No proprietary translation
|
||||
resources, fonts, screenshots or user documents are included.
|
||||
|
||||
## Terminology
|
||||
|
||||
| English source | Korean UI |
|
||||
|---|---|
|
||||
| Gaussian Blur | 가우시안 흐림 효과 |
|
||||
| Liquify | 픽셀 유동화 |
|
||||
| Adaptive Wide Angle | 응용 광각 |
|
||||
| Brush Tip Shape | 브러시 끝 모양 |
|
||||
| Smudge Tool | 손가락 도구 |
|
||||
| Puppet Warp | 퍼펫 뒤틀기 |
|
||||
| Render (menu) | 렌더 |
|
||||
| Sharpen (menu) | 선명 효과 |
|
||||
| Sharpen (command) | 선명하게 |
|
||||
|
||||
Command-ID translations disambiguate menu names from actions. Photoshop terms
|
||||
are used when the operation matches; PhotoCraft-specific features retain their
|
||||
own meaning. Technology names, units and user-provided names are not translated.
|
||||
The terminology test protects representative tool names and this distinction.
|
||||
|
||||
## Dynamic coverage
|
||||
|
||||
Generated filter parameter names, symbolic choices, gallery categories/effects,
|
||||
job titles, layer-style menus and shortcut labels now go through the catalog.
|
||||
The filter test enumerates engine filter/adjustment parameter specs and gallery
|
||||
names to catch new Korean omissions that literal-only scans cannot find.
|
||||
Two newly introduced literals, `Enable {name}` and `No patterns`, are present in
|
||||
all nine non-English catalogs. Existing fallback behavior is preserved for
|
||||
untranslated dynamic labels in other languages.
|
||||
|
||||
This is not a claim that all application output is localized. Engine error
|
||||
messages, arbitrary plug-in labels and user document/layer/preset names remain
|
||||
outside this translation change. The source coverage report measures its
|
||||
scanned key set, not every possible runtime string.
|
||||
|
||||
## Validation
|
||||
|
||||
On main 7055f0c plus this change:
|
||||
|
||||
- UI tests: 728 passed, 3 ignored, including live language-switch tests.
|
||||
- Clippy for the UI crate and all targets: passed with warnings denied.
|
||||
- i18n coverage: 1,406/1,406 scanned keys for all ten registered languages;
|
||||
all seven coverage-tool tests passed.
|
||||
- Dependency-layer and WebAssembly checks passed. Existing CMS dead-code
|
||||
warnings appear during WebAssembly checks.
|
||||
- Offscreen French-to-Korean language switching and the Korean brush settings
|
||||
panel were rendered and visually inspected for missing glyphs/clipping.
|
||||
|
||||
Re-run with `cargo test -p photocraft-ui-egui --locked`,
|
||||
`cargo clippy -p photocraft-ui-egui --all-targets --locked -- -D warnings`,
|
||||
`cargo test -p xtask i18n_coverage`, `cargo xtask i18n-coverage`,
|
||||
`cargo xtask layers`, and `cargo xtask wasm`.
|
||||
|
||||
IME composition and PSD font matching are separate contributions.
|
||||
Reference in new issue
Block a user