🐛 (locale): Preserva grafía original de "?lang="
Antes, RequestLocale sólo guardaba el LanguageIdentifier ya resuelto,
así que `Context::route()` propagaba siempre la forma canónica en los
enlaces generados ("?lang=en-US"), aunque el usuario hubiera pedido
"?lang=en". Ahora se conserva el valor original de la query junto al
idioma resuelto, y se reutiliza al construir nuevos enlaces mientras
siga siendo válido.
El idioma efectivo para renderizar la petición no cambia: sigue siendo
siempre el LanguageIdentifier canónico soportado por la aplicación.
This commit is contained in:
parent
fdd2dca280
commit
3e83f8e35e
2 changed files with 81 additions and 51 deletions
|
|
@ -455,7 +455,7 @@ impl Context {
|
||||||
pub fn route(&self, path: impl Into<RoutePath>) -> RoutePath {
|
pub fn route(&self, path: impl Into<RoutePath>) -> RoutePath {
|
||||||
let mut route = path.into();
|
let mut route = path.into();
|
||||||
if !route.is_external() && self.locale.needs_lang_query() {
|
if !route.is_external() && self.locale.needs_lang_query() {
|
||||||
route.alter_param("lang", self.locale.langid().to_string());
|
route.alter_param("lang", self.locale.lang_query_value());
|
||||||
}
|
}
|
||||||
route
|
route
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,45 +7,53 @@ use super::{LangId, LanguageIdentifier, Locale};
|
||||||
///
|
///
|
||||||
/// Determina qué idioma se usará para renderizar la respuesta asociada a una petición. También
|
/// Determina qué idioma se usará para renderizar la respuesta asociada a una petición. También
|
||||||
/// indica si es necesario propagar ese idioma en los enlaces usando el parámetro de *query*
|
/// indica si es necesario propagar ese idioma en los enlaces usando el parámetro de *query*
|
||||||
/// `?lang=...`. El comportamiento concreto depende de la política global
|
/// `?lang=...`. El comportamiento concreto depende de la política global [`LangNegotiation`]
|
||||||
/// [`LangNegotiation`](crate::global::LangNegotiation) configurada en la aplicación.
|
/// configurada en la aplicación.
|
||||||
///
|
///
|
||||||
/// El idioma resultante se expone a través del *trait* [`LangId`], de modo que pueda usarse
|
/// El idioma resultante se expone a través del *trait* [`LangId`], de modo que pueda usarse
|
||||||
/// [`RequestLocale`] como cualquier otra fuente de idioma en PageTop.
|
/// [`RequestLocale`] como cualquier otra fuente de idioma en PageTop.
|
||||||
|
///
|
||||||
|
/// [`LangNegotiation`]: crate::global::LangNegotiation
|
||||||
pub struct RequestLocale {
|
pub struct RequestLocale {
|
||||||
// Idioma elegido por la aplicación para esta petición, combinando la configuración, la cabecera
|
// Idioma elegido por la aplicación para esta petición, combinando la configuración, la cabecera
|
||||||
// `Accept-Language` y/o el idioma de respaldo.
|
// `Accept-Language` y/o el idioma de respaldo.
|
||||||
base: &'static LanguageIdentifier,
|
base: &'static LanguageIdentifier,
|
||||||
// Idioma finalmente aplicado a la petición (puede coincidir con `base` o no).
|
// Idioma finalmente aplicado a la petición (puede coincidir con `base` o no).
|
||||||
effective: &'static LanguageIdentifier,
|
effective: &'static LanguageIdentifier,
|
||||||
|
// Valor original de `?lang=...`, tal como llegó en la petición, si resultó en un idioma
|
||||||
|
// soportado. Permite propagarlo en los enlaces con su grafía original (p. ej. "en") en vez de
|
||||||
|
// expandirlo siempre a la forma canónica del idioma resuelto (p. ej. "en-US").
|
||||||
|
query_lang: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RequestLocale {
|
impl RequestLocale {
|
||||||
/// Construye un `RequestLocale` a partir de una petición HTTP.
|
/// Construye un `RequestLocale` a partir de una petición HTTP.
|
||||||
///
|
///
|
||||||
/// El idioma de la petición se decide según la estrategia definida por
|
/// El idioma de la petición se decide según la estrategia definida por [`LangNegotiation`]:
|
||||||
/// [`LangNegotiation`](crate::global::LangNegotiation):
|
|
||||||
///
|
///
|
||||||
/// - [`LangNegotiation::Full`](crate::global::LangNegotiation::Full) determina el idioma en
|
/// - [`LangNegotiation::Full`] determina el idioma en este orden:
|
||||||
/// este orden:
|
|
||||||
/// 1. Parámetro de *query* `?lang=...`, si existe y corresponde a un idioma soportado.
|
/// 1. Parámetro de *query* `?lang=...`, si existe y corresponde a un idioma soportado.
|
||||||
/// 2. [`Locale::try_langid()`], si la aplicación tiene un idioma por defecto válido.
|
/// 2. [`Locale::try_langid()`], si la aplicación tiene un idioma por defecto válido.
|
||||||
/// 3. Cabecera `Accept-Language`, si puede resolverse con [`Locale::resolve()`].
|
/// 3. Cabecera `Accept-Language`, si puede resolverse con [`Locale::resolve()`].
|
||||||
/// 4. Idioma de respaldo.
|
/// 4. Idioma de respaldo.
|
||||||
///
|
///
|
||||||
/// - [`LangNegotiation::NoQuery`](crate::global::LangNegotiation::NoQuery) descarta el uso del
|
/// - [`LangNegotiation::NoQuery`] descarta el uso del parámetro `?lang=...` y determina el
|
||||||
/// parámetro `?lang=...` y determina el idioma en este orden:
|
/// idioma en este orden:
|
||||||
/// 1. [`Locale::try_langid()`], si la aplicación tiene un idioma por defecto válido.
|
/// 1. [`Locale::try_langid()`], si la aplicación tiene un idioma por defecto válido.
|
||||||
/// 2. Cabecera `Accept-Language`, si puede resolverse con [`Locale::resolve()`].
|
/// 2. Cabecera `Accept-Language`, si puede resolverse con [`Locale::resolve()`].
|
||||||
/// 3. Idioma de respaldo.
|
/// 3. Idioma de respaldo.
|
||||||
///
|
///
|
||||||
/// - [`LangNegotiation::ConfigOnly`](crate::global::LangNegotiation::ConfigOnly) sólo usa la
|
/// - [`LangNegotiation::ConfigOnly`] sólo usa la configuración de la aplicación mediante
|
||||||
/// configuración de la aplicación mediante [`Locale::default_langid()`], sin consultar la
|
/// [`Locale::default_langid()`], sin consultar la cabecera `Accept-Language` ni el parámetro
|
||||||
/// cabecera `Accept-Language` ni el parámetro `?lang`. Este modo también aplica el idioma de
|
/// `?lang`. Este modo también aplica el idioma de respaldo si es necesario.
|
||||||
/// respaldo si es necesario.
|
|
||||||
///
|
///
|
||||||
/// En todos los casos, el idioma resultante es siempre un [`LanguageIdentifier`] soportado por
|
/// En todos los casos, el idioma resultante es siempre un [`LanguageIdentifier`] soportado por
|
||||||
/// la aplicación y será el que PageTop utilice para renderizar la respuesta de la petición.
|
/// la aplicación y será el que PageTop utilice para renderizar la respuesta de la petición.
|
||||||
|
///
|
||||||
|
/// [`LangNegotiation`]: crate::global::LangNegotiation
|
||||||
|
/// [`LangNegotiation::Full`]: crate::global::LangNegotiation::Full
|
||||||
|
/// [`LangNegotiation::NoQuery`]: crate::global::LangNegotiation::NoQuery
|
||||||
|
/// [`LangNegotiation::ConfigOnly`]: crate::global::LangNegotiation::ConfigOnly
|
||||||
pub fn from_request(request: Option<&HttpRequest>) -> Self {
|
pub fn from_request(request: Option<&HttpRequest>) -> Self {
|
||||||
let mode = global::SETTINGS.app.lang_negotiation;
|
let mode = global::SETTINGS.app.lang_negotiation;
|
||||||
|
|
||||||
|
|
@ -98,14 +106,14 @@ impl RequestLocale {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Idioma aplicado a la petición tras considerar la *query* `?lang=...`.
|
// Valor de `?lang=...`, si existe y corresponde a un idioma soportado. Se conserva junto al
|
||||||
let effective: &'static LanguageIdentifier = match mode {
|
// `LanguageIdentifier` resuelto para poder propagar más tarde su grafía original.
|
||||||
|
let query_lang: Option<(&'static LanguageIdentifier, String)> = match mode {
|
||||||
global::LangNegotiation::ConfigOnly | global::LangNegotiation::NoQuery => {
|
global::LangNegotiation::ConfigOnly | global::LangNegotiation::NoQuery => {
|
||||||
// En estos modos no se permite que la URL modifique el idioma.
|
// En estos modos no se permite que la URL modifique el idioma.
|
||||||
base
|
None
|
||||||
}
|
}
|
||||||
global::LangNegotiation::Full => {
|
global::LangNegotiation::Full => request
|
||||||
request
|
|
||||||
// Se obtiene el valor de `lang` de la petición, si existe.
|
// Se obtiene el valor de `lang` de la petición, si existe.
|
||||||
.and_then(|req| {
|
.and_then(|req| {
|
||||||
req.query_string().split('&').find_map(|pair| {
|
req.query_string().split('&').find_map(|pair| {
|
||||||
|
|
@ -119,17 +127,23 @@ impl RequestLocale {
|
||||||
// Se comprueba si es un idioma soportado.
|
// Se comprueba si es un idioma soportado.
|
||||||
.and_then(|language| {
|
.and_then(|language| {
|
||||||
if let Locale::Resolved(langid) = Locale::resolve(language) {
|
if let Locale::Resolved(langid) = Locale::resolve(language) {
|
||||||
Some(langid)
|
Some((langid, language.to_string()))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
// Si no hay `lang` o no es válido, se usa `base`.
|
|
||||||
.unwrap_or(base)
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
RequestLocale { base, effective }
|
// Idioma aplicado a la petición tras considerar la *query* `?lang=...`. Si no hay `lang` o
|
||||||
|
// no es válido, se usa `base`.
|
||||||
|
let effective: &'static LanguageIdentifier =
|
||||||
|
query_lang.as_ref().map_or(base, |(langid, _)| *langid);
|
||||||
|
|
||||||
|
RequestLocale {
|
||||||
|
base,
|
||||||
|
effective,
|
||||||
|
query_lang: query_lang.map(|(_, raw)| raw),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fuerza el idioma que se utilizará para las traducciones de esta petición.
|
/// Fuerza el idioma que se utilizará para las traducciones de esta petición.
|
||||||
|
|
@ -140,26 +154,29 @@ impl RequestLocale {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn with_langid(&mut self, language: &impl LangId) -> &mut Self {
|
pub fn with_langid(&mut self, language: &impl LangId) -> &mut Self {
|
||||||
self.effective = language.langid();
|
self.effective = language.langid();
|
||||||
|
self.query_lang = None;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Indica si conviene propagar `lang=...` en los enlaces generados.
|
/// Indica si conviene propagar `lang=...` en los enlaces generados.
|
||||||
///
|
///
|
||||||
/// El comportamiento depende de la estrategia configurada en
|
/// El comportamiento depende de la estrategia configurada en [`LangNegotiation`]:
|
||||||
/// [`LangNegotiation`](crate::global::LangNegotiation):
|
|
||||||
///
|
///
|
||||||
/// - En modo [`LangNegotiation::Full`](crate::global::LangNegotiation::Full) devuelve `true`
|
/// - En modo [`LangNegotiation::Full`] devuelve `true` cuando la respuesta se está generando en
|
||||||
/// cuando la respuesta se está generando en un idioma distinto del que la aplicación habría
|
/// un idioma distinto del que la aplicación habría elegido automáticamente a partir de la
|
||||||
/// elegido automáticamente a partir de la configuración, el navegador y el idioma de
|
/// configuración, el navegador y el idioma de respaldo. En la práctica suele significar que
|
||||||
/// respaldo. En la práctica suele significar que el usuario ha pedido expresamente otro
|
/// el usuario ha pedido expresamente otro idioma (por ejemplo, con `?lang=...`) o que se ha
|
||||||
/// idioma (por ejemplo, con `?lang=...`) o que se ha forzado con
|
/// forzado con [`with_langid()`](Self::with_langid), y por tanto es recomendable propagar
|
||||||
/// [`with_langid()`](Self::with_langid), y por tanto es recomendable propagar `lang=...` en
|
/// `lang=...` en los enlaces para mantener esa preferencia mientras se navega.
|
||||||
/// los enlaces para mantener esa preferencia mientras se navega.
|
|
||||||
///
|
///
|
||||||
/// - En modos [`LangNegotiation::NoQuery`](crate::global::LangNegotiation::NoQuery) y
|
/// - En modos [`LangNegotiation::NoQuery`] y [`LangNegotiation::ConfigOnly`] siempre devuelve
|
||||||
/// [`LangNegotiation::ConfigOnly`](crate::global::LangNegotiation::ConfigOnly) siempre
|
/// `false`, ya que en estas estrategias la aplicación no utiliza el parámetro `?lang=...`
|
||||||
/// devuelve `false`, ya que en estas estrategias la aplicación no utiliza el parámetro
|
/// para seleccionar ni para propagar el idioma.
|
||||||
/// `?lang=...` para seleccionar ni para propagar el idioma.
|
///
|
||||||
|
/// [`LangNegotiation`]: crate::global::LangNegotiation
|
||||||
|
/// [`LangNegotiation::Full`]: crate::global::LangNegotiation::Full
|
||||||
|
/// [`LangNegotiation::NoQuery`]: crate::global::LangNegotiation::NoQuery
|
||||||
|
/// [`LangNegotiation::ConfigOnly`]: crate::global::LangNegotiation::ConfigOnly
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn needs_lang_query(&self) -> bool {
|
pub(crate) fn needs_lang_query(&self) -> bool {
|
||||||
match global::SETTINGS.app.lang_negotiation {
|
match global::SETTINGS.app.lang_negotiation {
|
||||||
|
|
@ -167,6 +184,19 @@ impl RequestLocale {
|
||||||
global::LangNegotiation::NoQuery | global::LangNegotiation::ConfigOnly => false,
|
global::LangNegotiation::NoQuery | global::LangNegotiation::ConfigOnly => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Devuelve el valor a propagar en el parámetro `?lang=...` de los enlaces generados.
|
||||||
|
///
|
||||||
|
/// Si el idioma efectivo procede del parámetro `?lang=...` de la petición, devuelve ese valor
|
||||||
|
/// tal como llegó, conservando su grafía original (p. ej. `"en"` o `"en-US"`). En caso
|
||||||
|
/// contrario (por ejemplo, si el idioma se forzó con [`with_langid()`](Self::with_langid)),
|
||||||
|
/// devuelve la forma canónica del idioma efectivo.
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn lang_query_value(&self) -> String {
|
||||||
|
self.query_lang
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| self.effective.to_string())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Permite a [`RequestLocale`] actuar como proveedor de idioma.
|
/// Permite a [`RequestLocale`] actuar como proveedor de idioma.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue