feat(i18n): add internationalization with DE, FR, ES, PT translations
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>
This commit is contained in:
Sharang Parnerkar
2026-02-22 12:39:17 +01:00
parent 50237f5377
commit 007c7e2686
39 changed files with 2536 additions and 374 deletions

View File

@@ -1,6 +1,7 @@
use crate::components::{
ChatActionBar, ChatInputBar, ChatMessageList, ChatModelSelector, ChatSidebar,
};
use crate::i18n::{t, Locale};
use crate::infrastructure::chat::{
chat_complete, create_chat_session, delete_chat_session, list_chat_messages,
list_chat_sessions, rename_chat_session, save_chat_message,
@@ -15,6 +16,9 @@ use dioxus::prelude::*;
/// Messages stream via `EventSource` connected to `/api/chat/stream`.
#[component]
pub fn ChatPage() -> Element {
let locale = use_context::<Signal<Locale>>();
let l = *locale.read();
// ---- Signals ----
let mut active_session_id: Signal<Option<String>> = use_signal(|| None);
let mut messages: Signal<Vec<ChatMessage>> = use_signal(Vec::new);
@@ -68,9 +72,10 @@ pub fn ChatPage() -> Element {
// Create new session
let on_new = move |_: ()| {
let model = selected_model.read().clone();
let new_chat_title = t(l, "chat.new_chat");
spawn(async move {
match create_chat_session(
"New Chat".to_string(),
new_chat_title,
"General".to_string(),
"ollama".to_string(),
model,
@@ -235,14 +240,17 @@ pub fn ChatPage() -> Element {
let on_share = move |_: ()| {
#[cfg(feature = "web")]
{
let you_label = t(l, "chat.you");
let assistant_label = t(l, "chat.assistant");
let text: String = messages
.read()
.iter()
.filter(|m| m.role != ChatRole::System)
.map(|m| {
let label = match m.role {
ChatRole::User => "You",
ChatRole::Assistant => "Assistant",
ChatRole::User => &you_label,
ChatRole::Assistant => &assistant_label,
// Filtered out above, but required for exhaustive match
ChatRole::System => "System",
};
format!("{label}:\n{}\n", m.content)

View File

@@ -2,6 +2,7 @@ use dioxus::prelude::*;
use dioxus_sdk::storage::use_persistent;
use crate::components::{ArticleDetail, DashboardSidebar, NewsCardView, PageHeader};
use crate::i18n::{t, Locale};
use crate::infrastructure::chat::{create_chat_session, save_chat_message};
use crate::infrastructure::llm::FollowUpMessage;
use crate::models::NewsCard;
@@ -28,6 +29,9 @@ const DEFAULT_TOPICS: &[&str] = &[
/// - `certifai_ollama_model`: Ollama model ID for summarization
#[component]
pub fn DashboardPage() -> Element {
let locale = use_context::<Signal<Locale>>();
let l = *locale.read();
// Persistent state stored in localStorage
let mut custom_topics = use_persistent("certifai_topics".to_string(), Vec::<String>::new);
// Default to empty so the server functions use OLLAMA_URL / OLLAMA_MODEL
@@ -133,8 +137,8 @@ pub fn DashboardPage() -> Element {
rsx! {
section { class: "dashboard-page",
PageHeader {
title: "Dashboard".to_string(),
subtitle: "AI news and updates".to_string(),
title: t(l, "dashboard.title"),
subtitle: t(l, "dashboard.subtitle"),
}
// Topic tabs row
@@ -188,7 +192,7 @@ pub fn DashboardPage() -> Element {
input {
class: "topic-input",
r#type: "text",
placeholder: "Topic name...",
placeholder: "{t(l, \"dashboard.topic_placeholder\")}",
value: "{new_topic_text}",
oninput: move |e| new_topic_text.set(e.value()),
onkeypress: move |e| {
@@ -214,7 +218,7 @@ pub fn DashboardPage() -> Element {
show_add_input.set(false);
new_topic_text.set(String::new());
},
"Cancel"
"{t(l, \"common.cancel\")}"
}
}
} else {
@@ -236,33 +240,33 @@ pub fn DashboardPage() -> Element {
}
show_settings.set(!currently_shown);
},
"Settings"
"{t(l, \"common.settings\")}"
}
}
// Settings panel (collapsible)
if *show_settings.read() {
div { class: "settings-panel",
h4 { class: "settings-panel-title", "Ollama Settings" }
h4 { class: "settings-panel-title", "{t(l, \"dashboard.ollama_settings\")}" }
p { class: "settings-hint",
"Leave empty to use OLLAMA_URL / OLLAMA_MODEL from .env"
"{t(l, \"dashboard.settings_hint\")}"
}
div { class: "settings-field",
label { "Ollama URL" }
label { "{t(l, \"dashboard.ollama_url\")}" }
input {
class: "settings-input",
r#type: "text",
placeholder: "Uses OLLAMA_URL from .env",
placeholder: "{t(l, \"dashboard.ollama_url_placeholder\")}",
value: "{settings_url}",
oninput: move |e| settings_url.set(e.value()),
}
}
div { class: "settings-field",
label { "Model" }
label { "{t(l, \"dashboard.model\")}" }
input {
class: "settings-input",
r#type: "text",
placeholder: "Uses OLLAMA_MODEL from .env",
placeholder: "{t(l, \"dashboard.model_placeholder\")}",
value: "{settings_model}",
oninput: move |e| settings_model.set(e.value()),
}
@@ -274,14 +278,14 @@ pub fn DashboardPage() -> Element {
*ollama_model.write() = settings_model.read().trim().to_string();
show_settings.set(false);
},
"Save"
"{t(l, \"common.save\")}"
}
}
}
// Loading / error state
if is_loading {
div { class: "dashboard-loading", "Searching..." }
div { class: "dashboard-loading", "{t(l, \"dashboard.searching\")}" }
}
if let Some(ref err) = search_error {
div { class: "settings-hint", "{err}" }

View File

@@ -1,23 +1,26 @@
use dioxus::prelude::*;
use crate::i18n::{t, Locale};
/// Agents page placeholder for the LangGraph agent builder.
///
/// Shows a "Coming Soon" card with a disabled launch button.
/// Will eventually integrate with the LangGraph framework.
#[component]
pub fn AgentsPage() -> Element {
let locale = use_context::<Signal<Locale>>();
let l = *locale.read();
rsx! {
section { class: "placeholder-page",
div { class: "placeholder-card",
div { class: "placeholder-icon", "A" }
h2 { "Agent Builder" }
h2 { "{t(l, \"developer.agents_title\")}" }
p { class: "placeholder-desc",
"Build and manage AI agents with LangGraph. \
Create multi-step reasoning pipelines, tool-using agents, \
and autonomous workflows."
"{t(l, \"developer.agents_desc\")}"
}
button { class: "btn-primary", disabled: true, "Launch Agent Builder" }
span { class: "placeholder-badge", "Coming Soon" }
button { class: "btn-primary", disabled: true, "{t(l, \"developer.launch_agents\")}" }
span { class: "placeholder-badge", "{t(l, \"common.coming_soon\")}" }
}
}
}

View File

@@ -1,5 +1,6 @@
use dioxus::prelude::*;
use crate::i18n::{t, Locale};
use crate::models::AnalyticsMetric;
/// Analytics page placeholder for LangFuse integration.
@@ -8,7 +9,10 @@ use crate::models::AnalyticsMetric;
/// plus a mock stats bar showing sample metrics.
#[component]
pub fn AnalyticsPage() -> Element {
let metrics = mock_metrics();
let locale = use_context::<Signal<Locale>>();
let l = *locale.read();
let metrics = mock_metrics(l);
rsx! {
section { class: "placeholder-page",
@@ -25,39 +29,41 @@ pub fn AnalyticsPage() -> Element {
}
div { class: "placeholder-card",
div { class: "placeholder-icon", "L" }
h2 { "Analytics & Observability" }
h2 { "{t(l, \"developer.analytics_title\")}" }
p { class: "placeholder-desc",
"Monitor and analyze your AI pipelines with LangFuse. \
Track token usage, latency, costs, and quality metrics \
across all your deployments."
"{t(l, \"developer.analytics_desc\")}"
}
button { class: "btn-primary", disabled: true, "Launch LangFuse" }
span { class: "placeholder-badge", "Coming Soon" }
button { class: "btn-primary", disabled: true, "{t(l, \"developer.launch_analytics\")}" }
span { class: "placeholder-badge", "{t(l, \"common.coming_soon\")}" }
}
}
}
}
/// Returns mock analytics metrics for the stats bar.
fn mock_metrics() -> Vec<AnalyticsMetric> {
///
/// # Arguments
///
/// * `locale` - The current locale for translating metric labels
fn mock_metrics(locale: Locale) -> Vec<AnalyticsMetric> {
vec![
AnalyticsMetric {
label: "Total Requests".into(),
label: t(locale, "developer.total_requests"),
value: "12,847".into(),
change_pct: 14.2,
},
AnalyticsMetric {
label: "Avg Latency".into(),
label: t(locale, "developer.avg_latency"),
value: "245ms".into(),
change_pct: -8.5,
},
AnalyticsMetric {
label: "Tokens Used".into(),
label: t(locale, "developer.tokens_used"),
value: "2.4M".into(),
change_pct: 22.1,
},
AnalyticsMetric {
label: "Error Rate".into(),
label: t(locale, "developer.error_rate"),
value: "0.3%".into(),
change_pct: -12.0,
},

View File

@@ -1,23 +1,26 @@
use dioxus::prelude::*;
use crate::i18n::{t, Locale};
/// Flow page placeholder for the LangFlow visual workflow builder.
///
/// Shows a "Coming Soon" card with a disabled launch button.
/// Will eventually integrate with LangFlow for visual flow design.
#[component]
pub fn FlowPage() -> Element {
let locale = use_context::<Signal<Locale>>();
let l = *locale.read();
rsx! {
section { class: "placeholder-page",
div { class: "placeholder-card",
div { class: "placeholder-icon", "F" }
h2 { "Flow Builder" }
h2 { "{t(l, \"developer.flow_title\")}" }
p { class: "placeholder-desc",
"Design visual AI workflows with LangFlow. \
Drag-and-drop nodes to create data processing pipelines, \
prompt chains, and integration flows."
"{t(l, \"developer.flow_desc\")}"
}
button { class: "btn-primary", disabled: true, "Launch Flow Builder" }
span { class: "placeholder-badge", "Coming Soon" }
button { class: "btn-primary", disabled: true, "{t(l, \"developer.launch_flow\")}" }
span { class: "placeholder-badge", "{t(l, \"common.coming_soon\")}" }
}
}
}

View File

@@ -10,6 +10,7 @@ use dioxus::prelude::*;
use crate::app::Route;
use crate::components::sub_nav::{SubNav, SubNavItem};
use crate::i18n::{t, Locale};
/// Shell layout for the Developer section.
///
@@ -17,17 +18,20 @@ use crate::components::sub_nav::{SubNav, SubNavItem};
/// the child route outlet. Sits inside the main `AppShell` layout.
#[component]
pub fn DeveloperShell() -> Element {
let locale = use_context::<Signal<Locale>>();
let l = *locale.read();
let tabs = vec![
SubNavItem {
label: "Agents",
label: t(l, "nav.agents"),
route: Route::AgentsPage {},
},
SubNavItem {
label: "Flow",
label: t(l, "nav.flow"),
route: Route::FlowPage {},
},
SubNavItem {
label: "Analytics",
label: t(l, "nav.analytics"),
route: Route::AnalyticsPage {},
},
];

View File

@@ -2,6 +2,7 @@ use dioxus::prelude::*;
use dioxus_free_icons::icons::bs_icons::BsShieldCheck;
use dioxus_free_icons::Icon;
use crate::i18n::{t, Locale};
use crate::Route;
/// Impressum (legal notice) page required by German/EU law.
@@ -10,6 +11,9 @@ use crate::Route;
/// accessible without authentication.
#[component]
pub fn ImpressumPage() -> Element {
let locale = use_context::<Signal<Locale>>();
let l = *locale.read();
rsx! {
div { class: "legal-page",
nav { class: "legal-nav",
@@ -21,53 +25,53 @@ pub fn ImpressumPage() -> Element {
}
}
main { class: "legal-content",
h1 { "Impressum" }
h1 { "{t(l, \"impressum.title\")}" }
h2 { "Information according to 5 TMG" }
h2 { "{t(l, \"impressum.info_tmg\")}" }
p {
"CERTifAI GmbH"
"{t(l, \"impressum.company\")}"
br {}
"Musterstrasse 1"
"{t(l, \"impressum.address_street\")}"
br {}
"10115 Berlin"
"{t(l, \"impressum.address_city\")}"
br {}
"Germany"
"{t(l, \"impressum.address_country\")}"
}
h2 { "Represented by" }
p { "Managing Director: [Name]" }
h2 { "{t(l, \"impressum.represented_by\")}" }
p { "{t(l, \"impressum.managing_director\")}" }
h2 { "Contact" }
h2 { "{t(l, \"impressum.contact\")}" }
p {
"Email: info@certifai.example"
"{t(l, \"impressum.email\")}"
br {}
"Phone: +49 (0) 30 1234567"
"{t(l, \"impressum.phone\")}"
}
h2 { "Commercial Register" }
h2 { "{t(l, \"impressum.commercial_register\")}" }
p {
"Registered at: Amtsgericht Berlin-Charlottenburg"
"{t(l, \"impressum.registered_at\")}"
br {}
"Registration number: HRB XXXXXX"
"{t(l, \"impressum.registration_number\")}"
}
h2 { "VAT ID" }
p { "VAT identification number according to 27a UStG: DE XXXXXXXXX" }
h2 { "{t(l, \"impressum.vat_id\")}" }
p { "{t(l, \"impressum.vat_number\")}" }
h2 { "Responsible for content according to 55 Abs. 2 RStV" }
h2 { "{t(l, \"impressum.responsible_content\")}" }
p {
"[Name]"
br {}
"CERTifAI GmbH"
"{t(l, \"impressum.company\")}"
br {}
"Musterstrasse 1"
"{t(l, \"impressum.address_street\")}"
br {}
"10115 Berlin"
"{t(l, \"impressum.address_city\")}"
}
}
footer { class: "legal-footer",
Link { to: Route::LandingPage {}, "Back to Home" }
Link { to: Route::PrivacyPage {}, "Privacy Policy" }
Link { to: Route::LandingPage {}, "{t(l, \"common.back_to_home\")}" }
Link { to: Route::PrivacyPage {}, "{t(l, \"common.privacy_policy\")}" }
}
}
}

View File

@@ -1,6 +1,7 @@
use dioxus::prelude::*;
use crate::components::{FileRow, PageHeader};
use crate::i18n::{t, Locale};
use crate::models::{FileKind, KnowledgeFile};
/// Knowledge Base page with file explorer table and upload controls.
@@ -9,6 +10,9 @@ use crate::models::{FileKind, KnowledgeFile};
/// metadata, chunk counts, and management actions.
#[component]
pub fn KnowledgePage() -> Element {
let locale = use_context::<Signal<Locale>>();
let l = *locale.read();
let mut files = use_signal(mock_files);
let mut search_query = use_signal(String::new);
@@ -29,17 +33,17 @@ pub fn KnowledgePage() -> Element {
rsx! {
section { class: "knowledge-page",
PageHeader {
title: "Knowledge Base".to_string(),
subtitle: "Manage documents for RAG retrieval".to_string(),
title: t(l, "knowledge.title"),
subtitle: t(l, "knowledge.subtitle"),
actions: rsx! {
button { class: "btn-primary", "Upload File" }
button { class: "btn-primary", {t(l, "common.upload_file")} }
},
}
div { class: "knowledge-toolbar",
input {
class: "form-input knowledge-search",
r#type: "text",
placeholder: "Search files...",
placeholder: t(l, "knowledge.search_placeholder"),
value: "{search_query}",
oninput: move |evt: Event<FormData>| {
search_query.set(evt.value());
@@ -50,12 +54,12 @@ pub fn KnowledgePage() -> Element {
table { class: "knowledge-table",
thead {
tr {
th { "Name" }
th { "Type" }
th { "Size" }
th { "Chunks" }
th { "Uploaded" }
th { "Actions" }
th { {t(l, "knowledge.name")} }
th { {t(l, "knowledge.type")} }
th { {t(l, "knowledge.size")} }
th { {t(l, "knowledge.chunks")} }
th { {t(l, "knowledge.uploaded")} }
th { {t(l, "knowledge.actions")} }
}
}
tbody {

View File

@@ -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")} }
}
}
}

View File

@@ -1,6 +1,7 @@
use dioxus::prelude::*;
use crate::components::{MemberRow, PageHeader};
use crate::i18n::{t, tw, Locale};
use crate::models::{BillingUsage, MemberRole, OrgMember};
/// Organization dashboard with billing stats, member table, and invite modal.
@@ -9,6 +10,9 @@ use crate::models::{BillingUsage, MemberRole, OrgMember};
/// with role management, and a button to invite new members.
#[component]
pub fn OrgDashboardPage() -> Element {
let locale = use_context::<Signal<Locale>>();
let l = *locale.read();
let members = use_signal(mock_members);
let usage = mock_usage();
let mut show_invite = use_signal(|| false);
@@ -23,10 +27,10 @@ pub fn OrgDashboardPage() -> Element {
rsx! {
section { class: "org-dashboard-page",
PageHeader {
title: "Organization".to_string(),
subtitle: "Manage members and billing".to_string(),
title: t(l, "org.title"),
subtitle: t(l, "org.subtitle"),
actions: rsx! {
button { class: "btn-primary", onclick: move |_| show_invite.set(true), "Invite Member" }
button { class: "btn-primary", onclick: move |_| show_invite.set(true), {t(l, "org.invite_member")} }
},
}
@@ -34,15 +38,15 @@ pub fn OrgDashboardPage() -> Element {
div { class: "org-stats-bar",
div { class: "org-stat",
span { class: "org-stat-value", "{usage.seats_used}/{usage.seats_total}" }
span { class: "org-stat-label", "Seats Used" }
span { class: "org-stat-label", {t(l, "org.seats_used")} }
}
div { class: "org-stat",
span { class: "org-stat-value", "{tokens_display}" }
span { class: "org-stat-label", "of {tokens_limit_display} tokens" }
span { class: "org-stat-label", {tw(l, "org.of_tokens", &[("limit", &tokens_limit_display)])} }
}
div { class: "org-stat",
span { class: "org-stat-value", "{usage.billing_cycle_end}" }
span { class: "org-stat-label", "Cycle Ends" }
span { class: "org-stat-label", {t(l, "org.cycle_ends")} }
}
}
@@ -51,10 +55,10 @@ pub fn OrgDashboardPage() -> Element {
table { class: "org-table",
thead {
tr {
th { "Name" }
th { "Email" }
th { "Role" }
th { "Joined" }
th { {t(l, "org.name")} }
th { {t(l, "org.email")} }
th { {t(l, "org.role")} }
th { {t(l, "org.joined")} }
}
}
tbody {
@@ -78,13 +82,13 @@ pub fn OrgDashboardPage() -> Element {
class: "modal-content",
// Prevent clicks inside modal from closing it
onclick: move |evt: Event<MouseData>| evt.stop_propagation(),
h3 { "Invite New Member" }
h3 { {t(l, "org.invite_title")} }
div { class: "form-group",
label { "Email Address" }
label { {t(l, "org.email_address")} }
input {
class: "form-input",
r#type: "email",
placeholder: "colleague@company.com",
placeholder: t(l, "org.email_placeholder"),
value: "{invite_email}",
oninput: move |evt: Event<FormData>| {
invite_email.set(evt.value());
@@ -95,12 +99,12 @@ pub fn OrgDashboardPage() -> Element {
button {
class: "btn-secondary",
onclick: move |_| show_invite.set(false),
"Cancel"
{t(l, "common.cancel")}
}
button {
class: "btn-primary",
onclick: move |_| show_invite.set(false),
"Send Invite"
{t(l, "org.send_invite")}
}
}
}

View File

@@ -8,6 +8,7 @@ use dioxus::prelude::*;
use crate::app::Route;
use crate::components::sub_nav::{SubNav, SubNavItem};
use crate::i18n::{t, Locale};
/// Shell layout for the Organization section.
///
@@ -15,13 +16,16 @@ use crate::components::sub_nav::{SubNav, SubNavItem};
/// the child route outlet. Sits inside the main `AppShell` layout.
#[component]
pub fn OrgShell() -> Element {
let locale = use_context::<Signal<Locale>>();
let l = *locale.read();
let tabs = vec![
SubNavItem {
label: "Pricing",
label: t(l, "nav.pricing"),
route: Route::OrgPricingPage {},
},
SubNavItem {
label: "Dashboard",
label: t(l, "nav.dashboard"),
route: Route::OrgDashboardPage {},
},
];

View File

@@ -2,6 +2,7 @@ use dioxus::prelude::*;
use crate::app::Route;
use crate::components::{PageHeader, PricingCard};
use crate::i18n::{t, tw, Locale};
use crate::models::PricingPlan;
/// Organization pricing page displaying three plan tiers.
@@ -10,14 +11,17 @@ use crate::models::PricingPlan;
/// organization dashboard.
#[component]
pub fn OrgPricingPage() -> Element {
let locale = use_context::<Signal<Locale>>();
let l = *locale.read();
let navigator = use_navigator();
let plans = mock_plans();
let plans = mock_plans(l);
rsx! {
section { class: "pricing-page",
PageHeader {
title: "Pricing".to_string(),
subtitle: "Choose the plan that fits your organization".to_string(),
title: t(l, "org.pricing_title"),
subtitle: t(l, "org.pricing_subtitle"),
}
div { class: "pricing-grid",
for plan in plans {
@@ -34,52 +38,56 @@ pub fn OrgPricingPage() -> Element {
}
}
/// Returns mock pricing plans.
fn mock_plans() -> Vec<PricingPlan> {
/// Returns mock pricing plans with translated names and features.
///
/// # Arguments
///
/// * `l` - The active locale for translating user-facing plan text
fn mock_plans(l: Locale) -> Vec<PricingPlan> {
vec![
PricingPlan {
id: "starter".into(),
name: "Starter".into(),
name: t(l, "pricing.starter"),
price_eur: 49,
features: vec![
"Up to 5 users".into(),
"1 LLM provider".into(),
"100K tokens/month".into(),
"Community support".into(),
"Basic analytics".into(),
tw(l, "pricing.up_to_users", &[("n", "5")]),
t(l, "pricing.llm_provider_1"),
t(l, "pricing.tokens_100k"),
t(l, "pricing.community_support"),
t(l, "pricing.basic_analytics"),
],
highlighted: false,
max_seats: Some(5),
},
PricingPlan {
id: "team".into(),
name: "Team".into(),
name: t(l, "pricing.team"),
price_eur: 199,
features: vec![
"Up to 25 users".into(),
"All LLM providers".into(),
"1M tokens/month".into(),
"Priority support".into(),
"Advanced analytics".into(),
"Custom MCP tools".into(),
"SSO integration".into(),
tw(l, "pricing.up_to_users", &[("n", "25")]),
t(l, "pricing.all_providers"),
t(l, "pricing.tokens_1m"),
t(l, "pricing.priority_support"),
t(l, "pricing.advanced_analytics"),
t(l, "pricing.custom_mcp"),
t(l, "pricing.sso"),
],
highlighted: true,
max_seats: Some(25),
},
PricingPlan {
id: "enterprise".into(),
name: "Enterprise".into(),
name: t(l, "pricing.enterprise"),
price_eur: 499,
features: vec![
"Unlimited users".into(),
"All LLM providers".into(),
"Unlimited tokens".into(),
"Dedicated support".into(),
"Full observability".into(),
"Custom integrations".into(),
"SLA guarantee".into(),
"On-premise deployment".into(),
t(l, "pricing.unlimited_users"),
t(l, "pricing.all_providers"),
t(l, "pricing.unlimited_tokens"),
t(l, "pricing.dedicated_support"),
t(l, "pricing.full_observability"),
t(l, "pricing.custom_integrations"),
t(l, "pricing.sla"),
t(l, "pricing.on_premise"),
],
highlighted: false,
max_seats: None,

View File

@@ -2,6 +2,7 @@ use dioxus::prelude::*;
use dioxus_free_icons::icons::bs_icons::BsShieldCheck;
use dioxus_free_icons::Icon;
use crate::i18n::{t, Locale};
use crate::Route;
/// Privacy Policy page.
@@ -10,6 +11,9 @@ use crate::Route;
/// without authentication.
#[component]
pub fn PrivacyPage() -> Element {
let locale = use_context::<Signal<Locale>>();
let l = *locale.read();
rsx! {
div { class: "legal-page",
nav { class: "legal-nav",
@@ -21,85 +25,66 @@ pub fn PrivacyPage() -> Element {
}
}
main { class: "legal-content",
h1 { "Privacy Policy" }
p { class: "legal-updated", "Last updated: February 2026" }
h1 { "{t(l, \"privacy.title\")}" }
p { class: "legal-updated", "{t(l, \"privacy.last_updated\")}" }
h2 { "1. Introduction" }
p {
"CERTifAI GmbH (\"we\", \"our\", \"us\") is committed to "
"protecting your personal data. This privacy policy explains "
"how we collect, use, and safeguard your information when you "
"use our platform."
}
h2 { "{t(l, \"privacy.intro_title\")}" }
p { "{t(l, \"privacy.intro_text\")}" }
h2 { "2. Data Controller" }
h2 { "{t(l, \"privacy.controller_title\")}" }
p {
"CERTifAI GmbH"
"{t(l, \"impressum.company\")}"
br {}
"Musterstrasse 1, 10115 Berlin, Germany"
"{t(l, \"privacy.controller_address\")}"
br {}
"Email: privacy@certifai.example"
"{t(l, \"privacy.controller_email\")}"
}
h2 { "3. Data We Collect" }
p {
"We collect only the minimum data necessary to provide "
"our services:"
}
h2 { "{t(l, \"privacy.data_title\")}" }
p { "{t(l, \"privacy.data_intro\")}" }
ul {
li {
strong { "Account data: " }
"Name, email address, and organization details "
"provided during registration."
strong { "{t(l, \"privacy.data_account_label\")}" }
"{t(l, \"privacy.data_account_text\")}"
}
li {
strong { "Usage data: " }
"API call logs, token counts, and feature usage "
"metrics for billing and analytics."
strong { "{t(l, \"privacy.data_usage_label\")}" }
"{t(l, \"privacy.data_usage_text\")}"
}
li {
strong { "Technical data: " }
"IP addresses, browser type, and session identifiers "
"for security and platform stability."
strong { "{t(l, \"privacy.data_technical_label\")}" }
"{t(l, \"privacy.data_technical_text\")}"
}
}
h2 { "4. How We Use Your Data" }
h2 { "{t(l, \"privacy.use_title\")}" }
ul {
li { "To provide and maintain the CERTifAI platform" }
li { "To manage your account and subscription" }
li { "To communicate service updates and security notices" }
li { "To comply with legal obligations" }
li { "{t(l, \"privacy.use_1\")}" }
li { "{t(l, \"privacy.use_2\")}" }
li { "{t(l, \"privacy.use_3\")}" }
li { "{t(l, \"privacy.use_4\")}" }
}
h2 { "5. Data Storage and Sovereignty" }
p {
"CERTifAI is a self-hosted platform. All AI workloads, "
"model data, and inference results remain entirely within "
"your own infrastructure. We do not access, store, or "
"process your AI data on our servers."
}
h2 { "{t(l, \"privacy.storage_title\")}" }
p { "{t(l, \"privacy.storage_text\")}" }
h2 { "6. Your Rights (GDPR)" }
p { "Under the GDPR, you have the right to:" }
h2 { "{t(l, \"privacy.rights_title\")}" }
p { "{t(l, \"privacy.rights_intro\")}" }
ul {
li { "Access your personal data" }
li { "Rectify inaccurate data" }
li { "Request erasure of your data" }
li { "Restrict or object to processing" }
li { "Data portability" }
li { "Lodge a complaint with a supervisory authority" }
li { "{t(l, \"privacy.rights_access\")}" }
li { "{t(l, \"privacy.rights_rectify\")}" }
li { "{t(l, \"privacy.rights_erasure\")}" }
li { "{t(l, \"privacy.rights_restrict\")}" }
li { "{t(l, \"privacy.rights_portability\")}" }
li { "{t(l, \"privacy.rights_complaint\")}" }
}
h2 { "7. Contact" }
p {
"For privacy-related inquiries, contact us at "
"privacy@certifai.example."
}
h2 { "{t(l, \"privacy.contact_title\")}" }
p { "{t(l, \"privacy.contact_text\")}" }
}
footer { class: "legal-footer",
Link { to: Route::LandingPage {}, "Back to Home" }
Link { to: Route::ImpressumPage {}, "Impressum" }
Link { to: Route::LandingPage {}, "{t(l, \"common.back_to_home\")}" }
Link { to: Route::ImpressumPage {}, "{t(l, \"common.impressum\")}" }
}
}
}

View File

@@ -1,6 +1,7 @@
use dioxus::prelude::*;
use crate::components::PageHeader;
use crate::i18n::{t, Locale};
use crate::models::{EmbeddingEntry, LlmProvider, ModelEntry, ProviderConfig};
/// Providers page for configuring LLM and embedding model backends.
@@ -9,6 +10,9 @@ use crate::models::{EmbeddingEntry, LlmProvider, ModelEntry, ProviderConfig};
/// shows the currently active provider status.
#[component]
pub fn ProvidersPage() -> Element {
let locale = use_context::<Signal<Locale>>();
let l = *locale.read();
let mut selected_provider = use_signal(|| LlmProvider::Ollama);
let mut selected_model = use_signal(|| "llama3.1:8b".to_string());
let mut selected_embedding = use_signal(|| "nomic-embed-text".to_string());
@@ -39,13 +43,13 @@ pub fn ProvidersPage() -> Element {
rsx! {
section { class: "providers-page",
PageHeader {
title: "Providers".to_string(),
subtitle: "Configure your LLM and embedding backends".to_string(),
title: t(l, "providers.title"),
subtitle: t(l, "providers.subtitle"),
}
div { class: "providers-layout",
div { class: "providers-form",
div { class: "form-group",
label { "Provider" }
label { "{t(l, \"providers.provider\")}" }
select {
class: "form-select",
value: "{provider_val.label()}",
@@ -67,7 +71,7 @@ pub fn ProvidersPage() -> Element {
}
}
div { class: "form-group",
label { "Model" }
label { "{t(l, \"providers.model\")}" }
select {
class: "form-select",
value: "{selected_model}",
@@ -81,7 +85,7 @@ pub fn ProvidersPage() -> Element {
}
}
div { class: "form-group",
label { "Embedding Model" }
label { "{t(l, \"providers.embedding_model\")}" }
select {
class: "form-select",
value: "{selected_embedding}",
@@ -95,11 +99,11 @@ pub fn ProvidersPage() -> Element {
}
}
div { class: "form-group",
label { "API Key" }
label { "{t(l, \"providers.api_key\")}" }
input {
class: "form-input",
r#type: "password",
placeholder: "Enter API key...",
placeholder: "{t(l, \"providers.api_key_placeholder\")}",
value: "{api_key}",
oninput: move |evt: Event<FormData>| {
api_key.set(evt.value());
@@ -110,34 +114,34 @@ pub fn ProvidersPage() -> Element {
button {
class: "btn-primary",
onclick: move |_| saved.set(true),
"Save Configuration"
"{t(l, \"providers.save_config\")}"
}
if *saved.read() {
p { class: "form-success", "Configuration saved." }
p { class: "form-success", "{t(l, \"providers.config_saved\")}" }
}
}
div { class: "providers-status",
h3 { "Active Configuration" }
h3 { "{t(l, \"providers.active_config\")}" }
div { class: "status-card",
div { class: "status-row",
span { class: "status-label", "Provider" }
span { class: "status-label", "{t(l, \"providers.provider\")}" }
span { class: "status-value", "{active_config.provider.label()}" }
}
div { class: "status-row",
span { class: "status-label", "Model" }
span { class: "status-label", "{t(l, \"providers.model\")}" }
span { class: "status-value", "{active_config.selected_model}" }
}
div { class: "status-row",
span { class: "status-label", "Embedding" }
span { class: "status-label", "{t(l, \"providers.embedding\")}" }
span { class: "status-value", "{active_config.selected_embedding}" }
}
div { class: "status-row",
span { class: "status-label", "API Key" }
span { class: "status-label", "{t(l, \"providers.api_key\")}" }
span { class: "status-value",
if active_config.api_key_set {
"Set"
"{t(l, \"common.set\")}"
} else {
"Not set"
"{t(l, \"common.not_set\")}"
}
}
}

View File

@@ -1,6 +1,9 @@
use std::collections::HashMap;
use dioxus::prelude::*;
use crate::components::{PageHeader, ToolCard};
use crate::i18n::{t, Locale};
use crate::models::{McpTool, ToolCategory, ToolStatus};
/// Tools page displaying a grid of MCP tool cards with toggle switches.
@@ -9,24 +12,50 @@ use crate::models::{McpTool, ToolCategory, ToolStatus};
/// enabling/disabling them via toggle buttons.
#[component]
pub fn ToolsPage() -> Element {
let mut tools = use_signal(mock_tools);
let locale = use_context::<Signal<Locale>>();
let l = *locale.read();
// Toggle a tool's enabled state by its ID
let on_toggle = move |id: String| {
tools.write().iter_mut().for_each(|t| {
if t.id == id {
t.enabled = !t.enabled;
// Track which tool IDs have been toggled off/on by the user.
// The canonical tool definitions (including translated names) come
// from `mock_tools(l)` on every render so they react to locale changes.
let mut enabled_overrides = use_signal(HashMap::<String, bool>::new);
// Build the display list: translated names from mock_tools, with
// enabled state merged from user overrides.
let tool_list: Vec<McpTool> = mock_tools(l)
.into_iter()
.map(|mut tool| {
if let Some(&enabled) = enabled_overrides.read().get(&tool.id) {
tool.enabled = enabled;
}
});
};
tool
})
.collect();
let tool_list = tools.read().clone();
// Toggle a tool's enabled state by its ID.
// Reads the current state from overrides (or falls back to the default
// enabled value from mock_tools) and flips it.
let on_toggle = move |id: String| {
let defaults = mock_tools(l);
let current = enabled_overrides
.read()
.get(&id)
.copied()
.unwrap_or_else(|| {
defaults
.iter()
.find(|tool| tool.id == id)
.map(|tool| tool.enabled)
.unwrap_or(false)
});
enabled_overrides.write().insert(id, !current);
};
rsx! {
section { class: "tools-page",
PageHeader {
title: "Tools".to_string(),
subtitle: "Manage MCP servers and tool integrations".to_string(),
title: t(l, "tools.title"),
subtitle: t(l, "tools.subtitle"),
}
div { class: "tools-grid",
for tool in tool_list {
@@ -37,13 +66,17 @@ pub fn ToolsPage() -> Element {
}
}
/// Returns mock MCP tools for the tools grid.
fn mock_tools() -> Vec<McpTool> {
/// Returns mock MCP tools for the tools grid with translated names.
///
/// # Arguments
///
/// * `l` - The current locale for translating tool names and descriptions
fn mock_tools(l: Locale) -> Vec<McpTool> {
vec![
McpTool {
id: "calculator".into(),
name: "Calculator".into(),
description: "Mathematical computation and unit conversion".into(),
name: t(l, "tools.calculator"),
description: t(l, "tools.calculator_desc"),
category: ToolCategory::Compute,
status: ToolStatus::Active,
enabled: true,
@@ -51,8 +84,8 @@ fn mock_tools() -> Vec<McpTool> {
},
McpTool {
id: "tavily".into(),
name: "Tavily Search".into(),
description: "AI-optimized web search API for real-time information".into(),
name: t(l, "tools.tavily"),
description: t(l, "tools.tavily_desc"),
category: ToolCategory::Search,
status: ToolStatus::Active,
enabled: true,
@@ -60,8 +93,8 @@ fn mock_tools() -> Vec<McpTool> {
},
McpTool {
id: "searxng".into(),
name: "SearXNG".into(),
description: "Privacy-respecting metasearch engine".into(),
name: t(l, "tools.searxng"),
description: t(l, "tools.searxng_desc"),
category: ToolCategory::Search,
status: ToolStatus::Active,
enabled: true,
@@ -69,8 +102,8 @@ fn mock_tools() -> Vec<McpTool> {
},
McpTool {
id: "file-reader".into(),
name: "File Reader".into(),
description: "Read and parse local files in various formats".into(),
name: t(l, "tools.file_reader"),
description: t(l, "tools.file_reader_desc"),
category: ToolCategory::FileSystem,
status: ToolStatus::Active,
enabled: true,
@@ -78,8 +111,8 @@ fn mock_tools() -> Vec<McpTool> {
},
McpTool {
id: "code-exec".into(),
name: "Code Executor".into(),
description: "Sandboxed code execution for Python and JavaScript".into(),
name: t(l, "tools.code_executor"),
description: t(l, "tools.code_executor_desc"),
category: ToolCategory::Code,
status: ToolStatus::Inactive,
enabled: false,
@@ -87,8 +120,8 @@ fn mock_tools() -> Vec<McpTool> {
},
McpTool {
id: "web-scraper".into(),
name: "Web Scraper".into(),
description: "Extract structured data from web pages".into(),
name: t(l, "tools.web_scraper"),
description: t(l, "tools.web_scraper_desc"),
category: ToolCategory::Search,
status: ToolStatus::Active,
enabled: true,
@@ -96,8 +129,8 @@ fn mock_tools() -> Vec<McpTool> {
},
McpTool {
id: "email".into(),
name: "Email Sender".into(),
description: "Send emails via configured SMTP server".into(),
name: t(l, "tools.email_sender"),
description: t(l, "tools.email_sender_desc"),
category: ToolCategory::Communication,
status: ToolStatus::Inactive,
enabled: false,
@@ -105,8 +138,8 @@ fn mock_tools() -> Vec<McpTool> {
},
McpTool {
id: "git".into(),
name: "Git Operations".into(),
description: "Interact with Git repositories for version control".into(),
name: t(l, "tools.git_ops"),
description: t(l, "tools.git_ops_desc"),
category: ToolCategory::Code,
status: ToolStatus::Active,
enabled: true,