Añade formatos `DateFormat`, `TimeFormat`, `RelativeFormat` y `DatePrecision`, con estilos traducidos vía Fluent (orden día/mes/año, nombre del mes, plurales). La zona horaria efectiva se resuelve por petición, la propia del usuario autenticado si la tiene, si no la configurada para la aplicación (`app.timezone`), o UTC en su defecto- y se expone en `Contextual` junto a format_date(), format_time(), format_datetime(), format_iso_datetime(), format_relative(), format_since() y format_until().
270 lines
10 KiB
Rust
270 lines
10 KiB
Rust
use pagetop::prelude::*;
|
|
|
|
include_locales!(LOC from "examples/locale");
|
|
|
|
struct IntroDatetime;
|
|
|
|
#[async_trait]
|
|
impl Extension for IntroDatetime {
|
|
fn configure_router(&self, router: Router) -> Router {
|
|
router.route("/", web::get(intro_datetime))
|
|
}
|
|
}
|
|
|
|
async fn intro_datetime(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
|
Page::new(request)
|
|
.with_child(
|
|
Intro::custom()
|
|
.with_title(Lc::n("PageTop"))
|
|
.with_slogan(Lc::t("datetime_slogan", &LOC))
|
|
.with_child(world_block())
|
|
.with_child(formats_block())
|
|
.with_child(relative_block())
|
|
.with_child(since_until_block()),
|
|
)
|
|
.render()
|
|
.await
|
|
}
|
|
|
|
// Zonas IANA de la demostración, con la clave de su etiqueta traducible.
|
|
const ZONES: [(&str, &str); 4] = [
|
|
("datetime_zone_utc", "UTC"),
|
|
("datetime_zone_madrid", "Europe/Madrid"),
|
|
("datetime_zone_mexico", "America/Mexico_City"),
|
|
("datetime_zone_tokyo", "Asia/Tokyo"),
|
|
];
|
|
|
|
// El mismo instante (`Utc::now()`) mostrado en cada zona de `ZONES`: fecha y hora combinadas
|
|
// (`DateFormat::Medium` + `TimeFormat::Short`) y su equivalente ISO 8601, con el offset propio de
|
|
// cada zona.
|
|
fn world_block() -> Block {
|
|
Block::new()
|
|
.with_title(Lc::t("datetime_block_world", &LOC))
|
|
.with_child(Html::with(|cx| {
|
|
let now = Utc::now();
|
|
let rows: Vec<(Lc, String, String)> = ZONES
|
|
.into_iter()
|
|
.map(|(label_key, zone)| {
|
|
let tz: Tz = zone.parse().expect("valid IANA timezone");
|
|
let zone_cx = Context::default().with_langid(cx).with_timezone(tz);
|
|
(
|
|
Lc::t(label_key, &LOC),
|
|
zone_cx.format_datetime(now, DateFormat::Medium, TimeFormat::Short),
|
|
zone_cx.format_iso_datetime(now),
|
|
)
|
|
})
|
|
.collect();
|
|
|
|
html! {
|
|
ul {
|
|
@for (label, combined, iso) in &rows {
|
|
li {
|
|
strong { (label.using(cx)) ": " } (combined)
|
|
" (ISO " code { (iso) } ")"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}))
|
|
}
|
|
|
|
// Combinaciones de `DateFormat`/`TimeFormat` sobre la zona horaria efectiva del visitante. Para
|
|
// añadir un nuevo formato basta con ampliar el array correspondiente.
|
|
fn formats_block() -> Block {
|
|
Block::new()
|
|
.with_title(Lc::t("datetime_block_formats", &LOC))
|
|
.with_child(Html::with(|cx| {
|
|
let now = Utc::now();
|
|
let today = now.with_timezone(&cx.timezone()).date_naive();
|
|
|
|
let dates: Vec<(&str, String)> = [
|
|
("Short", DateFormat::Short),
|
|
("Medium", DateFormat::Medium),
|
|
("Long", DateFormat::Long),
|
|
("Iso", DateFormat::Iso),
|
|
("Custom(\"%d.%m.%Y\")", DateFormat::Custom("%d.%m.%Y")),
|
|
]
|
|
.into_iter()
|
|
.map(|(label, format)| (label, cx.format_date(today, format)))
|
|
.collect();
|
|
|
|
let times: Vec<(&str, String)> = [
|
|
("Short", TimeFormat::Short),
|
|
("Long", TimeFormat::Long),
|
|
("Custom(\"%I:%M %p\")", TimeFormat::Custom("%I:%M %p")),
|
|
]
|
|
.into_iter()
|
|
.map(|(label, format)| (label, cx.format_time(now, format)))
|
|
.collect();
|
|
|
|
let combos: Vec<(&str, String)> = [
|
|
("Short + Short", DateFormat::Short, TimeFormat::Short),
|
|
("Medium + Short", DateFormat::Medium, TimeFormat::Short),
|
|
("Long + Long", DateFormat::Long, TimeFormat::Long),
|
|
("Long + Short", DateFormat::Long, TimeFormat::Short),
|
|
("Iso + Long", DateFormat::Iso, TimeFormat::Long),
|
|
]
|
|
.into_iter()
|
|
.map(|(label, date, time)| (label, cx.format_datetime(now, date, time)))
|
|
.collect();
|
|
|
|
html! {
|
|
h3 { (Lc::t("datetime_formats_date", &LOC).using(cx)) }
|
|
ul {
|
|
@for (label, value) in &dates {
|
|
li { code { (label) } ": " (value) }
|
|
}
|
|
}
|
|
h3 { (Lc::t("datetime_formats_time", &LOC).using(cx)) }
|
|
ul {
|
|
@for (label, value) in × {
|
|
li { code { (label) } ": " (value) }
|
|
}
|
|
}
|
|
h3 { (Lc::t("datetime_formats_combos", &LOC).using(cx)) }
|
|
ul {
|
|
@for (label, value) in &combos {
|
|
li { code { (label) } ": " (value) }
|
|
}
|
|
}
|
|
p {
|
|
(Lc::t("datetime_formats_iso", &LOC).using(cx))
|
|
" " code { (cx.format_iso_datetime(now)) }
|
|
}
|
|
}
|
|
}))
|
|
}
|
|
|
|
// Casos ilustrativos de `RelativeFormat` sobre la zona horaria efectiva del visitante: hoy, un
|
|
// desplazamiento simple de días, uno con el componente de meses en cero, uno con los tres
|
|
// componentes y uno en el futuro -- siempre respecto al día actual. Añadir un caso nuevo es sólo
|
|
// ampliar `samples`. La primera columna muestra la fecha seleccionada (no una etiqueta fija),
|
|
// formateada con `DateFormat::Medium` en el idioma efectivo, para que se vea a qué fecha concreta
|
|
// corresponde cada resultado relativo.
|
|
fn relative_block() -> Block {
|
|
Block::new()
|
|
.with_title(Lc::t("datetime_block_relative", &LOC))
|
|
.with_child(Html::with(|cx| {
|
|
let today = Utc::now().date_naive();
|
|
|
|
// Desplazamientos de meses/años construidos con `checked_sub_months()` (mismo mecanismo
|
|
// que usa `RelativeFormat` por dentro), para que el resultado coincida exactamente con
|
|
// el criterio de cada caso.
|
|
let two_years_and_3_days_ago = today
|
|
.checked_sub_months(Months::new(24))
|
|
.expect("valid date")
|
|
- Duration::days(3);
|
|
let three_years_2_months_10_days_ago = today
|
|
.checked_sub_months(Months::new(38))
|
|
.expect("valid date")
|
|
- Duration::days(10);
|
|
|
|
let samples = [
|
|
today,
|
|
today - Duration::days(3),
|
|
two_years_and_3_days_ago,
|
|
three_years_2_months_10_days_ago,
|
|
today + Duration::days(5),
|
|
];
|
|
|
|
let rows: Vec<(String, String, String, String)> = samples
|
|
.into_iter()
|
|
.map(|date| {
|
|
let dt = at_noon(date);
|
|
(
|
|
cx.format_date(date, DateFormat::Medium),
|
|
cx.format_relative(dt, RelativeFormat::Short),
|
|
cx.format_relative(dt, RelativeFormat::Medium),
|
|
cx.format_relative(dt, RelativeFormat::Long),
|
|
)
|
|
})
|
|
.collect();
|
|
|
|
html! {
|
|
table style="width: 100%; border: 1px solid black;" {
|
|
thead {
|
|
tr {
|
|
th { (Lc::t("datetime_relative_col_date", &LOC).using(cx)) }
|
|
th { "Short" }
|
|
th { "Medium" }
|
|
th { "Long" }
|
|
}
|
|
}
|
|
tbody {
|
|
@for (date, short, medium, long) in &rows {
|
|
tr {
|
|
td { (date) }
|
|
td { (short) }
|
|
td { (medium) }
|
|
td { (long) }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}))
|
|
}
|
|
|
|
// Niveles de `DatePrecision` para `format_since()`/`format_until()`. Añadir un nivel nuevo es sólo
|
|
// ampliar este array.
|
|
const PRECISIONS: [(&str, DatePrecision); 3] = [
|
|
("Short", DatePrecision::Short),
|
|
("Medium", DatePrecision::Medium),
|
|
("Long", DatePrecision::Long),
|
|
];
|
|
|
|
// `format_since()`/`format_until()` sobre una única fecha de ejemplo: a diferencia de `DateFormat`,
|
|
// lo que cambia entre niveles no es el estilo, sino la propia precisión revelada (sólo el mes, mes
|
|
// y año, o fecha completa).
|
|
fn since_until_block() -> Block {
|
|
Block::new()
|
|
.with_title(Lc::t("datetime_block_since_until", &LOC))
|
|
.with_child(Html::with(|cx| {
|
|
let date = NaiveDate::from_ymd_opt(2026, 6, 3).unwrap();
|
|
|
|
let rows: Vec<(&str, String, String)> = PRECISIONS
|
|
.into_iter()
|
|
.map(|(label, precision)| {
|
|
(
|
|
label,
|
|
cx.format_since(date, precision),
|
|
cx.format_until(date, precision),
|
|
)
|
|
})
|
|
.collect();
|
|
|
|
html! {
|
|
table style="width: 100%; border: 1px solid black;" {
|
|
thead {
|
|
tr {
|
|
th { (Lc::t("datetime_since_until_col_precision", &LOC).using(cx)) }
|
|
th { (Lc::t("datetime_since_until_col_since", &LOC).using(cx)) }
|
|
th { (Lc::t("datetime_since_until_col_until", &LOC).using(cx)) }
|
|
}
|
|
}
|
|
tbody {
|
|
@for (label, since, until) in &rows {
|
|
tr {
|
|
td { (*label) }
|
|
td { (since) }
|
|
td { (until) }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}))
|
|
}
|
|
|
|
// Mediodía, para evitar que el redondeo horario de la conversión de zona horaria empuje la fecha
|
|
// civil resultante al día anterior o siguiente en zonas con un offset amplio.
|
|
fn at_noon(date: NaiveDate) -> DateTime<Utc> {
|
|
date.and_hms_opt(12, 0, 0)
|
|
.expect("noon is always a valid time")
|
|
.and_utc()
|
|
}
|
|
|
|
#[pagetop::main]
|
|
async fn main() -> std::io::Result<()> {
|
|
Application::prepare(&IntroDatetime).await.run().await
|
|
}
|