Skip to content

Commit 83348bf

Browse files
committed
feat(i18n): complete internationalization for API Keys and Administration pages
1 parent 5ca9f9a commit 83348bf

7 files changed

Lines changed: 306 additions & 92 deletions

File tree

rustmail_panel/src/components/api_keys.rs

Lines changed: 39 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::components::forbidden::Forbidden403;
22
use crate::types::PanelPermission;
33
use gloo_net::http::Request;
4+
use i18nrs::yew::use_translation;
45
use serde::{Deserialize, Serialize};
56
use wasm_bindgen_futures::spawn_local;
67
use web_sys::HtmlInputElement;
@@ -75,6 +76,7 @@ pub struct CreateApiKeyResponse {
7576

7677
#[function_component(ApiKeysPage)]
7778
pub fn api_keys_page() -> Html {
79+
let (i18n, _set_language) = use_translation();
7880
let api_keys = use_state(|| Vec::<ApiKeyListItem>::new());
7981
let loading = use_state(|| true);
8082
let error = use_state(|| None::<String>);
@@ -99,13 +101,13 @@ pub fn api_keys_page() -> Html {
99101
if let Some(perms) = (*permissions).as_ref() {
100102
if !perms.contains(&PanelPermission::ManageApiKeys) {
101103
return html! {
102-
<Forbidden403 required_permission="Gérer les clés API" />
104+
<Forbidden403 required_permission={i18n.t("navbar.apikeys")} />
103105
};
104106
}
105107
} else {
106108
return html! {
107109
<div class="flex items-center justify-center min-h-[70vh]">
108-
<div class="text-gray-400 animate-pulse">{"Vérification des permissions..."}</div>
110+
<div class="text-gray-400 animate-pulse">{i18n.t("panel.forbidden.checking_permissions")}</div>
109111
</div>
110112
};
111113
}
@@ -114,6 +116,7 @@ pub fn api_keys_page() -> Html {
114116
let api_keys = api_keys.clone();
115117
let loading = loading.clone();
116118
let error = error.clone();
119+
let i18n_clone = i18n.clone();
117120

118121
use_effect_with((), move |_| {
119122
spawn_local(async move {
@@ -125,14 +128,14 @@ pub fn api_keys_page() -> Html {
125128
api_keys.set(keys);
126129
error.set(None);
127130
} else {
128-
error.set(Some("Failed to parse API keys".to_string()));
131+
error.set(Some(i18n_clone.t("panel.apikeys.error_parse")));
129132
}
130133
} else {
131-
error.set(Some(format!("Failed to load API keys: {}", resp.status())));
134+
error.set(Some(format!("{}: {}", i18n_clone.t("panel.apikeys.error_load"), resp.status())));
132135
}
133136
}
134137
Err(e) => {
135-
error.set(Some(format!("Network error: {:?}", e)));
138+
error.set(Some(format!("{}: {:?}", i18n_clone.t("panel.apikeys.error_network"), e)));
136139
}
137140
}
138141
loading.set(false);
@@ -199,20 +202,20 @@ pub fn api_keys_page() -> Html {
199202
html! {
200203
<div class="space-y-6">
201204
<div class="flex justify-between items-center">
202-
<h1 class="text-3xl font-bold text-white">{"API Keys"}</h1>
205+
<h1 class="text-3xl font-bold text-white">{i18n.t("panel.apikeys.title")}</h1>
203206
<button
204207
onclick={on_create_click}
205208
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition"
206209
>
207-
{"+ Create API Key"}
210+
{i18n.t("panel.apikeys.create")}
208211
</button>
209212
</div>
210213

211214
{
212215
if *loading {
213216
html! {
214217
<div class="text-center text-gray-400 py-8">
215-
<p class="animate-pulse">{"Loading..."}</p>
218+
<p class="animate-pulse">{i18n.t("panel.apikeys.loading")}</p>
216219
</div>
217220
}
218221
} else if let Some(err) = (*error).clone() {
@@ -224,7 +227,7 @@ pub fn api_keys_page() -> Html {
224227
} else if api_keys.is_empty() {
225228
html! {
226229
<div class="bg-slate-800 rounded-lg p-8 text-center">
227-
<p class="text-gray-400">{"No API keys found. Create your first one!"}</p>
230+
<p class="text-gray-400">{i18n.t("panel.apikeys.no_keys")}</p>
228231
</div>
229232
}
230233
} else {
@@ -273,16 +276,17 @@ pub struct ApiKeyCardProps {
273276

274277
#[function_component(ApiKeyCard)]
275278
fn api_key_card(props: &ApiKeyCardProps) -> Html {
279+
let (i18n, _set_language) = use_translation();
276280
let key = &props.api_key;
277281
let created_date = chrono::DateTime::from_timestamp(key.created_at, 0)
278282
.map(|dt| dt.format("%Y-%m-%d %H:%M").to_string())
279-
.unwrap_or_else(|| "Unknown".to_string());
283+
.unwrap_or_else(|| i18n.t("panel.apikeys.unknown"));
280284

281285
let last_used = key
282286
.last_used_at
283287
.and_then(|ts| chrono::DateTime::from_timestamp(ts, 0))
284288
.map(|dt| dt.format("%Y-%m-%d %H:%M").to_string())
285-
.unwrap_or_else(|| "Never".to_string());
289+
.unwrap_or_else(|| i18n.t("panel.apikeys.never"));
286290

287291
html! {
288292
<div class="bg-slate-800 rounded-lg p-6 border border-slate-700">
@@ -297,20 +301,20 @@ fn api_key_card(props: &ApiKeyCardProps) -> Html {
297301
html! {
298302
<>
299303
<span class="px-3 py-1 bg-green-900/30 border border-green-500 text-green-200 rounded-full text-sm">
300-
{"Active"}
304+
{i18n.t("panel.apikeys.active")}
301305
</span>
302306
<button
303307
onclick={props.on_revoke.reform(|_| ())}
304308
class="px-3 py-1 bg-red-900/30 border border-red-500 text-red-200 hover:bg-red-900/50 rounded-md text-sm transition"
305309
>
306-
{"Revoke"}
310+
{i18n.t("panel.apikeys.revoke")}
307311
</button>
308312
</>
309313
}
310314
} else {
311315
html! {
312316
<span class="px-3 py-1 bg-gray-900/30 border border-gray-500 text-gray-400 rounded-full text-sm">
313-
{"Revoked"}
317+
{i18n.t("panel.apikeys.revoked")}
314318
</span>
315319
}
316320
}
@@ -330,8 +334,8 @@ fn api_key_card(props: &ApiKeyCardProps) -> Html {
330334
}).collect::<Html>()
331335
}
332336
</div>
333-
<p class="text-gray-400">{"Created: "}{created_date}</p>
334-
<p class="text-gray-400">{"Last used: "}{last_used}</p>
337+
<p class="text-gray-400">{i18n.t("panel.apikeys.created")}{" "}{created_date}</p>
338+
<p class="text-gray-400">{i18n.t("panel.apikeys.last_used")}{" "}{last_used}</p>
335339
</div>
336340
</div>
337341
}
@@ -346,6 +350,7 @@ pub struct CreateApiKeyModalProps {
346350

347351
#[function_component(CreateApiKeyModal)]
348352
fn create_api_key_modal(props: &CreateApiKeyModalProps) -> Html {
353+
let (i18n, _set_language) = use_translation();
349354
let name_ref = use_node_ref();
350355
let selected_permissions = use_state(|| Vec::<Permission>::new());
351356
let creating = use_state(|| false);
@@ -370,6 +375,7 @@ fn create_api_key_modal(props: &CreateApiKeyModalProps) -> Html {
370375
let creating = creating.clone();
371376
let error = error.clone();
372377
let on_created = props.on_created.clone();
378+
let i18n_clone = i18n.clone();
373379

374380
Callback::from(move |_| {
375381
let name = name_ref
@@ -378,12 +384,12 @@ fn create_api_key_modal(props: &CreateApiKeyModalProps) -> Html {
378384
.unwrap_or_default();
379385

380386
if name.trim().is_empty() {
381-
error.set(Some("Name is required".to_string()));
387+
error.set(Some(i18n_clone.t("panel.apikeys.modal.error_name_required")));
382388
return;
383389
}
384390

385391
if selected_permissions.is_empty() {
386-
error.set(Some("At least one permission is required".to_string()));
392+
error.set(Some(i18n_clone.t("panel.apikeys.modal.error_permission_required")));
387393
return;
388394
}
389395

@@ -396,6 +402,7 @@ fn create_api_key_modal(props: &CreateApiKeyModalProps) -> Html {
396402
let creating = creating.clone();
397403
let error = error.clone();
398404
let on_created = on_created.clone();
405+
let i18n_clone2 = i18n_clone.clone();
399406

400407
creating.set(true);
401408
spawn_local(async move {
@@ -411,14 +418,14 @@ fn create_api_key_modal(props: &CreateApiKeyModalProps) -> Html {
411418
on_created.emit(response.api_key);
412419
error.set(None);
413420
} else {
414-
error.set(Some("Failed to parse response".to_string()));
421+
error.set(Some(i18n_clone2.t("panel.apikeys.modal.error_parse_response")));
415422
}
416423
} else {
417-
error.set(Some(format!("Failed to create key: {}", resp.status())));
424+
error.set(Some(format!("{}: {}", i18n_clone2.t("panel.apikeys.modal.error_create"), resp.status())));
418425
}
419426
}
420427
Err(e) => {
421-
error.set(Some(format!("Network error: {:?}", e)));
428+
error.set(Some(format!("{}: {:?}", i18n_clone2.t("panel.apikeys.error_network"), e)));
422429
}
423430
}
424431
creating.set(false);
@@ -434,27 +441,27 @@ fn create_api_key_modal(props: &CreateApiKeyModalProps) -> Html {
434441
if let Some(key) = &props.created_key {
435442
html! {
436443
<>
437-
<h2 class="text-2xl font-bold text-white">{"API Key Created!"}</h2>
444+
<h2 class="text-2xl font-bold text-white">{i18n.t("panel.apikeys.modal.title_created")}</h2>
438445
<div class="bg-yellow-900/20 border border-yellow-500 text-yellow-200 p-4 rounded-md">
439-
<p class="font-semibold mb-2">{"⚠️ Important: Save this key now!"}</p>
440-
<p class="text-sm">{"You won't be able to see it again."}</p>
446+
<p class="font-semibold mb-2">{i18n.t("panel.apikeys.modal.warning_title")}</p>
447+
<p class="text-sm">{i18n.t("panel.apikeys.modal.warning_message")}</p>
441448
</div>
442449
<div class="bg-slate-900 p-4 rounded-md">
443-
<p class="text-xs text-gray-400 mb-2">{"Your API Key:"}</p>
450+
<p class="text-xs text-gray-400 mb-2">{i18n.t("panel.apikeys.modal.your_key")}</p>
444451
<code class="text-green-400 font-mono text-sm break-all">{key}</code>
445452
</div>
446453
<button
447454
onclick={props.on_close.reform(|_| ())}
448455
class="w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition"
449456
>
450-
{"Close"}
457+
{i18n.t("panel.apikeys.modal.close")}
451458
</button>
452459
</>
453460
}
454461
} else {
455462
html! {
456463
<>
457-
<h2 class="text-2xl font-bold text-white">{"Create API Key"}</h2>
464+
<h2 class="text-2xl font-bold text-white">{i18n.t("panel.apikeys.modal.title_create")}</h2>
458465

459466
{
460467
if let Some(err) = (*error).clone() {
@@ -469,17 +476,17 @@ fn create_api_key_modal(props: &CreateApiKeyModalProps) -> Html {
469476
}
470477

471478
<div>
472-
<label class="block text-sm font-medium text-gray-300 mb-2">{"Name"}</label>
479+
<label class="block text-sm font-medium text-gray-300 mb-2">{i18n.t("panel.apikeys.modal.name")}</label>
473480
<input
474481
ref={name_ref}
475482
type="text"
476-
placeholder="My API Key"
483+
placeholder={i18n.t("panel.apikeys.modal.name_placeholder")}
477484
class="w-full px-4 py-2 bg-slate-900 border border-slate-700 rounded-md text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
478485
/>
479486
</div>
480487

481488
<div>
482-
<label class="block text-sm font-medium text-gray-300 mb-2">{"Permissions"}</label>
489+
<label class="block text-sm font-medium text-gray-300 mb-2">{i18n.t("panel.apikeys.modal.permissions")}</label>
483490
<div class="space-y-2">
484491
{
485492
Permission::all().iter().map(|perm| {
@@ -508,14 +515,14 @@ fn create_api_key_modal(props: &CreateApiKeyModalProps) -> Html {
508515
disabled={*creating}
509516
class="flex-1 px-4 py-2 bg-slate-700 hover:bg-slate-600 text-white rounded-md transition disabled:opacity-50"
510517
>
511-
{"Cancel"}
518+
{i18n.t("panel.apikeys.modal.cancel")}
512519
</button>
513520
<button
514521
onclick={on_create}
515522
disabled={*creating}
516523
class="flex-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition disabled:opacity-50"
517524
>
518-
{if *creating { "Creating..." } else { "Create" }}
525+
{if *creating { i18n.t("panel.apikeys.modal.creating") } else { i18n.t("panel.apikeys.modal.create_button") }}
519526
</button>
520527
</div>
521528
</>

rustmail_panel/src/components/configuration.rs

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,13 @@ pub fn configuration_page() -> Html {
3838
if let Some(perms) = (*permissions).as_ref() {
3939
if !perms.contains(&PanelPermission::ManageConfig) {
4040
return html! {
41-
<Forbidden403 required_permission="Gérer la configuration" />
41+
<Forbidden403 required_permission={i18n.t("panel.configuration.title")} />
4242
};
4343
}
4444
} else {
4545
return html! {
4646
<div class="flex items-center justify-center min-h-[70vh]">
47-
<div class="text-gray-400 animate-pulse">{"Vérification des permissions..."}</div>
47+
<div class="text-gray-400 animate-pulse">{i18n.t("panel.forbidden.checking_permissions")}</div>
4848
</div>
4949
};
5050
}
@@ -879,6 +879,58 @@ fn bot_section(props: &BotSectionProps) -> Html {
879879
})
880880
}}
881881
/>
882+
883+
<div>
884+
<label class="block text-sm text-gray-300 mb-2">{i18n.t("panel.configuration.bot.super_admin_users")}</label>
885+
<input
886+
type="text"
887+
value={config.bot.panel_super_admin_users.iter().map(|id| id.to_string()).collect::<Vec<_>>().join(", ")}
888+
placeholder="123456789, 987654321"
889+
oninput={{
890+
let config = config.clone();
891+
move |e: InputEvent| {
892+
if let Some(input) = e.target_dyn_into::<web_sys::HtmlInputElement>() {
893+
let mut cfg = (*config).clone();
894+
cfg.bot.panel_super_admin_users = input.value()
895+
.split(',')
896+
.map(|s| s.trim())
897+
.filter(|s| !s.is_empty())
898+
.filter_map(|s| s.parse::<u64>().ok())
899+
.collect();
900+
config.set(cfg);
901+
}
902+
}
903+
}}
904+
class="w-full px-4 py-2 bg-slate-900/50 border border-slate-600 rounded-md text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
905+
/>
906+
<p class="mt-1 text-xs text-gray-500">{i18n.t("panel.configuration.bot.super_admin_users_help")}</p>
907+
</div>
908+
909+
<div>
910+
<label class="block text-sm text-gray-300 mb-2">{i18n.t("panel.configuration.bot.super_admin_roles")}</label>
911+
<input
912+
type="text"
913+
value={config.bot.panel_super_admin_roles.iter().map(|id| id.to_string()).collect::<Vec<_>>().join(", ")}
914+
placeholder="123456789, 987654321"
915+
oninput={{
916+
let config = config.clone();
917+
move |e: InputEvent| {
918+
if let Some(input) = e.target_dyn_into::<web_sys::HtmlInputElement>() {
919+
let mut cfg = (*config).clone();
920+
cfg.bot.panel_super_admin_roles = input.value()
921+
.split(',')
922+
.map(|s| s.trim())
923+
.filter(|s| !s.is_empty())
924+
.filter_map(|s| s.parse::<u64>().ok())
925+
.collect();
926+
config.set(cfg);
927+
}
928+
}
929+
}}
930+
class="w-full px-4 py-2 bg-slate-900/50 border border-slate-600 rounded-md text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
931+
/>
932+
<p class="mt-1 text-xs text-gray-500">{i18n.t("panel.configuration.bot.super_admin_roles_help")}</p>
933+
</div>
882934
</div>
883935
}
884936
}

0 commit comments

Comments
 (0)