All checks were successful
CI / Format (push) Successful in 30s
CI / Clippy (push) Successful in 2m36s
CI / Security Audit (push) Has been skipped
CI / Tests (push) Has been skipped
CI / Format (pull_request) Successful in 3s
CI / Clippy (pull_request) Successful in 2m15s
CI / Security Audit (pull_request) Has been skipped
CI / Tests (pull_request) Has been skipped
CI / Deploy (push) Has been skipped
CI / Deploy (pull_request) Has been skipped
Add a compile-time i18n system with 270 translation keys across 5 locales (EN, DE, FR, ES, PT). Translations are embedded via include_str! and parsed lazily into flat HashMaps with English fallback for missing keys. - Add src/i18n module with Locale enum, t()/tw() lookup functions, and tests - Add JSON translation files for all 5 locales under assets/i18n/ - Provide locale Signal via Dioxus context in App, persisted to localStorage - Replace all hardcoded UI strings across 33 component/page files - Add compact locale picker (globe icon + ISO alpha-2 code) in sidebar header - Add click-outside backdrop dismissal for locale dropdown Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
142 lines
4.8 KiB
Rust
142 lines
4.8 KiB
Rust
use crate::i18n::Locale;
|
|
use crate::{components::*, pages::*};
|
|
use dioxus::prelude::*;
|
|
|
|
/// Application routes.
|
|
///
|
|
/// Public pages (`LandingPage`, `ImpressumPage`, `PrivacyPage`) live
|
|
/// outside the `AppShell` layout. Authenticated pages are wrapped in
|
|
/// `AppShell` which renders the sidebar. `DeveloperShell` and `OrgShell`
|
|
/// provide nested tab navigation within the app shell.
|
|
#[derive(Debug, Clone, Routable, PartialEq)]
|
|
#[rustfmt::skip]
|
|
pub enum Route {
|
|
#[route("/")]
|
|
LandingPage {},
|
|
#[route("/impressum")]
|
|
ImpressumPage {},
|
|
#[route("/privacy")]
|
|
PrivacyPage {},
|
|
#[layout(AppShell)]
|
|
#[route("/dashboard")]
|
|
DashboardPage {},
|
|
#[route("/providers")]
|
|
ProvidersPage {},
|
|
#[route("/chat")]
|
|
ChatPage {},
|
|
#[route("/tools")]
|
|
ToolsPage {},
|
|
#[route("/knowledge")]
|
|
KnowledgePage {},
|
|
|
|
#[layout(DeveloperShell)]
|
|
#[route("/developer/agents")]
|
|
AgentsPage {},
|
|
#[route("/developer/flow")]
|
|
FlowPage {},
|
|
#[route("/developer/analytics")]
|
|
AnalyticsPage {},
|
|
#[end_layout]
|
|
|
|
#[layout(OrgShell)]
|
|
#[route("/organization/pricing")]
|
|
OrgPricingPage {},
|
|
#[route("/organization/dashboard")]
|
|
OrgDashboardPage {},
|
|
#[end_layout]
|
|
#[end_layout]
|
|
|
|
#[route("/login?:redirect_url")]
|
|
Login { redirect_url: String },
|
|
}
|
|
|
|
const FAVICON: Asset = asset!("/assets/favicon.svg");
|
|
const MAIN_CSS: Asset = asset!("/assets/main.css");
|
|
const TAILWIND_CSS: Asset = asset!("/assets/tailwind.css");
|
|
const MANIFEST: Asset = asset!("/assets/manifest.json");
|
|
|
|
/// Google Fonts URL for Inter (body) and Space Grotesk (headings).
|
|
const GOOGLE_FONTS: &str = "https://fonts.googleapis.com/css2?\
|
|
family=Inter:wght@400;500;600&\
|
|
family=Space+Grotesk:wght@500;600;700&\
|
|
display=swap";
|
|
|
|
/// Root application component. Loads global assets and mounts the router.
|
|
///
|
|
/// Provides a `Signal<Locale>` context that all child components can read
|
|
/// via `use_context::<Signal<Locale>>()` to access the current locale.
|
|
/// The locale is persisted in `localStorage` under `"certifai_locale"`.
|
|
#[component]
|
|
pub fn App() -> Element {
|
|
// Read persisted locale from localStorage on first render.
|
|
let initial_locale = {
|
|
#[cfg(feature = "web")]
|
|
{
|
|
web_sys::window()
|
|
.and_then(|w| w.local_storage().ok().flatten())
|
|
.and_then(|s| s.get_item("certifai_locale").ok().flatten())
|
|
.map(|code| Locale::from_code(&code))
|
|
.unwrap_or_default()
|
|
}
|
|
#[cfg(not(feature = "web"))]
|
|
{
|
|
Locale::default()
|
|
}
|
|
};
|
|
use_context_provider(|| Signal::new(initial_locale));
|
|
|
|
rsx! {
|
|
// Seggwat feedback widget
|
|
document::Script {
|
|
src: "https://seggwat.com/static/widgets/v1/seggwat-feedback.js",
|
|
r#defer: true,
|
|
"data-project-key": "a04b8cf1-9177-42ce-8a7b-084f38b99799",
|
|
"data-button-color": "#6d85c6",
|
|
"data-button-position": "right-side",
|
|
"data-enable-screenshots": "true",
|
|
}
|
|
|
|
document::Link { rel: "icon", href: FAVICON }
|
|
document::Link { rel: "manifest", href: MANIFEST }
|
|
document::Meta { name: "theme-color", content: "#4B3FE0" }
|
|
document::Meta { name: "apple-mobile-web-app-capable", content: "yes" }
|
|
document::Meta {
|
|
name: "apple-mobile-web-app-status-bar-style",
|
|
content: "black-translucent",
|
|
}
|
|
document::Link { rel: "apple-touch-icon", href: FAVICON }
|
|
document::Link { rel: "preconnect", href: "https://fonts.googleapis.com" }
|
|
document::Link {
|
|
rel: "preconnect",
|
|
href: "https://fonts.gstatic.com",
|
|
crossorigin: "anonymous",
|
|
}
|
|
document::Link { rel: "stylesheet", href: GOOGLE_FONTS }
|
|
document::Link { rel: "stylesheet", href: TAILWIND_CSS }
|
|
document::Link { rel: "stylesheet", href: MAIN_CSS }
|
|
|
|
// Register PWA service worker
|
|
document::Script {
|
|
r#"
|
|
if ('serviceWorker' in navigator) {{
|
|
navigator.serviceWorker.register('/assets/sw.js')
|
|
.catch(function(e) {{ console.warn('SW registration failed:', e); }});
|
|
}}
|
|
"#
|
|
}
|
|
|
|
// Apply persisted theme to <html> before first paint to avoid flash.
|
|
// Default to certifai-dark when no preference is stored.
|
|
document::Script {
|
|
r#"
|
|
(function() {{
|
|
var t = localStorage.getItem('theme') || 'certifai-dark';
|
|
document.documentElement.setAttribute('data-theme', t);
|
|
}})();
|
|
"#
|
|
}
|
|
|
|
Router::<Route> {}
|
|
}
|
|
}
|