From 967335d4ca91f63c324dfbb1f84d9b13b60b9b5f Mon Sep 17 00:00:00 2001 From: Priec Date: Fri, 14 Aug 2026 20:52:31 +0200 Subject: [PATCH] i18n on the web - deepseek translations2 --- web/locales/cs/main.ftl | 5 ++++ web/locales/en/main.ftl | 5 ++++ web/locales/sk/main.ftl | 5 ++++ web/src/pages/add_logic/logic.rs | 9 ++++++- web/src/pages/add_table/draft.rs | 2 +- web/src/pages/add_table/ui.rs | 2 +- web/src/pages/add_validation/logic.rs | 27 ++++++++++++++++--- web/src/pages/admin/admin/loader.rs | 23 +++++++++++----- web/src/pages/admin/table_definition/state.rs | 4 +-- web/src/pages/admin/table_definition/ui.rs | 2 +- web/src/pages/import_export/common/csv.rs | 2 +- web/src/pages/import_export/export/logic.rs | 9 ++++++- web/src/pages/import_export/import/logic.rs | 9 ++++++- web/src/pages/permissions/grants/loader.rs | 2 +- web/src/schema/mod.rs | 26 +++++++++--------- web/src/services/mod.rs | 11 +++++++- 16 files changed, 110 insertions(+), 33 deletions(-) diff --git a/web/locales/cs/main.ftl b/web/locales/cs/main.ftl index de6bd1c2..e01b0112 100644 --- a/web/locales/cs/main.ftl +++ b/web/locales/cs/main.ftl @@ -15,6 +15,7 @@ ui-back-to-admin-panel = ← Administrační panel ui-back-to-form = Zpět do formuláře ui-dismiss-alert = zavřít upozornění notice-log-in-first = Nejprve se prosím přihlaste. +services-cross-site-rejected = Zamítnuto odeslání formuláře z jiné stránky nav-admin = Administrace nav-permissions = Oprávnění @@ -112,6 +113,10 @@ admin-unavailable-title = Administrační panel není dostupný admin-forbidden = Tento účet nemá oprávnění otevřít administrační panel. admin-profiles = Profily +admin-global-label = Globální +admin-err-unknown-profile = Neznámý profil '{ $name }' +admin-err-table-not-in-profile = Tabulka '{ $name }' není součástí vybraného profilu +admin-err-backend-no-structure = Backend nevrátil strukturu pro '{ $name }' admin-no-profiles-available = Nejsou k dispozici žádné profily. admin-table-count = { $count -> [one] { $count } tabulka diff --git a/web/locales/en/main.ftl b/web/locales/en/main.ftl index 73eef2ae..718f2215 100644 --- a/web/locales/en/main.ftl +++ b/web/locales/en/main.ftl @@ -20,6 +20,7 @@ ui-back-to-admin-panel = ← Admin panel ui-back-to-form = Back to the form ui-dismiss-alert = dismiss alert notice-log-in-first = Please log in first. +services-cross-site-rejected = Cross-site form submission rejected nav-admin = Admin nav-permissions = Permissions @@ -117,6 +118,10 @@ admin-unavailable-title = Admin panel unavailable admin-forbidden = This account is not allowed to open the admin panel. admin-profiles = Profiles +admin-global-label = Global +admin-err-unknown-profile = Unknown profile '{ $name }' +admin-err-table-not-in-profile = Table '{ $name }' is not part of the selected profile +admin-err-backend-no-structure = The backend did not return the structure for '{ $name }' admin-no-profiles-available = No profiles available. admin-table-count = { $count -> [one] { $count } table diff --git a/web/locales/sk/main.ftl b/web/locales/sk/main.ftl index f82b3e41..eb63eb18 100644 --- a/web/locales/sk/main.ftl +++ b/web/locales/sk/main.ftl @@ -15,6 +15,7 @@ ui-back-to-admin-panel = ← Administračný panel ui-back-to-form = Späť do formulára ui-dismiss-alert = zatvoriť upozornenie notice-log-in-first = Najprv sa prosím prihláste. +services-cross-site-rejected = Zamietnuté odoslanie formulára z inej stránky nav-admin = Administrácia nav-permissions = Oprávnenia @@ -112,6 +113,10 @@ admin-unavailable-title = Administračný panel nie je dostupný admin-forbidden = Tento účet nemá povolenie otvoriť administračný panel. admin-profiles = Profily +admin-global-label = Globálne +admin-err-unknown-profile = Neznámy profil '{ $name }' +admin-err-table-not-in-profile = Tabuľka '{ $name }' nie je súčasťou vybraného profilu +admin-err-backend-no-structure = Backend nevrátil štruktúru pre '{ $name }' admin-no-profiles-available = Nie sú k dispozícii žiadne profily. admin-table-count = { $count -> [one] { $count } tabuľka diff --git a/web/src/pages/add_logic/logic.rs b/web/src/pages/add_logic/logic.rs index 5f0007b3..334f1966 100644 --- a/web/src/pages/add_logic/logic.rs +++ b/web/src/pages/add_logic/logic.rs @@ -37,7 +37,14 @@ pub(crate) async fn create_logic( .get("sec-fetch-site") .is_some_and(|value| value == "cross-site") { - return (StatusCode::FORBIDDEN, "Cross-site form submission rejected").into_response(); + return ( + StatusCode::FORBIDDEN, + tr!( + Locale::from_headers(&headers), + "services-cross-site-rejected" + ), + ) + .into_response(); } let request = match form.into_request(Locale::from_headers(&headers)) { Ok(request) => request, diff --git a/web/src/pages/add_table/draft.rs b/web/src/pages/add_table/draft.rs index 7a58d142..e21f2027 100644 --- a/web/src/pages/add_table/draft.rs +++ b/web/src/pages/add_table/draft.rs @@ -1089,6 +1089,6 @@ mod tests { source: "name".to_string(), alias: "row_revision".to_string(), }]; - assert!(draft.validate_generated_aliases().is_err()); + assert!(draft.validate_generated_aliases(crate::i18n::Locale::default()).is_err()); } } diff --git a/web/src/pages/add_table/ui.rs b/web/src/pages/add_table/ui.rs index cce9c359..b61c1128 100644 --- a/web/src/pages/add_table/ui.rs +++ b/web/src/pages/add_table/ui.rs @@ -512,7 +512,7 @@ mod tests { /// A load failure has no draft to render, so the dialog is the response. #[test] fn a_load_failure_answers_with_the_dialog() { - let html = render_submission_error("The backend is unreachable."); + let html = render_submission_error(crate::i18n::Locale::default(), "The backend is unreachable."); assert!(!html.contains("Template error"), "{html}"); assert!(html.contains(r#"role="dialog""#)); diff --git a/web/src/pages/add_validation/logic.rs b/web/src/pages/add_validation/logic.rs index dfd21e04..5a4c1358 100644 --- a/web/src/pages/add_validation/logic.rs +++ b/web/src/pages/add_validation/logic.rs @@ -37,7 +37,14 @@ pub(crate) async fn save_field_validation( return render_loaded(&headers, Err(error)); } if cross_site(&headers) { - return (StatusCode::FORBIDDEN, "Cross-site form submission rejected").into_response(); + return ( + StatusCode::FORBIDDEN, + tr!( + Locale::from_headers(&headers), + "services-cross-site-rejected" + ), + ) + .into_response(); } if !form.validation_set_name.trim().is_empty() { let request = match form.apply_set_request(Locale::from_headers(&headers)) { @@ -125,7 +132,14 @@ pub(crate) async fn save_validation_rule( return render_loaded(&headers, Err(error)); } if cross_site(&headers) { - return (StatusCode::FORBIDDEN, "Cross-site form submission rejected").into_response(); + return ( + StatusCode::FORBIDDEN, + tr!( + Locale::from_headers(&headers), + "services-cross-site-rejected" + ), + ) + .into_response(); } let request = match form.rule_request(Locale::from_headers(&headers)) { Ok(request) => request, @@ -174,7 +188,14 @@ pub(crate) async fn save_validation_set( return render_set_loaded(&headers, Err(error)); } if cross_site(&headers) { - return (StatusCode::FORBIDDEN, "Cross-site form submission rejected").into_response(); + return ( + StatusCode::FORBIDDEN, + tr!( + Locale::from_headers(&headers), + "services-cross-site-rejected" + ), + ) + .into_response(); } let request = match form.request(Locale::from_headers(&headers)) { Ok(request) => request, diff --git a/web/src/pages/admin/admin/loader.rs b/web/src/pages/admin/admin/loader.rs index faccba97..74cd35c7 100644 --- a/web/src/pages/admin/admin/loader.rs +++ b/web/src/pages/admin/admin/loader.rs @@ -61,7 +61,10 @@ pub(crate) async fn load_admin_page( ); let mut profiles = vec![ProfileView { - label: "Global".to_string(), + label: crate::tr!( + crate::i18n::Locale::from_headers(headers), + "admin-global-label" + ), scope: GLOBAL_SCOPE.to_string(), table_count: global_tables.len(), global: true, @@ -78,7 +81,11 @@ pub(crate) async fn load_admin_page( Some(GLOBAL_SCOPE) => Some(global_tables), Some(name) => { if !profile_tree.profiles.iter().any(|profile| profile.name == name) { - return Err(LoadError::InvalidSelection(format!("Unknown profile '{name}'"))); + return Err(LoadError::InvalidSelection(crate::tr!( + crate::i18n::Locale::from_headers(headers), + "admin-err-unknown-profile", + "name" => name.to_string(), + ))); } Some(table_scope::profile_owned_tables(&profile_tree.profiles, name)) } @@ -112,8 +119,10 @@ pub(crate) async fn load_admin_page( if let Some(table_name) = selected_table.as_deref() && (!scope_is_chosen || !tables.iter().any(|table| table.name == table_name)) { - return Err(LoadError::InvalidSelection(format!( - "Table '{table_name}' is not part of the selected profile" + return Err(LoadError::InvalidSelection(crate::tr!( + crate::i18n::Locale::from_headers(headers), + "admin-err-table-not-in-profile", + "name" => table_name.to_string(), ))); } @@ -135,8 +144,10 @@ pub(crate) async fn load_admin_page( .table_structures .get(table_name) .ok_or_else(|| { - LoadError::Backend(format!( - "The backend did not return the structure for '{table_name}'" + LoadError::Backend(crate::tr!( + crate::i18n::Locale::from_headers(headers), + "admin-err-backend-no-structure", + "name" => table_name.to_string(), )) })? .columns diff --git a/web/src/pages/admin/table_definition/state.rs b/web/src/pages/admin/table_definition/state.rs index d71f3c1a..96920668 100644 --- a/web/src/pages/admin/table_definition/state.rs +++ b/web/src/pages/admin/table_definition/state.rs @@ -431,11 +431,11 @@ mod tests { assert_eq!(renameable[0].name, "work_phone"); assert_eq!(renameable[1].name, "charge"); assert_eq!( - detail.columns[1].flags(), + detail.columns[1].flags(&crate::i18n::Locale::default()), vec!["read only", "generated from work_phone"] ); assert_eq!( - detail.columns[2].flags(), + detail.columns[2].flags(&crate::i18n::Locale::default()), vec!["EUR", "generated from accounting"] ); } diff --git a/web/src/pages/admin/table_definition/ui.rs b/web/src/pages/admin/table_definition/ui.rs index 89dd2d30..97f4cbe4 100644 --- a/web/src/pages/admin/table_definition/ui.rs +++ b/web/src/pages/admin/table_definition/ui.rs @@ -591,7 +591,7 @@ mod tests { #[test] fn a_load_failure_answers_with_the_dialog() { - let html = render_load_error("The backend is unreachable."); + let html = render_load_error(crate::i18n::Locale::default(), "The backend is unreachable."); assert!(!html.contains("Template error"), "{html}"); assert!(html.contains(r#"role="dialog""#)); diff --git a/web/src/pages/import_export/common/csv.rs b/web/src/pages/import_export/common/csv.rs index 49bd60d7..663fa4ae 100644 --- a/web/src/pages/import_export/common/csv.rs +++ b/web/src/pages/import_export/common/csv.rs @@ -72,6 +72,6 @@ mod tests { let fields = vec!["name".to_string(), "hello, \"world\"\nnext".to_string()]; let mut csv = String::new(); write_record(&mut csv, &fields); - assert_eq!(parse_csv(&csv).unwrap(), vec![fields]); + assert_eq!(parse_csv(crate::i18n::Locale::default(), &csv).unwrap(), vec![fields]); } } diff --git a/web/src/pages/import_export/export/logic.rs b/web/src/pages/import_export/export/logic.rs index dc51ff87..f82a3ec0 100644 --- a/web/src/pages/import_export/export/logic.rs +++ b/web/src/pages/import_export/export/logic.rs @@ -49,7 +49,14 @@ pub(crate) async fn export_csv( Err(error) => return load_error(&headers, error), }; if cross_site(&headers) { - return (StatusCode::FORBIDDEN, "Cross-site form submission rejected").into_response(); + return ( + StatusCode::FORBIDDEN, + tr!( + Locale::from_headers(&headers), + "services-cross-site-rejected" + ), + ) + .into_response(); } let (profile_name, table_names) = match form.targets(Locale::from_headers(&headers)) { Ok(targets) => targets, diff --git a/web/src/pages/import_export/import/logic.rs b/web/src/pages/import_export/import/logic.rs index e9919c41..79f1c3fe 100644 --- a/web/src/pages/import_export/import/logic.rs +++ b/web/src/pages/import_export/import/logic.rs @@ -49,7 +49,14 @@ pub(crate) async fn import_csv( Err(error) => return load_error(&headers, error), }; if cross_site(&headers) { - return (StatusCode::FORBIDDEN, "Cross-site form submission rejected").into_response(); + return ( + StatusCode::FORBIDDEN, + tr!( + Locale::from_headers(&headers), + "services-cross-site-rejected" + ), + ) + .into_response(); } let (profile_name, table_names) = match form.targets(Locale::from_headers(&headers)) { Ok(targets) => targets, diff --git a/web/src/pages/permissions/grants/loader.rs b/web/src/pages/permissions/grants/loader.rs index 7a06e4cd..58922446 100644 --- a/web/src/pages/permissions/grants/loader.rs +++ b/web/src/pages/permissions/grants/loader.rs @@ -235,7 +235,7 @@ mod tests { let direct = [permission("data:acme/*", "read")]; let effective = [permission("data:acme/*", "read")]; - let groups = group_objects(&objects, &direct, &effective); + let groups = group_objects(crate::i18n::Locale::default(), &objects, &direct, &effective); assert_eq!(groups.len(), 2); assert!(groups[0].global); assert_eq!(groups[1].title, "acme"); diff --git a/web/src/schema/mod.rs b/web/src/schema/mod.rs index f0491904..ba940624 100644 --- a/web/src/schema/mod.rs +++ b/web/src/schema/mod.rs @@ -1718,7 +1718,7 @@ pub(crate) mod tests { draft.type_input = "temporal".to_string(); // Incomplete while no subtype is chosen. - assert_eq!(draft.canonical_type_input().unwrap(), None); + assert_eq!(draft.canonical_type_input(crate::i18n::Locale::default()).unwrap(), None); assert!(draft.show_temporal_type()); draft.temporal_type_input = "raw_datetime".to_string(); @@ -1735,7 +1735,7 @@ pub(crate) mod tests { draft.name_input = "weight".to_string(); draft.type_input = "decimal".to_string(); - assert_eq!(draft.canonical_type_input().unwrap(), None); + assert_eq!(draft.canonical_type_input(crate::i18n::Locale::default()).unwrap(), None); assert!(draft.show_decimal_arguments()); draft.decimal_precision_input = "12".to_string(); draft.decimal_scale_input = "3".to_string(); @@ -1750,7 +1750,7 @@ pub(crate) mod tests { draft.type_input = "link".to_string(); assert!(draft.show_link_target()); - assert_eq!(draft.canonical_type_input().unwrap(), None); + assert_eq!(draft.canonical_type_input(crate::i18n::Locale::default()).unwrap(), None); draft.link_table_input = "customer".to_string(); draft.add_from_inputs(crate::i18n::Locale::default()).unwrap(); @@ -1763,7 +1763,7 @@ pub(crate) mod tests { #[test] fn bare_link_type_is_rejected_without_a_target() { assert_eq!( - draft().catalog.validate_field_type("link"), + draft().catalog.validate_field_type(crate::i18n::Locale::default(), "link"), Some("`link` needs a referenced table.".to_string()) ); } @@ -1858,7 +1858,7 @@ pub(crate) mod tests { assert!(draft.add_from_inputs(crate::i18n::Locale::default()).is_err()); assert!( catalog() - .validate_field_type("phone_calling_code") + .validate_field_type(crate::i18n::Locale::default(), "phone_calling_code") .is_some() ); } @@ -2344,13 +2344,13 @@ pub(crate) mod tests { assert_eq!(MAX_TABLE_NAME_LENGTH, 38); let longest = "t".repeat(MAX_TABLE_NAME_LENGTH); - assert_eq!(validate_table_name(&longest), None); - assert_eq!(validate_identifier(&longest, "Column name", true), None); + assert_eq!(validate_table_name(crate::i18n::Locale::default(), &longest), None); + assert_eq!(validate_identifier(crate::i18n::Locale::default(), &longest, "Column name", true), None); let too_long = "t".repeat(MAX_TABLE_NAME_LENGTH + 1); - assert!(validate_table_name(&too_long).is_some()); + assert!(validate_table_name(crate::i18n::Locale::default(), &too_long).is_some()); // Still a perfectly good column name, which is why the two differ. - assert_eq!(validate_identifier(&too_long, "Column name", true), None); + assert_eq!(validate_identifier(crate::i18n::Locale::default(), &too_long, "Column name", true), None); } /// Every profile is given these tables when it is created, so a new table @@ -2358,11 +2358,11 @@ pub(crate) mod tests { #[test] fn the_tables_every_profile_is_given_keep_their_names() { for name in RESERVED_TABLE_NAMES { - let error = validate_table_name(name) + let error = validate_table_name(crate::i18n::Locale::default(), name) .unwrap_or_else(|| panic!("`{name}` should be reserved")); assert!(error.contains(name), "{error}"); } - assert_eq!(validate_table_name("invoice"), None); + assert_eq!(validate_table_name(crate::i18n::Locale::default(), "invoice"), None); } } @@ -2374,7 +2374,7 @@ mod link_alias_tests { /// derived from a table name any more. #[test] fn a_column_name_may_end_in_id() { - assert_eq!(validate_identifier("external_id", "Column name", true), None); + assert_eq!(validate_identifier(crate::i18n::Locale::default(), "external_id", "Column name", true), None); } /// The system columns stay reserved, since they share one namespace with @@ -2383,7 +2383,7 @@ mod link_alias_tests { fn the_system_columns_stay_reserved() { for name in ["id", "deleted", "created_at", "row_revision"] { assert!( - validate_identifier(name, "Column name", true).is_some(), + validate_identifier(crate::i18n::Locale::default(), name, "Column name", true).is_some(), "`{name}` must stay reserved" ); } diff --git a/web/src/services/mod.rs b/web/src/services/mod.rs index 98128514..ef7c7b5d 100644 --- a/web/src/services/mod.rs +++ b/web/src/services/mod.rs @@ -33,5 +33,14 @@ pub(crate) fn reject_cross_site(headers: &HeaderMap) -> Option { headers .get("sec-fetch-site") .is_some_and(|value| value == "cross-site") - .then(|| (StatusCode::FORBIDDEN, "Cross-site form submission rejected").into_response()) + .then(|| { + ( + StatusCode::FORBIDDEN, + crate::tr!( + crate::i18n::Locale::from_headers(headers), + "services-cross-site-rejected" + ), + ) + .into_response() + }) }