Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: i18n system prototype #2107

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
308 changes: 297 additions & 11 deletions Cargo.lock

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions server/web_ui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ yew-router = { workspace = true }
time = { workspace = true }
gloo-timers = "0.3.0"
wasm-timer = "0.2.5"
i18n-embed = { version = "0.14.0", features = ["fluent-system", "web-sys-requester"]}
i18n-embed-fl = "0.7.0"
rust-embed = "8"
unic-langid = "0.9.1"
fluent = "0.16.0"
fluent-bundle = "0.15.2"
fluent-fallback = "0.7.0"
regex.workspace = true
lazy_static.workspace = true

Expand Down
4 changes: 4 additions & 0 deletions server/web_ui/i18n.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
fallback_language = "en-US"

[fluent]
assets_dir = "i18n"
13 changes: 13 additions & 0 deletions server/web_ui/i18n/en-US/kanidmd_web_ui.ftl
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
page-not-found = 404 — Page Not Found
goto-home = Home
breadcrumb-admin = Admin
breadcrumb-admin-accounts = Accounts
header-account-admin = Account Administration
accounts-list-load-waiting = Waiting on the accounts list to load...
th-accounts-display-name = Display Name
th-accounts-username = Username
th-accounts-description = Description
account-type-service = Service Account
account-type-person = Person
alert-failed-to-query-accounts = Failed to query accounts
alert-unauthorized = You're not authorized to see this page
28 changes: 16 additions & 12 deletions server/web_ui/pkg/kanidmd_web_ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ function makeMutClosure(arg0, arg1, dtor, f) {
return real;
}
function __wbg_adapter_48(arg0, arg1) {
wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hee64a599f575a1b3(arg0, arg1);
wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hd5062f8742a3e5bf(arg0, arg1);
}

let stack_pointer = 128;
Expand All @@ -237,19 +237,19 @@ function addBorrowedObject(obj) {
}
function __wbg_adapter_51(arg0, arg1, arg2) {
try {
wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h6821a64a8b32b54e(arg0, arg1, addBorrowedObject(arg2));
wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h3973ccc10169b3e2(arg0, arg1, addBorrowedObject(arg2));
} finally {
heap[stack_pointer++] = undefined;
}
}

function __wbg_adapter_54(arg0, arg1, arg2) {
wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h578c1a08304759e7(arg0, arg1, addHeapObject(arg2));
wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hf0d692ef0f316208(arg0, arg1, addHeapObject(arg2));
}

function __wbg_adapter_57(arg0, arg1, arg2) {
try {
wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hb3016357f235b290(arg0, arg1, addBorrowedObject(arg2));
wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__he09faeaf47590909(arg0, arg1, addBorrowedObject(arg2));
} finally {
heap[stack_pointer++] = undefined;
}
Expand Down Expand Up @@ -724,6 +724,10 @@ function __wbg_get_imports() {
const ret = getObject(arg0).credentials;
return addHeapObject(ret);
};
imports.wbg.__wbg_languages_4ab80469955a57f7 = function(arg0) {
const ret = getObject(arg0).languages;
return addHeapObject(ret);
};
imports.wbg.__wbg_parentNode_9e53f8b17eb98c9d = function(arg0) {
const ret = getObject(arg0).parentNode;
return isLikeNone(ret) ? 0 : addHeapObject(ret);
Expand Down Expand Up @@ -1146,20 +1150,20 @@ function __wbg_get_imports() {
const ret = wasm.memory;
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper659 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 380, __wbg_adapter_48);
imports.wbg.__wbindgen_closure_wrapper673 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 387, __wbg_adapter_48);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper4175 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 1999, __wbg_adapter_51);
imports.wbg.__wbindgen_closure_wrapper4684 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 2258, __wbg_adapter_51);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper4941 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 2306, __wbg_adapter_54);
imports.wbg.__wbindgen_closure_wrapper5454 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 2565, __wbg_adapter_54);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper5000 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 2330, __wbg_adapter_57);
imports.wbg.__wbindgen_closure_wrapper5513 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 2589, __wbg_adapter_57);
return addHeapObject(ret);
};

Expand Down
Binary file modified server/web_ui/pkg/kanidmd_web_ui_bg.wasm
Binary file not shown.
Binary file modified server/web_ui/pkg/kanidmd_web_ui_bg.wasm.br
Binary file not shown.
37 changes: 26 additions & 11 deletions server/web_ui/src/components/admin_accounts.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::collections::BTreeMap;

use gloo::console;
use i18n_embed_fl::fl;
use yew::{html, Component, Context, Html, Properties};
use yew_router::prelude::Link;

Expand All @@ -9,6 +10,7 @@ use crate::components::alpha_warning_banner;
use crate::constants::{
CSS_BREADCRUMB_ITEM, CSS_BREADCRUMB_ITEM_ACTIVE, CSS_CELL, CSS_DT, CSS_TABLE,
};
use crate::manager::I18n;
use crate::utils::{do_alert_error, do_page_header};
use crate::views::AdminRoute;
use crate::{do_request, RequestMethod};
Expand All @@ -24,6 +26,8 @@ impl From<GetError> for AdminListAccountsMsg {

pub struct AdminListAccounts {
state: ViewState,
i18n: Rc<I18n>,
_context_listener: ContextHandle<Rc<I18n>>,
}

// callback messaging for this confused pile of crab-bait
Expand All @@ -36,6 +40,7 @@ pub enum AdminListAccountsMsg {
emsg: String,
kopid: Option<String>,
},
I18n(I18n),
}

enum ViewState {
Expand Down Expand Up @@ -143,6 +148,10 @@ impl Component for AdminListAccounts {
fn create(ctx: &Context<Self>) -> Self {
// TODO: work out the querystring thing so we can just show x number of elements
// console::log!("query: {:?}", location().query);
let (i18n, context_listener) = ctx
.link()
.context(ctx.link().callback(Self::Message::I18n))
.unwrap();

// start pulling the account data on startup
ctx.link().send_future(async move {
Expand All @@ -153,6 +162,8 @@ impl Component for AdminListAccounts {
});
AdminListAccounts {
state: ViewState::Loading,
i18n,
_context_listener: context_listener,
}
}

Expand All @@ -178,6 +189,10 @@ impl Component for AdminListAccounts {
console::log!("emsg: {:?}", emsg);
console::log!("kopid: {:?}", kopid);
}
AdminListAccountsMsg::I18n(i18n) => {
self.i18n = i18n;
return true;
}
}
false
}
Expand All @@ -187,15 +202,15 @@ impl Component for AdminListAccounts {
<>

<ol class="breadcrumb">
<li class={CSS_BREADCRUMB_ITEM}><Link<AdminRoute> to={AdminRoute::AdminMenu}>{"Admin"}</Link<AdminRoute>></li>
<li class={CSS_BREADCRUMB_ITEM_ACTIVE} aria-current="page">{"Accounts"}</li>
<li class={CSS_BREADCRUMB_ITEM}><Link<AdminRoute> to={AdminRoute::AdminMenu}>{fl!(self.i18n.i18n, "breadcrumb-admin")}</Link<AdminRoute>></li>
<li class={CSS_BREADCRUMB_ITEM_ACTIVE} aria-current="page">{fl!(self.i18n.i18n, "breadcrumb-admin-accounts")}</li>
</ol>
{do_page_header("Account Administration")}
{do_page_header(fl!(self.i18n.i18n, "header-account-admin"))}
{ alpha_warning_banner() }
<div id={"accountlist"}>
{match &self.state {
ViewState::Loading => {
html! {"Waiting on the accounts list to load..."}
html! {fl!(self.i18n.i18n, "accounts-list-load-waiting")}
}

ViewState::Responded { response } => {
Expand All @@ -206,9 +221,9 @@ impl Component for AdminListAccounts {
<thead>
<tr>
<th scope={scope_col}></th>
<th scope={scope_col}>{"Display Name"}</th>
<th scope={scope_col}>{"Username"}</th>
<th scope={scope_col}>{"Description"}</th>
<th scope={scope_col}>{fl!(self.i18n.i18n, "th-accounts-display-name")}</th>
<th scope={scope_col}>{fl!(self.i18n.i18n, "th-accounts-username")}</th>
<th scope={scope_col}>{fl!(self.i18n.i18n, "th-accounts-description")}</th>
</tr>
</thead>

Expand All @@ -227,8 +242,8 @@ impl Component for AdminListAccounts {
None => String::from(""),
};
let account_type: Html = match account.object_type {
EntityType::ServiceAccount => html!{<img src={"/pkg/img/icon-robot.svg"} alt={"Service Account"} class={"p-0"} />},
EntityType::Person => html!{<img src={"/pkg/img/icon-person.svg"} alt={"Person"} class={"p-0"} />},
EntityType::ServiceAccount => html!{<img src={"/pkg/img/icon-robot.svg"} alt={fl!(self.i18n.i18n, "account-type-service")} class={"p-0"} />},
EntityType::Person => html!{<img src={"/pkg/img/icon-person.svg"} alt={fl!(self.i18n.i18n, "account-type-person")} class={"p-0"} />},
_ => html!("x"),
};

Expand Down Expand Up @@ -269,12 +284,12 @@ impl Component for AdminListAccounts {
console::error!("Failed to pull details", format!("{:?}", kopid));
html!(
<>
{do_alert_error("Failed to Query Accounts", Some(emsg))}
{do_alert_error(fl!(self.i18n.i18n, "alert-failed-to-query-accounts"), Some(emsg))}
</>
)
}
ViewState::NotAuthorized {} => {
do_alert_error("You're not authorized to see this page!", None)
do_alert_error(fl!(self.i18n.i18n, "alert-unauthorized"), None)
}
}}
</div>
Expand Down
99 changes: 78 additions & 21 deletions server/web_ui/src/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@
//! not authenticated, this will determine that and send you to authentication first, then
//! will allow you to proceed with the oauth flow.

use std::rc::Rc;

use gloo::console;
use i18n_embed::LanguageLoader;
use i18n_embed::unic_langid::LanguageIdentifier;
use i18n_embed_fl::fl;
use serde::{Deserialize, Serialize};
use wasm_bindgen::UnwrapThrowExt;
use yew::functional::*;
Expand All @@ -16,6 +21,15 @@ use crate::login::{LoginApp, LoginWorkflow};
use crate::oauth2::Oauth2App;
use crate::views::{ViewRoute, ViewsApp};

use i18n_embed::{WebLanguageRequester, fluent::{
FluentLanguageLoader, fluent_language_loader
}};
use rust_embed::RustEmbed;

#[derive(RustEmbed)]
#[folder = "i18n"]
struct Localizations;

// router to decide on state.
#[derive(Routable, PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
pub enum Route {
Expand Down Expand Up @@ -53,6 +67,28 @@ fn landing() -> Html {
html! { <main></main> }
}

#[function_component]
fn NotFound() -> Html {
let i18n = use_context::<Rc<I18n>>().unwrap();

html! {
<>
<main class="flex-shrink-0 form-signin text-center">
<img src="/pkg/img/logo-square.svg" alt="Kanidm" class="kanidm_logo"/>
// TODO: replace this with a call to domain info
<h3>{ fl!(i18n.i18n, "page-not-found") }</h3>

<div class="container">
<Link<ViewRoute> to={ ViewRoute::Apps }>
{ fl!(i18n.i18n, "goto-home") }
</Link<ViewRoute>>
</div>
</main>
{ crate::utils::do_footer() }
</>
}
}

// Needed for yew to pass by value
#[allow(clippy::needless_pass_by_value)]
fn switch(route: Route) -> Html {
Expand All @@ -74,27 +110,46 @@ fn switch(route: Route) -> Html {
Route::NotFound => {
add_body_form_classes!();

html! {
<>
<main class="flex-shrink-0 form-signin text-center">
<img src="/pkg/img/logo-square.svg" alt="Kanidm" class="kanidm_logo"/>
// TODO: replace this with a call to domain info
<h3>{ "404 - Page not found" }</h3>

<div class="container">
<Link<ViewRoute> to={ ViewRoute::Apps }>
{ "Home" }
</Link<ViewRoute>>
</div>
</main>
{ crate::utils::do_footer() }
</>
}
html! { <NotFound /> }
}
}
}

pub struct ManagerApp {}
#[derive(Clone, Debug)]
pub struct I18n {
pub i18n: Rc<FluentLanguageLoader>
}

impl I18n {
fn new() -> I18n {
let loader: FluentLanguageLoader = fluent_language_loader!();
let requested_languages = {
let mut it = WebLanguageRequester::requested_languages();
it.push(loader.fallback_language().clone());
it
};

let languages_vec = requested_languages.iter().map(|it| it).collect::<Vec<&LanguageIdentifier>>();
let languages = languages_vec.as_slice();
let _ = loader
.load_languages(&Localizations, &languages)
.map_err(|err| {
console::warn!("issue loading i18n: {}", err.to_string());
});

I18n { i18n: loader.into() }
}
}

impl PartialEq for I18n {
fn eq(&self, _rhs: &I18n) -> bool {
true
}
}

pub struct ManagerApp {
i18n: Rc<I18n>
}

impl Component for ManagerApp {
type Message = ();
Expand All @@ -103,7 +158,7 @@ impl Component for ManagerApp {
fn create(_ctx: &Context<Self>) -> Self {
#[cfg(debug_assertions)]
console::debug!("manager::create");
ManagerApp {}
ManagerApp { i18n: I18n::new().into() }
}

fn changed(&mut self, _ctx: &Context<Self>, _props: &Self::Properties) -> bool {
Expand All @@ -127,9 +182,11 @@ impl Component for ManagerApp {

fn view(&self, _ctx: &Context<Self>) -> Html {
html! {
<BrowserRouter>
<Switch<Route> render={ switch } />
</BrowserRouter>
<ContextProvider<Rc<I18n>> context={self.i18n.clone()}>
<BrowserRouter>
<Switch<Route> render={ switch } />
</BrowserRouter>
</ContextProvider<Rc<I18n>>>
}
}
}