feat(i18n): add internationalization with DE, FR, ES, PT translations (#12)
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> Co-authored-by: Sharang Parnerkar <parnerkarsharang@gmail.com> Reviewed-on: #12
This commit was merged in pull request #12.
This commit is contained in:
@@ -5,6 +5,7 @@ use dioxus_free_icons::icons::bs_icons::{
|
||||
use dioxus_free_icons::icons::fa_solid_icons::FaCubes;
|
||||
use dioxus_free_icons::Icon;
|
||||
|
||||
use crate::i18n::{t, Locale};
|
||||
use crate::Route;
|
||||
|
||||
/// Public landing page for the CERTifAI platform.
|
||||
@@ -30,6 +31,9 @@ pub fn LandingPage() -> Element {
|
||||
/// Sticky top navigation bar with logo, nav links, and CTA buttons.
|
||||
#[component]
|
||||
fn LandingNav() -> Element {
|
||||
let locale = use_context::<Signal<Locale>>();
|
||||
let l = *locale.read();
|
||||
|
||||
rsx! {
|
||||
nav { class: "landing-nav",
|
||||
div { class: "landing-nav-inner",
|
||||
@@ -40,9 +44,9 @@ fn LandingNav() -> Element {
|
||||
span { "CERTifAI" }
|
||||
}
|
||||
div { class: "landing-nav-links",
|
||||
a { href: "#features", "Features" }
|
||||
a { href: "#how-it-works", "How It Works" }
|
||||
a { href: "#pricing", "Pricing" }
|
||||
a { href: "#features", {t(l, "common.features")} }
|
||||
a { href: "#how-it-works", {t(l, "common.how_it_works")} }
|
||||
a { href: "#pricing", {t(l, "nav.pricing")} }
|
||||
}
|
||||
div { class: "landing-nav-actions",
|
||||
Link {
|
||||
@@ -50,14 +54,14 @@ fn LandingNav() -> Element {
|
||||
redirect_url: "/dashboard".into(),
|
||||
},
|
||||
class: "btn btn-ghost btn-sm",
|
||||
"Log In"
|
||||
{t(l, "common.log_in")}
|
||||
}
|
||||
Link {
|
||||
to: Route::Login {
|
||||
redirect_url: "/dashboard".into(),
|
||||
},
|
||||
class: "btn btn-primary btn-sm",
|
||||
"Get Started"
|
||||
{t(l, "common.get_started")}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,19 +72,20 @@ fn LandingNav() -> Element {
|
||||
/// Hero section with headline, subtitle, and CTA buttons.
|
||||
#[component]
|
||||
fn HeroSection() -> Element {
|
||||
let locale = use_context::<Signal<Locale>>();
|
||||
let l = *locale.read();
|
||||
|
||||
rsx! {
|
||||
section { class: "hero-section",
|
||||
div { class: "hero-content",
|
||||
div { class: "hero-badge badge badge-outline", "Privacy-First GenAI Infrastructure" }
|
||||
div { class: "hero-badge badge badge-outline", {t(l, "landing.badge")} }
|
||||
h1 { class: "hero-title",
|
||||
"Your AI. Your Data."
|
||||
{t(l, "landing.hero_title_1")}
|
||||
br {}
|
||||
span { class: "hero-title-accent", "Your Infrastructure." }
|
||||
span { class: "hero-title-accent", {t(l, "landing.hero_title_2")} }
|
||||
}
|
||||
p { class: "hero-subtitle",
|
||||
"Self-hosted, GDPR-compliant generative AI platform for "
|
||||
"enterprises that refuse to compromise on data sovereignty. "
|
||||
"Deploy LLMs, agents, and MCP servers on your own terms."
|
||||
{t(l, "landing.hero_subtitle")}
|
||||
}
|
||||
div { class: "hero-actions",
|
||||
Link {
|
||||
@@ -88,10 +93,12 @@ fn HeroSection() -> Element {
|
||||
redirect_url: "/dashboard".into(),
|
||||
},
|
||||
class: "btn btn-primary btn-lg",
|
||||
"Get Started"
|
||||
{t(l, "common.get_started")}
|
||||
Icon { icon: BsArrowRight, width: 18, height: 18 }
|
||||
}
|
||||
a { href: "#features", class: "btn btn-outline btn-lg", "Learn More" }
|
||||
a { href: "#features", class: "btn btn-outline btn-lg",
|
||||
{t(l, "landing.learn_more")}
|
||||
}
|
||||
}
|
||||
}
|
||||
div { class: "hero-graphic",
|
||||
@@ -273,31 +280,34 @@ fn HeroSection() -> Element {
|
||||
/// Social proof / trust indicator strip.
|
||||
#[component]
|
||||
fn SocialProof() -> Element {
|
||||
let locale = use_context::<Signal<Locale>>();
|
||||
let l = *locale.read();
|
||||
|
||||
rsx! {
|
||||
section { class: "social-proof",
|
||||
p { class: "social-proof-text",
|
||||
"Built for enterprises that value "
|
||||
span { class: "social-proof-highlight", "data sovereignty" }
|
||||
{t(l, "landing.social_proof")}
|
||||
span { class: "social-proof-highlight", {t(l, "landing.data_sovereignty")} }
|
||||
}
|
||||
div { class: "social-proof-stats",
|
||||
div { class: "proof-stat",
|
||||
span { class: "proof-stat-value", "100%" }
|
||||
span { class: "proof-stat-label", "On-Premise" }
|
||||
span { class: "proof-stat-label", {t(l, "landing.on_premise")} }
|
||||
}
|
||||
div { class: "proof-divider" }
|
||||
div { class: "proof-stat",
|
||||
span { class: "proof-stat-value", "GDPR" }
|
||||
span { class: "proof-stat-label", "Compliant" }
|
||||
span { class: "proof-stat-label", {t(l, "landing.compliant")} }
|
||||
}
|
||||
div { class: "proof-divider" }
|
||||
div { class: "proof-stat",
|
||||
span { class: "proof-stat-value", "EU" }
|
||||
span { class: "proof-stat-label", "Data Residency" }
|
||||
span { class: "proof-stat-label", {t(l, "landing.data_residency")} }
|
||||
}
|
||||
div { class: "proof-divider" }
|
||||
div { class: "proof-stat",
|
||||
span { class: "proof-stat-value", "Zero" }
|
||||
span { class: "proof-stat-label", "Third-Party Sharing" }
|
||||
span { class: "proof-stat-label", {t(l, "landing.third_party")} }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -307,60 +317,57 @@ fn SocialProof() -> Element {
|
||||
/// Feature cards grid section.
|
||||
#[component]
|
||||
fn FeaturesGrid() -> Element {
|
||||
let locale = use_context::<Signal<Locale>>();
|
||||
let l = *locale.read();
|
||||
|
||||
rsx! {
|
||||
section { id: "features", class: "features-section",
|
||||
h2 { class: "section-title", "Everything You Need" }
|
||||
h2 { class: "section-title", {t(l, "landing.features_title")} }
|
||||
p { class: "section-subtitle",
|
||||
"A complete, self-hosted GenAI stack under your full control."
|
||||
{t(l, "landing.features_subtitle")}
|
||||
}
|
||||
div { class: "features-grid",
|
||||
FeatureCard {
|
||||
icon: rsx! {
|
||||
Icon { icon: BsServer, width: 28, height: 28 }
|
||||
},
|
||||
title: "Self-Hosted Infrastructure",
|
||||
description: "Deploy on your own hardware or private cloud. \
|
||||
Full control over your AI stack with no external dependencies.",
|
||||
title: t(l, "landing.feat_infra_title"),
|
||||
description: t(l, "landing.feat_infra_desc"),
|
||||
}
|
||||
FeatureCard {
|
||||
icon: rsx! {
|
||||
Icon { icon: BsShieldCheck, width: 28, height: 28 }
|
||||
},
|
||||
title: "GDPR Compliant",
|
||||
description: "EU data residency guaranteed. Your data never \
|
||||
leaves your infrastructure or gets shared with third parties.",
|
||||
title: t(l, "landing.feat_gdpr_title"),
|
||||
description: t(l, "landing.feat_gdpr_desc"),
|
||||
}
|
||||
FeatureCard {
|
||||
icon: rsx! {
|
||||
Icon { icon: FaCubes, width: 28, height: 28 }
|
||||
},
|
||||
title: "LLM Management",
|
||||
description: "Deploy, monitor, and manage multiple language \
|
||||
models. Switch between models with zero downtime.",
|
||||
title: t(l, "landing.feat_llm_title"),
|
||||
description: t(l, "landing.feat_llm_desc"),
|
||||
}
|
||||
FeatureCard {
|
||||
icon: rsx! {
|
||||
Icon { icon: BsRobot, width: 28, height: 28 }
|
||||
},
|
||||
title: "Agent Builder",
|
||||
description: "Create custom AI agents with integrated Langchain \
|
||||
and Langfuse for full observability and control.",
|
||||
title: t(l, "landing.feat_agent_title"),
|
||||
description: t(l, "landing.feat_agent_desc"),
|
||||
}
|
||||
FeatureCard {
|
||||
icon: rsx! {
|
||||
Icon { icon: BsGlobe2, width: 28, height: 28 }
|
||||
},
|
||||
title: "MCP Server Management",
|
||||
description: "Manage Model Context Protocol servers to extend \
|
||||
your AI capabilities with external tool integrations.",
|
||||
title: t(l, "landing.feat_mcp_title"),
|
||||
description: t(l, "landing.feat_mcp_desc"),
|
||||
}
|
||||
FeatureCard {
|
||||
icon: rsx! {
|
||||
Icon { icon: BsKey, width: 28, height: 28 }
|
||||
},
|
||||
title: "API Key Management",
|
||||
description: "Generate API keys, track usage per seat, and \
|
||||
set fine-grained permissions for every integration.",
|
||||
title: t(l, "landing.feat_api_title"),
|
||||
description: t(l, "landing.feat_api_desc"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -372,10 +379,10 @@ fn FeaturesGrid() -> Element {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `icon` - The icon element to display
|
||||
/// * `title` - Feature title
|
||||
/// * `description` - Feature description text
|
||||
/// * `title` - Feature title (owned String from translation lookup)
|
||||
/// * `description` - Feature description text (owned String from translation lookup)
|
||||
#[component]
|
||||
fn FeatureCard(icon: Element, title: &'static str, description: &'static str) -> Element {
|
||||
fn FeatureCard(icon: Element, title: String, description: String) -> Element {
|
||||
rsx! {
|
||||
div { class: "card feature-card",
|
||||
div { class: "feature-card-icon", {icon} }
|
||||
@@ -388,31 +395,28 @@ fn FeatureCard(icon: Element, title: &'static str, description: &'static str) ->
|
||||
/// Three-step "How It Works" section.
|
||||
#[component]
|
||||
fn HowItWorks() -> Element {
|
||||
let locale = use_context::<Signal<Locale>>();
|
||||
let l = *locale.read();
|
||||
|
||||
rsx! {
|
||||
section { id: "how-it-works", class: "how-it-works-section",
|
||||
h2 { class: "section-title", "Up and Running in Minutes" }
|
||||
p { class: "section-subtitle", "Three steps to sovereign AI infrastructure." }
|
||||
h2 { class: "section-title", {t(l, "landing.how_title")} }
|
||||
p { class: "section-subtitle", {t(l, "landing.how_subtitle")} }
|
||||
div { class: "steps-grid",
|
||||
StepCard {
|
||||
number: "01",
|
||||
title: "Deploy",
|
||||
description: "Install CERTifAI on your infrastructure \
|
||||
with a single command. Supports Docker, Kubernetes, \
|
||||
and bare metal.",
|
||||
title: t(l, "landing.step_deploy"),
|
||||
description: t(l, "landing.step_deploy_desc"),
|
||||
}
|
||||
StepCard {
|
||||
number: "02",
|
||||
title: "Configure",
|
||||
description: "Connect your identity provider, select \
|
||||
your models, and set up team permissions through \
|
||||
the admin dashboard.",
|
||||
title: t(l, "landing.step_configure"),
|
||||
description: t(l, "landing.step_configure_desc"),
|
||||
}
|
||||
StepCard {
|
||||
number: "03",
|
||||
title: "Scale",
|
||||
description: "Add users, deploy more models, and \
|
||||
integrate with your existing tools via API keys \
|
||||
and MCP servers.",
|
||||
title: t(l, "landing.step_scale"),
|
||||
description: t(l, "landing.step_scale_desc"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -424,10 +428,10 @@ fn HowItWorks() -> Element {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `number` - Step number string (e.g. "01")
|
||||
/// * `title` - Step title
|
||||
/// * `description` - Step description text
|
||||
/// * `title` - Step title (owned String from translation lookup)
|
||||
/// * `description` - Step description text (owned String from translation lookup)
|
||||
#[component]
|
||||
fn StepCard(number: &'static str, title: &'static str, description: &'static str) -> Element {
|
||||
fn StepCard(number: &'static str, title: String, description: String) -> Element {
|
||||
rsx! {
|
||||
div { class: "step-card",
|
||||
span { class: "step-number", "{number}" }
|
||||
@@ -440,11 +444,14 @@ fn StepCard(number: &'static str, title: &'static str, description: &'static str
|
||||
/// Call-to-action banner before the footer.
|
||||
#[component]
|
||||
fn CtaBanner() -> Element {
|
||||
let locale = use_context::<Signal<Locale>>();
|
||||
let l = *locale.read();
|
||||
|
||||
rsx! {
|
||||
section { class: "cta-banner",
|
||||
h2 { class: "cta-title", "Ready to take control of your AI infrastructure?" }
|
||||
h2 { class: "cta-title", {t(l, "landing.cta_title")} }
|
||||
p { class: "cta-subtitle",
|
||||
"Start deploying sovereign GenAI today. No credit card required."
|
||||
{t(l, "landing.cta_subtitle")}
|
||||
}
|
||||
div { class: "cta-actions",
|
||||
Link {
|
||||
@@ -452,7 +459,7 @@ fn CtaBanner() -> Element {
|
||||
redirect_url: "/dashboard".into(),
|
||||
},
|
||||
class: "btn btn-primary btn-lg",
|
||||
"Get Started Free"
|
||||
{t(l, "landing.get_started_free")}
|
||||
Icon { icon: BsArrowRight, width: 18, height: 18 }
|
||||
}
|
||||
Link {
|
||||
@@ -460,7 +467,7 @@ fn CtaBanner() -> Element {
|
||||
redirect_url: "/dashboard".into(),
|
||||
},
|
||||
class: "btn btn-outline btn-lg",
|
||||
"Log In"
|
||||
{t(l, "common.log_in")}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -470,6 +477,9 @@ fn CtaBanner() -> Element {
|
||||
/// Landing page footer with links and copyright.
|
||||
#[component]
|
||||
fn LandingFooter() -> Element {
|
||||
let locale = use_context::<Signal<Locale>>();
|
||||
let l = *locale.read();
|
||||
|
||||
rsx! {
|
||||
footer { class: "landing-footer",
|
||||
div { class: "landing-footer-inner",
|
||||
@@ -480,28 +490,28 @@ fn LandingFooter() -> Element {
|
||||
}
|
||||
span { "CERTifAI" }
|
||||
}
|
||||
p { class: "footer-tagline", "Sovereign GenAI infrastructure for enterprises." }
|
||||
p { class: "footer-tagline", {t(l, "landing.footer_tagline")} }
|
||||
}
|
||||
div { class: "footer-links-group",
|
||||
h4 { class: "footer-links-heading", "Product" }
|
||||
a { href: "#features", "Features" }
|
||||
a { href: "#how-it-works", "How It Works" }
|
||||
a { href: "#pricing", "Pricing" }
|
||||
h4 { class: "footer-links-heading", {t(l, "landing.product")} }
|
||||
a { href: "#features", {t(l, "common.features")} }
|
||||
a { href: "#how-it-works", {t(l, "common.how_it_works")} }
|
||||
a { href: "#pricing", {t(l, "nav.pricing")} }
|
||||
}
|
||||
div { class: "footer-links-group",
|
||||
h4 { class: "footer-links-heading", "Legal" }
|
||||
Link { to: Route::ImpressumPage {}, "Impressum" }
|
||||
Link { to: Route::PrivacyPage {}, "Privacy Policy" }
|
||||
h4 { class: "footer-links-heading", {t(l, "landing.legal")} }
|
||||
Link { to: Route::ImpressumPage {}, {t(l, "common.impressum")} }
|
||||
Link { to: Route::PrivacyPage {}, {t(l, "common.privacy_policy")} }
|
||||
}
|
||||
div { class: "footer-links-group",
|
||||
h4 { class: "footer-links-heading", "Resources" }
|
||||
a { href: "#", "Documentation" }
|
||||
a { href: "#", "API Reference" }
|
||||
a { href: "#", "Support" }
|
||||
h4 { class: "footer-links-heading", {t(l, "landing.resources")} }
|
||||
a { href: "#", {t(l, "landing.documentation")} }
|
||||
a { href: "#", {t(l, "landing.api_reference")} }
|
||||
a { href: "#", {t(l, "landing.support")} }
|
||||
}
|
||||
}
|
||||
div { class: "footer-bottom",
|
||||
p { "2026 CERTifAI. All rights reserved." }
|
||||
p { {t(l, "landing.copyright")} }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user