i18n on the web - deepseek translations2

This commit is contained in:
Priec
2026-08-14 20:52:31 +02:00
parent 8f1bfdbe0c
commit 967335d4ca
16 changed files with 110 additions and 33 deletions

View File

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

View File

@@ -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());
}
}

View File

@@ -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""#));

View File

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

View File

@@ -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

View File

@@ -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"]
);
}

View File

@@ -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""#));

View File

@@ -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]);
}
}

View File

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

View File

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

View File

@@ -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");

View File

@@ -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"
);
}

View File

@@ -33,5 +33,14 @@ pub(crate) fn reject_cross_site(headers: &HeaderMap) -> Option<Response> {
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()
})
}