Compare commits
3 commits
ef8d16f41f
...
ac0889cb8c
Author | SHA1 | Date | |
---|---|---|---|
ac0889cb8c | |||
126b31a7ed | |||
c680b94995 |
9 changed files with 221 additions and 138 deletions
|
@ -28,12 +28,15 @@ async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
|||
.with_title(L10n::l("welcome_page"))
|
||||
.with_theme("Basic")
|
||||
.with_assets(AssetsOp::AddStyleSheet(StyleSheet::from("/css/welcome.css")))
|
||||
.with_component(Html::with(move |_| html! {
|
||||
.with_component(Html::with(move |cx| html! {
|
||||
div id="main-header" {
|
||||
header {
|
||||
h1 id="header-title" aria-label=(L10n::l("welcome_aria").with_arg("app", app)) {
|
||||
span { (L10n::l("welcome_title")) }
|
||||
(L10n::l("welcome_intro").with_arg("app", app))
|
||||
h1
|
||||
id="header-title"
|
||||
aria-label=(L10n::l("welcome_aria").with_arg("app", app).to_markup(cx))
|
||||
{
|
||||
span { (L10n::l("welcome_title").to_markup(cx)) }
|
||||
(L10n::l("welcome_intro").with_arg("app", app).to_markup(cx))
|
||||
}
|
||||
}
|
||||
aside id="header-image" aria-hidden="true" {
|
||||
|
@ -59,20 +62,25 @@ async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
|||
main id="main-content" {
|
||||
section class="content-body" {
|
||||
div id="poweredby-button" {
|
||||
a id="poweredby-link" href="https://pagetop.cillero.es" target="_blank" rel="noreferrer" {
|
||||
a
|
||||
id="poweredby-link"
|
||||
href="https://pagetop.cillero.es"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
{
|
||||
span {} span {} span {}
|
||||
div id="poweredby-text" { (L10n::l("welcome_powered")) }
|
||||
div id="poweredby-text" { (L10n::l("welcome_powered").to_markup(cx)) }
|
||||
}
|
||||
}
|
||||
div class="content-text" {
|
||||
p { (L10n::l("welcome_text1")) }
|
||||
p { (L10n::l("welcome_text2")) }
|
||||
p { (L10n::l("welcome_text1").to_markup(cx)) }
|
||||
p { (L10n::l("welcome_text2").to_markup(cx)) }
|
||||
|
||||
div class="subcontent" {
|
||||
h1 { span { (L10n::l("welcome_about")) } }
|
||||
p { (L10n::l("welcome_pagetop")) }
|
||||
p { (L10n::l("welcome_issues1")) }
|
||||
p { (L10n::l("welcome_issues2").with_arg("app", app)) }
|
||||
h1 { span { (L10n::l("welcome_about").to_markup(cx)) } }
|
||||
p { (L10n::l("welcome_pagetop").to_markup(cx)) }
|
||||
p { (L10n::l("welcome_issues1").to_markup(cx)) }
|
||||
p { (L10n::l("welcome_issues2").with_arg("app", app).to_markup(cx)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -85,7 +93,7 @@ async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
|||
viewBox="0 0 1614 1614"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
role="img"
|
||||
aria-label=(L10n::l("pagetop_logo"))
|
||||
aria-label=[L10n::l("pagetop_logo").using(cx)]
|
||||
preserveAspectRatio="xMidYMid slice"
|
||||
focusable="false"
|
||||
{
|
||||
|
@ -97,8 +105,8 @@ async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
|||
div class="footer-links" {
|
||||
a href="https://crates.io/crates/pagetop" target="_blank" rel="noreferrer" { ("Crates.io") }
|
||||
a href="https://docs.rs/pagetop" target="_blank" rel="noreferrer" { ("Docs.rs") }
|
||||
a href="https://git.cillero.es/manuelcillero/pagetop" target="_blank" rel="noreferrer" { (L10n::l("welcome_code")) }
|
||||
em { (L10n::l("welcome_have_fun")) }
|
||||
a href="https://git.cillero.es/manuelcillero/pagetop" target="_blank" rel="noreferrer" { (L10n::l("welcome_code").to_markup(cx)) }
|
||||
em { (L10n::l("welcome_have_fun").to_markup(cx)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -240,7 +240,7 @@ pub static CONFIG_VALUES: LazyLock<ConfigBuilder<DefaultState>> = LazyLock::new(
|
|||
macro_rules! include_config {
|
||||
( $SETTINGS_NAME:ident : $Settings_Type:ty => [ $( $k:literal => $v:expr ),* $(,)? ] ) => {
|
||||
#[doc = concat!(
|
||||
"Referencia a los ajustes de configuración deserializados de [`",
|
||||
"Referencia y valores por defecto de los ajustes de configuración para [`",
|
||||
stringify!($Settings_Type),
|
||||
"`]."
|
||||
)]
|
||||
|
|
|
@ -74,12 +74,12 @@ impl InRegion {
|
|||
/// use pagetop::prelude::*;
|
||||
///
|
||||
/// // Banner global, en la región por defecto de cualquier página.
|
||||
/// InRegion::Content.add(Child::with(Html::with(
|
||||
/// InRegion::Content.add(Child::with(Html::with(|_|
|
||||
/// html! { ("🎉 ¡Bienvenido!") }
|
||||
/// )));
|
||||
///
|
||||
/// // Texto en la región "sidebar".
|
||||
/// InRegion::Named("sidebar").add(Child::with(Html::with(
|
||||
/// InRegion::Named("sidebar").add(Child::with(Html::with(|_|
|
||||
/// html! { ("Publicidad") }
|
||||
/// )));
|
||||
/// ```
|
||||
|
|
|
@ -9,7 +9,7 @@ include_config!(SETTINGS: Settings => [
|
|||
"app.name" => "PageTop App",
|
||||
"app.description" => "Developed with the amazing PageTop framework.",
|
||||
"app.theme" => "Basic",
|
||||
"app.language" => "en-US",
|
||||
"app.language" => "",
|
||||
"app.startup_banner" => "Slant",
|
||||
|
||||
// [dev]
|
||||
|
@ -48,7 +48,14 @@ pub struct App {
|
|||
pub description: String,
|
||||
/// Tema predeterminado.
|
||||
pub theme: String,
|
||||
/// Idioma predeterminado (localización).
|
||||
/// Idioma por defecto para la aplicación.
|
||||
///
|
||||
/// Si no se especifica un valor válido, normalmente se usará el idioma devuelto por la
|
||||
/// implementación de [`LangId`](crate::locale::LangId) para [`Context`](crate::html::Context),
|
||||
/// en el siguiente orden: primero, el idioma establecido explícitamente con
|
||||
/// [`Context::with_langid`](crate::html::Context::with_langid); si no se ha definido, se usará
|
||||
/// el indicado en la cabecera `Accept-Language` del navegador; y, si ninguno aplica, se
|
||||
/// empleará el idioma de respaldo ("en-US").
|
||||
pub language: String,
|
||||
/// Banner ASCII mostrado al inicio: *"Off"* (desactivado), *"Slant"*, *"Small"*, *"Speed"* o
|
||||
/// *"Starwars"*.
|
||||
|
|
|
@ -3,7 +3,7 @@ use crate::core::theme::ThemeRef;
|
|||
use crate::core::TypeInfo;
|
||||
use crate::html::{html, Markup};
|
||||
use crate::html::{Assets, Favicon, JavaScript, StyleSheet};
|
||||
use crate::locale::{LanguageIdentifier, DEFAULT_LANGID};
|
||||
use crate::locale::{LangId, LangMatch, LanguageIdentifier, DEFAULT_LANGID, FALLBACK_LANGID};
|
||||
use crate::service::HttpRequest;
|
||||
use crate::{builder_fn, join};
|
||||
|
||||
|
@ -69,9 +69,9 @@ impl Error for ErrorParam {}
|
|||
/// use pagetop::prelude::*;
|
||||
///
|
||||
/// fn new_context(request: HttpRequest) -> Context {
|
||||
/// Context::new(request)
|
||||
/// Context::new(Some(request))
|
||||
/// // Establece el idioma del documento a español.
|
||||
/// .with_langid(LangMatch::langid_or_default("es-ES"))
|
||||
/// .with_langid(&LangMatch::resolve("es-ES"))
|
||||
/// // Selecciona un tema (por su nombre corto).
|
||||
/// .with_theme("aliner")
|
||||
/// // Asigna un favicon.
|
||||
|
@ -125,9 +125,23 @@ impl Context {
|
|||
/// cargados.
|
||||
#[rustfmt::skip]
|
||||
pub fn new(request: Option<HttpRequest>) -> Self {
|
||||
// Se intenta DEFAULT_LANGID.
|
||||
let langid = DEFAULT_LANGID
|
||||
// Si es None evalúa la cadena de extracción desde la cabecera HTTP.
|
||||
.or_else(|| {
|
||||
request
|
||||
// Se usa `as_ref()` sobre `Option<HttpRequest>` para no mover el valor.
|
||||
.as_ref()
|
||||
.and_then(|req| req.headers().get("Accept-Language"))
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.and_then(|language| LangMatch::resolve(language).as_option())
|
||||
})
|
||||
// Si todo falla, se recurre a &FALLBACK_LANGID.
|
||||
.unwrap_or(&FALLBACK_LANGID);
|
||||
|
||||
Context {
|
||||
request,
|
||||
langid : &DEFAULT_LANGID,
|
||||
langid,
|
||||
theme : *DEFAULT_THEME,
|
||||
layout : "default",
|
||||
favicon : None,
|
||||
|
@ -140,10 +154,10 @@ impl Context {
|
|||
|
||||
// Context BUILDER *****************************************************************************
|
||||
|
||||
/// Modifica el identificador de idioma del documento.
|
||||
/// Modifica la fuente de idioma del documento.
|
||||
#[builder_fn]
|
||||
pub fn with_langid(mut self, langid: &'static LanguageIdentifier) -> Self {
|
||||
self.langid = langid;
|
||||
pub fn with_langid(mut self, language: &impl LangId) -> Self {
|
||||
self.langid = language.langid();
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -202,11 +216,6 @@ impl Context {
|
|||
self.request.as_ref()
|
||||
}
|
||||
|
||||
/// Devuelve el identificador de idioma asociado al documento.
|
||||
pub fn langid(&self) -> &LanguageIdentifier {
|
||||
self.langid
|
||||
}
|
||||
|
||||
/// Devuelve el tema que se usará para renderizar el documento.
|
||||
pub fn theme(&self) -> ThemeRef {
|
||||
self.theme
|
||||
|
@ -281,3 +290,21 @@ impl Context {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Permite a [`Context`](crate::html::Context) actuar como proveedor de idioma.
|
||||
///
|
||||
/// Devuelve un [`LanguageIdentifier`] siguiendo este orden de prioridad:
|
||||
///
|
||||
/// 1. Un idioma válido establecido explícitamente con [`Context::with_langid`].
|
||||
/// 2. El idioma por defecto configurado para la aplicación.
|
||||
/// 3. Un idioma válido extraído de la cabecera `Accept-Language` del navegador.
|
||||
/// 4. Y si ninguna de las opciones anteriores aplica, se usa el idioma de respaldo (`"en-US"`).
|
||||
///
|
||||
/// Resulta útil para usar un contexto ([`Context`]) como fuente de traducción en
|
||||
/// [`L10n::using`](crate::locale::L10n::using) o
|
||||
/// [`L10n::to_markup`](crate::locale::L10n::to_markup).
|
||||
impl LangId for Context {
|
||||
fn langid(&self) -> &'static LanguageIdentifier {
|
||||
self.langid
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::html::Markup;
|
||||
use crate::locale::{L10n, LanguageIdentifier};
|
||||
use crate::locale::{L10n, LangId};
|
||||
use crate::{builder_fn, AutoDefault};
|
||||
|
||||
/// Cadena para traducir al renderizar ([`locale`](crate::locale)).
|
||||
|
@ -16,18 +16,18 @@ use crate::{builder_fn, AutoDefault};
|
|||
///
|
||||
/// // Español disponible.
|
||||
/// assert_eq!(
|
||||
/// hello.using(LangMatch::langid_or_default("es-ES")),
|
||||
/// hello.using(&LangMatch::resolve("es-ES")),
|
||||
/// Some(String::from("¡Hola mundo!"))
|
||||
/// );
|
||||
///
|
||||
/// // Japonés no disponible, traduce al idioma de respaldo ("en-US").
|
||||
/// assert_eq!(
|
||||
/// hello.using(LangMatch::langid_or_fallback("ja-JP")),
|
||||
/// hello.using(&LangMatch::resolve("ja-JP")),
|
||||
/// Some(String::from("Hello world!"))
|
||||
/// );
|
||||
///
|
||||
/// // Para incrustar en HTML escapado:
|
||||
/// let markup = hello.escaped(LangMatch::langid_or_default("es-ES"));
|
||||
/// let markup = hello.to_markup(&LangMatch::resolve("es-ES"));
|
||||
/// assert_eq!(markup.into_string(), "¡Hola mundo!");
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Debug)]
|
||||
|
@ -50,16 +50,16 @@ impl OptionTranslated {
|
|||
|
||||
// OptionTranslated GETTERS ********************************************************************
|
||||
|
||||
/// Devuelve la traducción para `langid`, si existe.
|
||||
pub fn using(&self, langid: &LanguageIdentifier) -> Option<String> {
|
||||
self.0.using(langid)
|
||||
/// Devuelve la traducción para `language`, si existe.
|
||||
pub fn using(&self, language: &impl LangId) -> Option<String> {
|
||||
self.0.using(language)
|
||||
}
|
||||
|
||||
/// Devuelve la traducción *escapada* como [`Markup`] para `langid`, si existe.
|
||||
/// Devuelve la traducción *escapada* como [`Markup`] para `language`, si existe.
|
||||
///
|
||||
/// Útil para incrustar el texto directamente en plantillas HTML sin riesgo de inyección de
|
||||
/// contenido.
|
||||
pub fn escaped(&self, langid: &LanguageIdentifier) -> Markup {
|
||||
self.0.escaped(langid)
|
||||
pub fn to_markup(&self, language: &impl LangId) -> Markup {
|
||||
self.0.to_markup(language)
|
||||
}
|
||||
}
|
||||
|
|
207
src/locale.rs
207
src/locale.rs
|
@ -90,7 +90,7 @@
|
|||
//! Y *voilà*, sólo queda operar con los idiomas soportados por `PageTop` usando [`LangMatch`] y
|
||||
//! traducir textos con [`L10n`].
|
||||
|
||||
use crate::html::{Markup, PreEscaped, Render};
|
||||
use crate::html::{Markup, PreEscaped};
|
||||
use crate::{global, hm, AutoDefault};
|
||||
|
||||
pub use fluent_templates;
|
||||
|
@ -112,10 +112,10 @@ use std::fmt;
|
|||
static LANGUAGES: LazyLock<HashMap<&str, (LanguageIdentifier, &str)>> = LazyLock::new(|| {
|
||||
hm![
|
||||
"en" => ( langid!("en-US"), "english" ),
|
||||
"en-GB" => ( langid!("en-GB"), "english_british" ),
|
||||
"en-US" => ( langid!("en-US"), "english_united_states" ),
|
||||
"en-gb" => ( langid!("en-GB"), "english_british" ),
|
||||
"en-us" => ( langid!("en-US"), "english_united_states" ),
|
||||
"es" => ( langid!("es-ES"), "spanish" ),
|
||||
"es-ES" => ( langid!("es-ES"), "spanish_spain" ),
|
||||
"es-es" => ( langid!("es-ES"), "spanish_spain" ),
|
||||
]
|
||||
});
|
||||
|
||||
|
@ -123,14 +123,23 @@ static LANGUAGES: LazyLock<HashMap<&str, (LanguageIdentifier, &str)>> = LazyLock
|
|||
//
|
||||
// Se usa cuando el valor del identificador de idioma en las traducciones no corresponde con ningún
|
||||
// idioma soportado por la aplicación.
|
||||
static FALLBACK_LANGID: LazyLock<LanguageIdentifier> = LazyLock::new(|| langid!("en-US"));
|
||||
pub(crate) static FALLBACK_LANGID: LazyLock<LanguageIdentifier> =
|
||||
LazyLock::new(|| langid!("en-US"));
|
||||
|
||||
// Identificador de idioma **por defecto** para la aplicación.
|
||||
//
|
||||
// Se resuelve a partir de [`global::SETTINGS.app.language`](global::SETTINGS). Si el identificador
|
||||
// de idioma no es válido o no está disponible entonces resuelve como [`FALLBACK_LANGID`].
|
||||
pub(crate) static DEFAULT_LANGID: LazyLock<&LanguageIdentifier> =
|
||||
LazyLock::new(|| LangMatch::langid_or_fallback(&global::SETTINGS.app.language));
|
||||
pub(crate) static DEFAULT_LANGID: LazyLock<Option<&LanguageIdentifier>> =
|
||||
LazyLock::new(|| LangMatch::resolve(&global::SETTINGS.app.language).as_option());
|
||||
|
||||
/// Representa la fuente de idioma (`LanguageIdentifier`) asociada a un recurso.
|
||||
///
|
||||
/// Este *trait* permite que distintas estructuras expongan su fuente de idioma de forma uniforme.
|
||||
pub trait LangId {
|
||||
/// Devuelve el identificador de idioma asociado al recurso.
|
||||
fn langid(&self) -> &'static LanguageIdentifier;
|
||||
}
|
||||
|
||||
/// Operaciones con los idiomas soportados por `PageTop`.
|
||||
///
|
||||
|
@ -144,11 +153,11 @@ pub(crate) static DEFAULT_LANGID: LazyLock<&LanguageIdentifier> =
|
|||
///
|
||||
/// // Coincidencia exacta.
|
||||
/// let lang = LangMatch::resolve("es-ES");
|
||||
/// assert_eq!(lang.as_langid().to_string(), "es-ES");
|
||||
/// assert_eq!(lang.langid().to_string(), "es-ES");
|
||||
///
|
||||
/// // Coincidencia parcial (con el idioma base).
|
||||
/// let lang = LangMatch::resolve("es-EC");
|
||||
/// assert_eq!(lang.as_langid().to_string(), "es-ES"); // Porque "es-EC" no está soportado.
|
||||
/// assert_eq!(lang.langid().to_string(), "es-ES"); // Porque "es-EC" no está soportado.
|
||||
///
|
||||
/// // Idioma no especificado.
|
||||
/// let lang = LangMatch::resolve("");
|
||||
|
@ -159,21 +168,16 @@ pub(crate) static DEFAULT_LANGID: LazyLock<&LanguageIdentifier> =
|
|||
/// assert_eq!(lang, LangMatch::Unsupported(String::from("ja-JP")));
|
||||
/// ```
|
||||
///
|
||||
/// Las siguientes líneas devuelven siempre un [`LanguageIdentifier`] válido, ya sea porque
|
||||
/// resuelven un idioma soportado o porque aplican el idioma por defecto o de respaldo:
|
||||
/// Con la siguiente instrucción siempre se obtiene un [`LanguageIdentifier`] válido, ya sea porque
|
||||
/// resuelve un idioma soportado o porque se aplica el idioma por defecto o, en último caso, el de
|
||||
/// respaldo ("en-US"):
|
||||
///
|
||||
/// ```rust
|
||||
/// use pagetop::prelude::*;
|
||||
///
|
||||
/// // Idioma por defecto si no resuelve.
|
||||
/// // Idioma por defecto o de respaldo si no resuelve.
|
||||
/// let lang = LangMatch::resolve("it-IT");
|
||||
/// let langid = lang.as_langid();
|
||||
///
|
||||
/// // Idioma por defecto si no se encuentra.
|
||||
/// let langid = LangMatch::langid_or_default("es-MX");
|
||||
///
|
||||
/// // Idioma de respaldo ("en-US") si no se encuentra.
|
||||
/// let langid = LangMatch::langid_or_fallback("es-MX");
|
||||
/// let langid = lang.langid();
|
||||
/// ```
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum LangMatch {
|
||||
|
@ -187,6 +191,13 @@ pub enum LangMatch {
|
|||
Unsupported(String),
|
||||
}
|
||||
|
||||
impl Default for LangMatch {
|
||||
/// Resuelve al idioma por defecto y, si no está disponible, al idioma de respaldo ("en-US").
|
||||
fn default() -> Self {
|
||||
LangMatch::Found(DEFAULT_LANGID.unwrap_or(&FALLBACK_LANGID))
|
||||
}
|
||||
}
|
||||
|
||||
impl LangMatch {
|
||||
/// Resuelve `language` y devuelve la variante [`LangMatch`] apropiada.
|
||||
pub fn resolve(language: impl AsRef<str>) -> Self {
|
||||
|
@ -198,12 +209,13 @@ impl LangMatch {
|
|||
}
|
||||
|
||||
// Intenta aplicar coincidencia exacta con el código completo (p.ej. "es-MX").
|
||||
if let Some(langid) = LANGUAGES.get(language).map(|(langid, _)| langid) {
|
||||
let lang = language.to_ascii_lowercase();
|
||||
if let Some(langid) = LANGUAGES.get(lang.as_str()).map(|(langid, _)| langid) {
|
||||
return Self::Found(langid);
|
||||
}
|
||||
|
||||
// Si la variante regional no existe, retrocede al idioma base (p.ej. "es").
|
||||
if let Some((base_lang, _)) = language.split_once('-') {
|
||||
if let Some((base_lang, _)) = lang.split_once('-') {
|
||||
if let Some(langid) = LANGUAGES.get(base_lang).map(|(langid, _)| langid) {
|
||||
return Self::Found(langid);
|
||||
}
|
||||
|
@ -213,37 +225,47 @@ impl LangMatch {
|
|||
Self::Unsupported(String::from(language))
|
||||
}
|
||||
|
||||
/// Devuelve el idioma de la variante de la instancia, o el idioma por defecto si no está
|
||||
/// soportado.
|
||||
/// Devuelve el [`LanguageIdentifier`] si el idioma fue reconocido.
|
||||
///
|
||||
/// Siempre devuelve un [`LanguageIdentifier`] válido.
|
||||
/// Solo retorna `Some` si la variante es [`LangMatch::Found`]. En cualquier otro caso (por
|
||||
/// ejemplo, si el identificador es vacío o no está soportado), devuelve `None`.
|
||||
///
|
||||
/// Este método es útil cuando se desea acceder directamente al idioma reconocido sin aplicar el
|
||||
/// idioma por defecto ni el de respaldo.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// use pagetop::prelude::*;
|
||||
///
|
||||
/// let lang = LangMatch::resolve("es-ES").as_option();
|
||||
/// assert_eq!(lang.unwrap().to_string(), "es-ES");
|
||||
///
|
||||
/// let lang = LangMatch::resolve("jp-JP").as_option();
|
||||
/// assert!(lang.is_none());
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn as_langid(&self) -> &'static LanguageIdentifier {
|
||||
pub fn as_option(&self) -> Option<&'static LanguageIdentifier> {
|
||||
match self {
|
||||
LangMatch::Found(l) => Some(l),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Permite a [`LangMatch`] actuar como proveedor de idioma.
|
||||
///
|
||||
/// Devuelve el [`LanguageIdentifier`] si la variante es [`LangMatch::Found`]; en caso contrario,
|
||||
/// devuelve el idioma por defecto de la aplicación y, si tampoco está disponible, el idioma de
|
||||
/// respaldo ("en-US").
|
||||
///
|
||||
/// Resulta útil para usar un valor de [`LangMatch`] como fuente de traducción en [`L10n::using`] o
|
||||
/// [`L10n::to_markup`].
|
||||
impl LangId for LangMatch {
|
||||
fn langid(&self) -> &'static LanguageIdentifier {
|
||||
match self {
|
||||
LangMatch::Found(l) => l,
|
||||
_ => &DEFAULT_LANGID,
|
||||
}
|
||||
}
|
||||
|
||||
/// Si `language` está vacío o no está soportado, devuelve el idioma por defecto.
|
||||
///
|
||||
/// Siempre devuelve un [`LanguageIdentifier`] válido.
|
||||
#[inline]
|
||||
pub fn langid_or_default(language: impl AsRef<str>) -> &'static LanguageIdentifier {
|
||||
match Self::resolve(language) {
|
||||
Self::Found(l) => l,
|
||||
_ => &DEFAULT_LANGID,
|
||||
}
|
||||
}
|
||||
|
||||
/// Si `language` está vacío o no está soportado, devuelve el idioma de respaldo ("en-US").
|
||||
///
|
||||
/// Siempre devuelve un [`LanguageIdentifier`] válido.
|
||||
#[inline]
|
||||
pub fn langid_or_fallback(language: impl AsRef<str>) -> &'static LanguageIdentifier {
|
||||
match Self::resolve(language) {
|
||||
Self::Found(l) => l,
|
||||
_ => &FALLBACK_LANGID,
|
||||
_ => DEFAULT_LANGID.unwrap_or(&FALLBACK_LANGID),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -297,7 +319,7 @@ enum L10nOp {
|
|||
/// Cada instancia puede representar:
|
||||
///
|
||||
/// - Un texto puro (`n()`) que no requiere traducción.
|
||||
/// - Una clave para traducir un texto de las traducciones por defecto de `PageTop` (`l()`).
|
||||
/// - Una clave para traducir un texto de las traducciones predefinidas de `PageTop` (`l()`).
|
||||
/// - Una clave para traducir de un conjunto concreto de traducciones (`t()`).
|
||||
///
|
||||
/// # Ejemplo
|
||||
|
@ -319,8 +341,8 @@ enum L10nOp {
|
|||
/// También para traducciones a idiomas concretos.
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// // Traducción con clave, conjunto de traducciones e identificador de idioma a usar.
|
||||
/// let bye = L10n::t("goodbye", &LOCALES_CUSTOM).using(LangMatch::langid_or_default("it"));
|
||||
/// // Traducción con clave, conjunto de traducciones y fuente de idioma.
|
||||
/// let bye = L10n::t("goodbye", &LOCALES_CUSTOM).using(&LangMatch::resolve("it"));
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone)]
|
||||
pub struct L10n {
|
||||
|
@ -339,8 +361,8 @@ impl L10n {
|
|||
}
|
||||
}
|
||||
|
||||
/// **l** = *“lookup”*. Crea una instancia para traducir usando una clave de la tabla de
|
||||
/// traducciones por defecto.
|
||||
/// **l** = *“lookup”*. Crea una instancia para traducir usando una clave del conjunto de
|
||||
/// traducciones predefinidas.
|
||||
pub fn l(key: impl Into<String>) -> Self {
|
||||
L10n {
|
||||
op: L10nOp::Translate(key.into()),
|
||||
|
@ -348,8 +370,8 @@ impl L10n {
|
|||
}
|
||||
}
|
||||
|
||||
/// **t** = *“translate”*. Crea una instancia para traducir usando una clave de una tabla de
|
||||
/// traducciones específica.
|
||||
/// **t** = *“translate”*. Crea una instancia para traducir usando una clave de un conjunto de
|
||||
/// traducciones específico.
|
||||
pub fn t(key: impl Into<String>, locales: &'static Locales) -> Self {
|
||||
L10n {
|
||||
op: L10nOp::Translate(key.into()),
|
||||
|
@ -377,20 +399,47 @@ impl L10n {
|
|||
self
|
||||
}
|
||||
|
||||
/// Resuelve la traducción usando el idioma por defecto de la aplicación. Devuelve `None` si no
|
||||
/// aplica o no encuentra una traducción.
|
||||
/// Resuelve la traducción usando el idioma por defecto o de respaldo de la aplicación.
|
||||
///
|
||||
/// Devuelve `None` si no aplica o no encuentra una traducción válida.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// use pagetop::prelude::*;
|
||||
///
|
||||
/// let text = L10n::l("greeting").with_arg("name", "Manuel").get();
|
||||
/// ```
|
||||
pub fn get(&self) -> Option<String> {
|
||||
self.using(&DEFAULT_LANGID)
|
||||
self.using(&LangMatch::default())
|
||||
}
|
||||
|
||||
/// Resuelve la traducción usando el [`LanguageIdentifier`] indicado. Devuelve `None` si no
|
||||
/// aplica o no encuentra una traducción.
|
||||
pub fn using(&self, langid: &LanguageIdentifier) -> Option<String> {
|
||||
/// Resuelve la traducción usando la fuente de idioma proporcionada.
|
||||
///
|
||||
/// Devuelve `None` si no aplica o no encuentra una traducción válida.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// use pagetop::prelude::*;
|
||||
///
|
||||
/// struct ResourceLang;
|
||||
///
|
||||
/// impl LangId for ResourceLang {
|
||||
/// fn langid(&self) -> &'static LanguageIdentifier {
|
||||
/// LangMatch::resolve("es-MX").langid()
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// let r = ResourceLang;
|
||||
/// let text = L10n::l("greeting").with_arg("name", "Usuario").using(&r);
|
||||
/// ```
|
||||
pub fn using(&self, language: &impl LangId) -> Option<String> {
|
||||
match &self.op {
|
||||
L10nOp::None => None,
|
||||
L10nOp::Text(text) => Some(text.to_owned()),
|
||||
L10nOp::Translate(key) => self.locales.try_lookup_with_args(
|
||||
langid,
|
||||
language.langid(),
|
||||
key,
|
||||
&self.args.iter().fold(HashMap::new(), |mut arg, (k, v)| {
|
||||
arg.insert(Cow::Owned(k.clone()), v.to_owned().into());
|
||||
|
@ -400,16 +449,19 @@ impl L10n {
|
|||
}
|
||||
}
|
||||
|
||||
/// Traduce y escapa con el [`LanguageIdentifier`] indicado, devolviendo [`Markup`].
|
||||
pub fn escaped(&self, langid: &LanguageIdentifier) -> Markup {
|
||||
PreEscaped(self.using(langid).unwrap_or_default())
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for L10n {
|
||||
/// Traduce y escapa con el idioma por defecto, devolviendo [`Markup`].
|
||||
fn render(&self) -> Markup {
|
||||
PreEscaped(self.get().unwrap_or_default())
|
||||
/// Traduce el texto y lo devuelve como [`Markup`] usando la fuente de idioma proporcionada.
|
||||
///
|
||||
/// Si no se encuentra una traducción válida, devuelve una cadena vacía.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// use pagetop::prelude::*;
|
||||
///
|
||||
/// let html = L10n::l("welcome.message").to_markup(&LangMatch::resolve("es"));
|
||||
/// ```
|
||||
pub fn to_markup(&self, language: &impl LangId) -> Markup {
|
||||
PreEscaped(self.using(language).unwrap_or_default())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -423,14 +475,3 @@ impl fmt::Debug for L10n {
|
|||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for L10n {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let content = match &self.op {
|
||||
L10nOp::None => String::new(),
|
||||
L10nOp::Text(text) => text.clone(),
|
||||
L10nOp::Translate(key) => self.get().unwrap_or_else(|| format!("??<{}>", key)),
|
||||
};
|
||||
write!(f, "{content}")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ use crate::core::component::{Child, ChildOp, ComponentTrait};
|
|||
use crate::core::theme::{ChildrenInRegions, ThemeRef, CONTENT_REGION_NAME};
|
||||
use crate::html::{html, AssetsOp, Context, Markup, DOCTYPE};
|
||||
use crate::html::{ClassesOp, OptionClasses, OptionId, OptionTranslated};
|
||||
use crate::locale::{CharacterDirection, L10n, LanguageIdentifier};
|
||||
use crate::locale::{CharacterDirection, L10n, LangId, LanguageIdentifier};
|
||||
use crate::service::HttpRequest;
|
||||
|
||||
/// Representa una página HTML completa lista para renderizar.
|
||||
|
@ -78,10 +78,10 @@ impl Page {
|
|||
self
|
||||
}
|
||||
|
||||
/// Modifica el identificador de idioma de la página ([`Context::with_langid`]).
|
||||
/// Modifica la fuente de idioma de la página ([`Context::with_langid`]).
|
||||
#[builder_fn]
|
||||
pub fn with_langid(mut self, langid: &'static LanguageIdentifier) -> Self {
|
||||
self.context.alter_langid(langid);
|
||||
pub fn with_langid(mut self, language: &impl LangId) -> Self {
|
||||
self.context.alter_langid(language);
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -147,14 +147,14 @@ impl Page {
|
|||
|
||||
// Page GETTERS ********************************************************************************
|
||||
|
||||
/// Devuelve el título traducido para el idioma activo, si existe.
|
||||
/// Devuelve el título traducido para el idioma de la página, si existe.
|
||||
pub fn title(&mut self) -> Option<String> {
|
||||
self.title.using(self.context.langid())
|
||||
self.title.using(&self.context)
|
||||
}
|
||||
|
||||
/// Devuelve la descripción traducida para el idioma activo, si existe.
|
||||
/// Devuelve la descripción traducida para el idioma de la página, si existe.
|
||||
pub fn description(&mut self) -> Option<String> {
|
||||
self.description.using(self.context.langid())
|
||||
self.description.using(&self.context)
|
||||
}
|
||||
|
||||
/// Devuelve la lista de metadatos `<meta name=...>`.
|
||||
|
|
|
@ -13,7 +13,7 @@ async fn translation_without_args() {
|
|||
let _app = service::test::init_service(Application::new().test()).await;
|
||||
|
||||
let l10n = L10n::l("test-hello-world");
|
||||
let translation = l10n.using(LangMatch::langid_or_default("es-ES"));
|
||||
let translation = l10n.using(&LangMatch::resolve("es-ES"));
|
||||
assert_eq!(translation, Some("¡Hola mundo!".to_string()));
|
||||
}
|
||||
|
||||
|
@ -22,7 +22,7 @@ async fn translation_with_args() {
|
|||
let _app = service::test::init_service(Application::new().test()).await;
|
||||
|
||||
let l10n = L10n::l("test-hello-user").with_arg("userName", "Manuel");
|
||||
let translation = l10n.using(LangMatch::langid_or_default("es-ES"));
|
||||
let translation = l10n.using(&LangMatch::resolve("es-ES"));
|
||||
assert_eq!(translation, Some("¡Hola, Manuel!".to_string()));
|
||||
}
|
||||
|
||||
|
@ -35,7 +35,7 @@ async fn translation_with_plural_and_select() {
|
|||
("photoCount", "3"),
|
||||
("userGender", "male"),
|
||||
]);
|
||||
let translation = l10n.using(LangMatch::langid_or_default("es-ES")).unwrap();
|
||||
let translation = l10n.using(&LangMatch::resolve("es-ES")).unwrap();
|
||||
assert!(translation.contains("añadido 3 nuevas fotos de él"));
|
||||
}
|
||||
|
||||
|
@ -44,7 +44,7 @@ async fn check_fallback_language() {
|
|||
let _app = service::test::init_service(Application::new().test()).await;
|
||||
|
||||
let l10n = L10n::l("test-hello-world");
|
||||
let translation = l10n.using(LangMatch::langid_or_fallback("xx-YY")); // Retrocede a "en-US".
|
||||
let translation = l10n.using(&LangMatch::resolve("xx-YY")); // Retrocede a "en-US".
|
||||
assert_eq!(translation, Some("Hello world!".to_string()));
|
||||
}
|
||||
|
||||
|
@ -53,6 +53,6 @@ async fn check_unknown_key() {
|
|||
let _app = service::test::init_service(Application::new().test()).await;
|
||||
|
||||
let l10n = L10n::l("non-existent-key");
|
||||
let translation = l10n.using(LangMatch::langid_or_default("en-US"));
|
||||
let translation = l10n.using(&LangMatch::resolve("en-US"));
|
||||
assert_eq!(translation, None);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue