Skip to content

Commit

Permalink
Add support for localized validation errors and hints. Add an example…
Browse files Browse the repository at this point in the history
… for testing localized validation support.
  • Loading branch information
hydra committed Jan 14, 2025
1 parent 737b65c commit dface87
Show file tree
Hide file tree
Showing 8 changed files with 249 additions and 44 deletions.
27 changes: 27 additions & 0 deletions examples/assets/localizations/en-US/validation-localized.ftl
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
window-title = Localized validation example
language-en-us = English (United States)
language-es-es = Spanish (Spain)
#
# Translations specific to the example form
#
form-example-hinted-label = Hinted
form-example-not-hinted-label = Not hinted
# Specific validation reason
form-input-invalid-non-whitespace-required = This field must have at least one non-whitespace character
#
# Translations applicable to all forms
#

# Hints
form-hint-field-required = * Required
# Generic buttons
form-generic-submit-button = Submit
form-generic-reset-button = Reset
# Generic validation reasons
form-generic-invalid-empty = This field cannot be empty.
27 changes: 27 additions & 0 deletions examples/assets/localizations/es-ES/validation-localized.ftl
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
window-title = Ejemplo de validación localizada
language-en-us = Inglés (Estados Unidos)
language-es-es = Español (España)
#
# Traducciones específicas del formulario de ejemplo
#
form-example-hinted-label = Insinuado
form-example-not-hinted-label = No insinuado
# Razón de validación específica
form-input-invalid-non-whitespace-required = Este campo debe tener al menos un carácter que no sea un espacio en blanco
#
# Traducciones aplicables a todos los formularios
#

# Sugerencias
form-hint-field-required = * Requerido
# Botones genéricos
form-generic-submit-button = Enviar
form-generic-reset-button = Restablecer
# Razones genéricas de validación
form-generic-invalid-empty = Este campo no puede estar vacío.
131 changes: 131 additions & 0 deletions examples/validation-localized.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
//! An example that demonstrates localized validation messages and errors.
//!
//! Note how all the form elements, hints and validation error messages use `localize!`
//!
//! Refer to the `localization.rs` example for more details on how localization works in general.
use unic_langid::LanguageIdentifier;
use cushy::figures::units::Lp;
use cushy::value::{Destination, Dynamic, IntoValue, Source, Validations, Value};
use cushy::widget::MakeWidget;
use cushy::widgets::input::InputValue;
use cushy::{localize, MaybeLocalized, Open, PendingApp};
use cushy::localization::Localization;

// This example is based on the `validation.rs` example and should be kept in sync with it.

fn form() -> impl MakeWidget {
let text = Dynamic::default();
let validations = Validations::default();

localize!("form-example-hinted-label")
.and(
text.to_input()
.validation(validations.validate(&text, validate_input))
.hint(localize!("form-hint-field-required")),
)
.and(localize!("form-example-not-hinted-label"))
.and(
text.to_input()
.validation(validations.validate(&text, validate_input)),
)
.and(
localize!("form-generic-submit-button")
.into_button()
.on_click(validations.clone().when_valid(move |_| {

// Note: This is non-localized string is for developers, not users of the UI.
println!(
"Success! This callback only happens when all associated validations are valid"
);
})),
)
.and(localize!("form-generic-reset-button").into_button().on_click(move |_| {
let _value = text.take();
validations.reset();
}))
.into_rows()
.pad()
.width(Lp::inches(6))
.centered()
.make_widget()
}

#[allow(clippy::ptr_arg)] // Changing &String to &str breaks type inference
fn validate_input(input: &String) -> Result<(), Value<MaybeLocalized>> {
if input.is_empty() {
Err(localize!("form-generic-invalid-empty").into_value())
} else if input.trim().is_empty() {
Err(localize!("form-input-invalid-non-whitespace-required").into_value())
} else {
Ok(())
}
}

#[derive(Default, Eq, PartialEq, Debug, Clone, Copy)]
pub enum LanguageChoices {
#[default]
EnUs,
EsEs,
}

impl LanguageChoices {
pub fn to_locale(&self) -> LanguageIdentifier {
match self {
LanguageChoices::EnUs => "en-US".parse().unwrap(),
LanguageChoices::EsEs => "es-ES".parse().unwrap(),
}
}
}

#[cushy::main]
fn main(app: &mut PendingApp) -> cushy::Result {
app.cushy().localizations().add_default(
Localization::for_language(
"en-US",
include_str!("assets/localizations/en-US/validation-localized.ftl"),
)
.expect("valid language id"),
);
app.cushy().localizations().add(
Localization::for_language(
"es-ES",
include_str!("assets/localizations/es-ES/validation-localized.ftl"),
)
.expect("valid language id"),
);

let dynamic_locale: Dynamic<LanguageChoices> = Dynamic::default();

ui(&dynamic_locale)
.localized_in(dynamic_locale.map_each(LanguageChoices::to_locale))
.into_window()
.titled(localize!("window-title"))
.open(app)?;

Ok(())
}

fn language_selector(dynamic_locale: &Dynamic<LanguageChoices>) -> impl MakeWidget {
let dynamic_language_selector = dynamic_locale
.new_radio(LanguageChoices::EnUs)
.labelled_by(localize!("language-en-us"))
.and(
dynamic_locale
.new_radio(LanguageChoices::EsEs)
.labelled_by(localize!("language-es-es")),
)
.into_rows()
.contain();

dynamic_language_selector
.make_widget()
}

fn ui(dynamic_locale: &Dynamic<LanguageChoices>) -> impl MakeWidget {
language_selector(dynamic_locale)
.and(form())
.into_rows()
.centered()
.make_widget()
}
5 changes: 4 additions & 1 deletion examples/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ use cushy::figures::units::Lp;
use cushy::value::{Destination, Dynamic, Validations};
use cushy::widget::MakeWidget;
use cushy::widgets::input::InputValue;
use cushy::Run;
use cushy::{localize, Run};

// For an example of localized validation, see the `validation-localized.rs` example which should
// be kept in sync with this example.

fn main() -> cushy::Result {
let text = Dynamic::default();
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ pub mod localization;
use std::ops::{Add, AddAssign, Sub, SubAssign};

/// A string that may be a localized message.
#[derive(Clone, Debug)]
#[derive(Clone, Debug, PartialEq)]
pub enum MaybeLocalized {
/// A non-localized message.
Text(String),
Expand Down
2 changes: 1 addition & 1 deletion src/localization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ impl DynamicDisplay for MaybeLocalized {
}

/// The primary of defining localized message
#[derive(Clone, Debug)]
#[derive(Clone, Debug, PartialEq)]
pub struct Localize {
key: Cow<'static, str>,
args: Vec<(String, Value<FluentValue<'static>>)>,
Expand Down
Loading

0 comments on commit dface87

Please sign in to comment.