diff --git a/Cargo.lock b/Cargo.lock index bd2f629..9d8e2af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "accept-language" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f27d075294830fcab6f66e320dab524bc6d048f4a151698e153205559113772" + [[package]] name = "aho-corasick" version = "1.1.4" @@ -129,9 +135,9 @@ dependencies = [ [[package]] name = "axum" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" dependencies = [ "axum-core", "bytes", @@ -179,6 +185,28 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-extra" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be44683b41ccb9ab2d23a5230015c9c3c55be97a25e4428366de8873103f7970" +dependencies = [ + "axum", + "axum-core", + "bytes", + "cookie", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "base64" version = "0.22.1" @@ -353,6 +381,17 @@ dependencies = [ "memchr", ] +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -437,6 +476,15 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + [[package]] name = "deunicode" version = "1.6.2" @@ -491,6 +539,85 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "fluent-bundle" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01203cb8918f5711e73891b347816d932046f95f54207710bda99beaeb423bf4" +dependencies = [ + "fluent-langneg", + "fluent-syntax", + "intl-memoizer", + "intl_pluralrules", + "rustc-hash", + "self_cell", + "smallvec", + "unic-langid", +] + +[[package]] +name = "fluent-langneg" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eebbe59450baee8282d71676f3bfed5689aeab00b27545e83e5f14b1195e8b0" +dependencies = [ + "unic-langid", +] + +[[package]] +name = "fluent-syntax" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54f0d287c53ffd184d04d8677f590f4ac5379785529e5e08b1c8083acdd5c198" +dependencies = [ + "memchr", + "thiserror 2.0.18", +] + +[[package]] +name = "fluent-template-macros" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6b1dac7e7f07f8a60aa844f616bede13deeffaf24e9e35ae9e4979a7dac4a5c" +dependencies = [ + "flume", + "ignore", + "proc-macro2", + "quote", + "syn", + "unic-langid", +] + +[[package]] +name = "fluent-templates" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d48ac2b8e7c3a2198fc584af78e674d15dacdede8a6cebb686c844713f7d0ba" +dependencies = [ + "fluent-bundle", + "fluent-langneg", + "fluent-syntax", + "fluent-template-macros", + "flume", + "heck", + "ignore", + "intl-memoizer", + "log", + "serde_json", + "tera", + "thiserror 2.0.18", + "unic-langid", +] + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "spin", +] + [[package]] name = "fnv" version = "1.0.7" @@ -983,6 +1110,25 @@ dependencies = [ "hashbrown 0.16.1", ] +[[package]] +name = "intl-memoizer" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "310da2e345f5eb861e7a07ee182262e94975051db9e4223e909ba90f392f163f" +dependencies = [ + "type-map", + "unic-langid", +] + +[[package]] +name = "intl_pluralrules" +version = "7.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972" +dependencies = [ + "unic-langid", +] + [[package]] name = "ipnet" version = "2.12.0" @@ -1170,6 +1316,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + [[package]] name = "num-traits" version = "0.2.19" @@ -1363,6 +1515,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1372,6 +1530,12 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + [[package]] name = "proc-macro2" version = "1.0.106" @@ -1767,6 +1931,12 @@ dependencies = [ "libc", ] +[[package]] +name = "self_cell" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" + [[package]] name = "serde" version = "1.0.228" @@ -1875,10 +2045,13 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" name = "slimes-website" version = "0.1.2" dependencies = [ + "accept-language", "anyhow", "axum", + "axum-extra", "chrono", "clap", + "fluent-templates", "lazy_static", "reqwest", "serde", @@ -1917,6 +2090,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + [[package]] name = "spinning_top" version = "0.3.0" @@ -2067,6 +2249,37 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" version = "0.8.2" @@ -2074,6 +2287,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", + "serde_core", "zerovec", ] @@ -2312,6 +2526,15 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "type-map" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" +dependencies = [ + "rustc-hash", +] + [[package]] name = "typenum" version = "1.19.0" @@ -2324,6 +2547,49 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" +[[package]] +name = "unic-langid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05" +dependencies = [ + "unic-langid-impl", + "unic-langid-macros", +] + +[[package]] +name = "unic-langid-impl" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658" +dependencies = [ + "tinystr", +] + +[[package]] +name = "unic-langid-macros" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5957eb82e346d7add14182a3315a7e298f04e1ba4baac36f7f0dbfedba5fc25" +dependencies = [ + "proc-macro-hack", + "tinystr", + "unic-langid-impl", + "unic-langid-macros-impl", +] + +[[package]] +name = "unic-langid-macros-impl" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1249a628de3ad34b821ecb1001355bca3940bcb2f88558f1a8bd82e977f75b5" +dependencies = [ + "proc-macro-hack", + "quote", + "syn", + "unic-langid-impl", +] + [[package]] name = "unicode-ident" version = "1.0.24" @@ -2922,6 +3188,7 @@ version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ + "serde", "yoke", "zerofrom", "zerovec-derive", diff --git a/Cargo.toml b/Cargo.toml index 63486c7..06a3deb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,3 +18,6 @@ serde = { version = "1.0.228", features = ["derive"] } chrono = { version = "0.4.44", features = ["serde"] } reqwest = { version = "0.13.2", features = ["json"] } serde_json = "1.0.149" +fluent-templates = { version = "0.14.0", features = ["tera"] } +axum-extra = { version = "0.12.6", features = ["cookie"] } +accept-language = "3.1.0" diff --git a/locales/en.ftl b/locales/en.ftl new file mode 100644 index 0000000..c0f1559 --- /dev/null +++ b/locales/en.ftl @@ -0,0 +1,67 @@ +lang = en +nav-leaderboard = Leaderboard +nav-analytics = Analytics +nav-source-header = Source Code +nav-lang-header = Language +nav-cli-title = CLI + Server +nav-cli-desc = Core lib, CLI and API monorepo +nav-web-title = Website +nav-web-desc = This very website +nav-gui-title = GUI Version +nav-gui-desc = Experimental Cross-platform GUI +footer-text = Slimes + +home-title = Benchmark Reports +home-experimental = Experimental +home-experimental-tooltip = Show Android clients +home-sort-by = Sort by: +home-order-desc = Descending +home-order-asc = Ascending + +sort-score = Score +sort-single = Single Thread +sort-multi = Multi Thread +sort-ram = RAM Amount +sort-threads = Thread Count +sort-time = Time Ago + +sort-desc-score = (Singlethread + Multithread) / 2 +sort-desc-single = Single-core efficiency +sort-desc-multi = Multi-core efficiency +sort-desc-ram = System memory capacity +sort-desc-threads = Total logical cores +sort-desc-time = Newest reports first + +table-cpu-score = CPU Score +table-threads = Threads +table-ram = RAM +table-os = OS +table-hostname = Hostname +table-time = Time +table-note = Note +badge-new = NEW + +details-back = Back to Leaderboard +details-device = Device { $sig } +details-report-id = ID +details-version = VERSION +details-mac = MAC +details-reported = REPORTED +details-cpu-score = CPU Score +details-perf-breakdown = CPU Performance breakdown +details-single = Single-Thread +details-multi = Multi-Thread +details-threads = Threads + +analytics-title = Benchmark Analytics +analytics-insights = Insights from { $count } benchmarks +analytics-ram-eff = RAM Efficiency +analytics-ram-eff-desc = Avg Score per GB RAM +analytics-cpu-eff = CPU Thread Efficiency +analytics-cpu-eff-desc = Avg Score per Thread +analytics-chart-dist = Score Distribution +analytics-chart-os-count = Reports by Operating System +analytics-chart-os-avg = Avg Score by Operating System +analytics-chart-os-max = Highest Score by Operating System +analytics-chart-os-min = Lowest Score by Operating System +analytics-full-view = Full View diff --git a/locales/fr.ftl b/locales/fr.ftl new file mode 100644 index 0000000..837734a --- /dev/null +++ b/locales/fr.ftl @@ -0,0 +1,67 @@ +lang = fr +nav-leaderboard = Classement +nav-analytics = Statistiques +nav-source-header = Code Source +nav-lang-header = Langue +nav-cli-title = CLI + Serveur +nav-cli-desc = Lib principale, CLI et API +nav-web-title = Site Web +nav-web-desc = Ce site même +nav-gui-title = Version GUI +nav-gui-desc = GUI multiplateforme expérimentale +footer-text = Slimes + +home-title = Rapports de Benchmark +home-experimental = Expérimental +home-experimental-tooltip = Afficher les clients Android +home-sort-by = Trier par : +home-order-desc = Décroissant +home-order-asc = Croissant + +sort-score = Score +sort-single = Singlethread +sort-multi = Multithread +sort-ram = Quantité de RAM +sort-threads = Nombre de threads +sort-time = Temps + +sort-desc-score = (Singlethread + Multithread) / 2 +sort-desc-single = Efficacité singlethread +sort-desc-multi = Efficacité multithread +sort-desc-ram = Capacité mémoire système +sort-desc-threads = Total des coeurs logiques +sort-desc-time = Rapports les plus récents + +table-cpu-score = Score CPU +table-threads = Threads +table-ram = RAM +table-os = Système +table-hostname = Nom d'hôte +table-time = Temps +table-note = Note +badge-new = NOUVEAU + +details-back = Retour au classement +details-device = Machine { $sig } +details-report-id = ID +details-version = VERSION +details-mac = MAC +details-reported = ENVOYÉ +details-cpu-score = Score CPU +details-perf-breakdown = Détails des performances CPU +details-single = Mono-thread +details-multi = Multi-thread +details-threads = Threads + +analytics-title = Statistiques de Benchmark +analytics-insights = Aperçu de { $count } benchmarks +analytics-ram-eff = Efficacité RAM +analytics-ram-eff-desc = Score moyen par Go de RAM +analytics-cpu-eff = Efficacité CPU +analytics-cpu-eff-desc = Score moyen par thread +analytics-chart-dist = Distribution des scores +analytics-chart-os-count = Rapports par système d'exploitation +analytics-chart-os-avg = Score moyen par système d'exploitation +analytics-chart-os-max = Score le plus élevé par système +analytics-chart-os-min = Score le plus bas par système +analytics-full-view = Plein écran diff --git a/src/handlers.rs b/src/handlers.rs index 7bd4ed6..136bb85 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -1,20 +1,26 @@ use axum::{ extract::{Path, Query, State}, + http::HeaderMap, response::{Html, IntoResponse}, }; +use axum_extra::extract::CookieJar; use chrono::{DateTime, Utc}; -use reqwest::StatusCode; +use reqwest::{StatusCode, header::REFERER}; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, sync::Arc, time::Instant}; use tera::Context; -use crate::{AppState, TEMPLATES}; +use crate::{ + AppState, TEMPLATES, + i18n::{Lang, get_available_languages}, +}; #[derive(Debug, Serialize, Deserialize)] -pub struct SortParams { +pub struct Params { pub sort: Option, pub order: Option, pub experimental: Option, + pub lang: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -189,10 +195,37 @@ async fn get_cached_reports(state: &AppState) -> Result, (StatusCode Ok(fresh_reports) } +pub async fn set_lang_handler( + Path(lang): Path, + jar: CookieJar, + headers: HeaderMap, +) -> impl IntoResponse { + let cookie = axum_extra::extract::cookie::Cookie::build(("lang", lang)) + .path("/") + // .max_age(value) + .build(); + + let redirect_url = headers + .get(REFERER) + .and_then(|val| val.to_str().ok()) + .unwrap_or("/"); + + (jar.add(cookie), axum::response::Redirect::to(redirect_url)) +} + +fn add_common_context(context: &mut Context, lang: &str) { + context.insert("lang", lang); + context.insert("available_languages", &get_available_languages()); +} + pub async fn home_handler( State(state): State>, - Query(params): Query, + jar: CookieJar, + headers: HeaderMap, + Query(params): Query, ) -> Result { + let lang = Lang::resolve(&jar, &headers); + let raw_reports = get_cached_reports(&state).await?; let mut reports = process_reports(raw_reports); @@ -220,6 +253,7 @@ pub async fn home_handler( }); let mut context = Context::new(); + add_common_context(&mut context, &lang); context.insert("reports", &reports); context.insert("title", &state.app_name); context.insert("current_sort", &sort_field); @@ -227,6 +261,7 @@ pub async fn home_handler( context.insert("show_experimental", &show_experimental); context.insert("current_page", "leaderboard"); context.insert("prefix", ""); + context.insert("lang", &lang); match TEMPLATES.render("index.html", &context) { Ok(html) => Ok(Html(html)), @@ -237,13 +272,17 @@ pub async fn home_handler( pub async fn report_details_handler( State(state): State>, Path(id): Path, + jar: CookieJar, + headers: HeaderMap, ) -> Result { + let lang = Lang::resolve(&jar, &headers); + // Check if the report exists in our current cache to avoid API call { let cache = state.reports_cache.read().await; if let Some(c) = &*cache { if let Some(report) = c.data.iter().find(|r| r.id == id) { - return render_details(report.clone(), &state).await; + return render_details(report.clone(), &state, &lang).await; } } } @@ -257,14 +296,16 @@ pub async fn report_details_handler( .await .map_err(|e| (StatusCode::NOT_FOUND, format!("Report not found: {}", e)))?; - render_details(report, &state).await + render_details(report, &state, &lang).await } async fn render_details( report: Report, state: &AppState, + lang: &str, ) -> Result, (StatusCode, String)> { let mut context = Context::new(); + add_common_context(&mut context, lang); context.insert("report", &report); context.insert( "title", @@ -303,7 +344,11 @@ pub struct AnalyticsData { pub async fn analytics_handler( State(state): State>, + jar: CookieJar, + headers: HeaderMap, ) -> Result { + let lang = Lang::resolve(&jar, &headers); + let raw_reports = get_cached_reports(&state).await?; let reports = process_reports(raw_reports); @@ -416,6 +461,7 @@ pub async fn analytics_handler( .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; let mut context = Context::new(); + add_common_context(&mut context, &lang); context.insert("title", &format!("Analytics | {}", state.app_name)); context.insert("json_data", &json_data); context.insert("data", &data); diff --git a/src/i18n.rs b/src/i18n.rs new file mode 100644 index 0000000..4fd26ee --- /dev/null +++ b/src/i18n.rs @@ -0,0 +1,46 @@ +use axum_extra::extract::CookieJar; +use fluent_templates::{Loader, static_loader}; +use reqwest::header::ACCEPT_LANGUAGE; + +static_loader! { + pub static LOCALES = { + locales: "./locales", + fallback_language: "en", + }; +} + +pub struct Lang; +impl Lang { + pub fn resolve(jar: &CookieJar, headers: &axum::http::HeaderMap) -> String { + if let Some(lang_cookie) = jar.get("lang").map(|c| c.value().to_string()) { + return lang_cookie; + } + + if let Some(accept_lang) = headers.get(ACCEPT_LANGUAGE).and_then(|h| h.to_str().ok()) { + let preferred: Vec<&str> = accept_lang + .split(',') + .map(|s| s.split(';').next().unwrap_or("").trim()) + .filter(|s| !s.is_empty()) + .collect(); + + let available = get_available_languages(); + for lang in preferred { + // Exact match (e.g., "en-US") + if available.iter().any(|a| a == lang) { + return lang.to_string(); + } + // Base match (e.g., "en" from "en-US") + let base = lang.split('-').next().unwrap_or(""); + if available.iter().any(|a| a == base) { + return base.to_string(); + } + } + } + + "en".to_string() + } +} + +pub fn get_available_languages() -> Vec { + LOCALES.locales().map(|id| id.to_string()).collect() +} diff --git a/src/main.rs b/src/main.rs index b153369..a00ab84 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ use axum::{Router, routing::get}; use clap::Parser; +use fluent_templates::FluentLoader; use lazy_static::lazy_static; use std::{ sync::Arc, @@ -10,8 +11,12 @@ use tokio::sync::RwLock; mod handlers; use handlers::home_handler; +mod i18n; -use crate::handlers::{Report, analytics_handler, report_details_handler}; +use crate::{ + handlers::{Report, analytics_handler, report_details_handler, set_lang_handler}, + i18n::LOCALES, +}; lazy_static! { pub static ref TEMPLATES: Tera = { @@ -27,6 +32,7 @@ lazy_static! { ), ]) .unwrap(); + tera.register_function("fluent", FluentLoader::new(&*LOCALES)); tera }; } @@ -76,6 +82,7 @@ async fn main() -> anyhow::Result<()> { .route("/", get(home_handler)) .route("/report/{id}", get(report_details_handler)) .route("/analytics", get(analytics_handler)) + .route("/set-lang/{lang}", get(set_lang_handler)) .with_state(shared_state); let addr = if cli.host { diff --git a/templates/_layout.html b/templates/_layout.html index ecda191..dec4bb1 100644 --- a/templates/_layout.html +++ b/templates/_layout.html @@ -1,5 +1,5 @@ - + @@ -15,33 +15,58 @@ @@ -49,23 +74,33 @@
{% block content %}{% endblock %}
-
{{ now() | date(format="%Y") }} - Slimes
+
{{ now() | date(format="%Y") }} - {{ fluent(key="footer-text", lang=lang) }}
@@ -93,6 +128,14 @@ gap: 2rem; } +.nav-right { + display: flex; + align-items: center; + flex-direction: row; + justify-content: center; + gap: 2rem; +} + .nav-logo { font-weight: 800; color: var(--primary-color); @@ -227,7 +270,7 @@ } @media (max-width: 640px) { - + /* .nav-links { display: none; } */ .nav-left { gap: 1rem; } } diff --git a/templates/analytics.html b/templates/analytics.html index 4934534..4fcce35 100644 --- a/templates/analytics.html +++ b/templates/analytics.html @@ -2,8 +2,8 @@ {% macro chart_card(id, title) %}
-

{{ title }}

@@ -12,8 +12,8 @@ {% block content %}
-

Benchmark Analytics

-

Insights from {{ data.total_reports }} benchmarks

+

{{ fluent(key="analytics-title", lang=lang) }}

+

{{ fluent(key="analytics-insights", lang=lang, count=data.total_reports) }}