Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 85 additions & 4 deletions definy-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ impl narumincho_vdom_client::App<AppState> for DefinyApp {
.unwrap();
on_keydown.forget();

let filter_query = {
let query_string = {
let initial_url = web_sys::window()
.unwrap()
.document()
Expand All @@ -109,7 +109,43 @@ impl narumincho_vdom_client::App<AppState> for DefinyApp {
search.strip_prefix('?').unwrap_or(search.as_str()).to_string()
};

let filter_for_fetch = definy_ui::event_filter_from_query_str(&filter_query);
let query_params = definy_ui::query::parse_query(Some(query_string.as_str()));
let filter_for_fetch = query_params.event_type;
let html_lang = web_sys::window()
.and_then(|window| window.document())
.and_then(|document| document.document_element())
.and_then(|element| element.get_attribute("lang"));
let fallback_language = html_lang
.as_deref()
.and_then(definy_ui::language::language_from_tag)
.or_else(definy_ui::language::best_language_from_browser)
.unwrap_or_else(definy_ui::language::default_language);
let language_resolution = if let Some(requested_lang) = query_params.lang {
if let Some(language) = definy_ui::language::language_from_tag(requested_lang.as_str()) {
definy_ui::language::LanguageResolution {
language,
unsupported_query_lang: None,
}
} else {
definy_ui::language::LanguageResolution {
language: fallback_language,
unsupported_query_lang: Some(requested_lang),
}
}
} else {
definy_ui::language::LanguageResolution {
language: fallback_language,
unsupported_query_lang: None,
}
};
let language_fallback_notice =
language_resolution
.unsupported_query_lang
.as_ref()
.map(|requested| definy_ui::LanguageFallbackNotice {
requested: requested.to_string(),
fallback_to_code: language_resolution.language.code,
});
wasm_bindgen_futures::spawn_local(async move {
if let Some(ssr_event_binaries) = ssr_event_binaries {
let event_pairs = ssr_event_binaries
Expand Down Expand Up @@ -248,7 +284,9 @@ impl narumincho_vdom_client::App<AppState> for DefinyApp {
is_loading,
has_more,
None,
definy_ui::event_filter_from_query_str(&filter_query),
filter_for_fetch,
language_resolution.language,
language_fallback_notice,
)
}

Expand All @@ -259,10 +297,35 @@ impl narumincho_vdom_client::App<AppState> for DefinyApp {
let location = definy_ui::Location::from_url(&pathname);
let search = web_url.search();
let query = search.strip_prefix('?').unwrap_or(search.as_str());
let filter_event_type = definy_ui::event_filter_from_query_str(query);
let query_params = definy_ui::query::parse_query(Some(query));
let filter_event_type = query_params.event_type;
let requested_lang = query_params.lang.clone();
let parsed_language = requested_lang
.as_deref()
.and_then(definy_ui::language::language_from_tag);
let (language, language_fallback_notice) = if let Some(requested_lang) = requested_lang
{
if let Some(parsed_language) = parsed_language {
(parsed_language, None)
} else {
let fallback_language = definy_ui::language::best_language_from_browser()
.unwrap_or_else(definy_ui::language::default_language);
(
fallback_language,
Some(definy_ui::LanguageFallbackNotice {
requested: requested_lang,
fallback_to_code: fallback_language.code,
}),
)
}
} else {
(state.language, None)
};
let mut next = AppState {
location,
event_detail_eval_result: None,
language,
language_fallback_notice,
..state
};
if matches!(next.location, Some(definy_ui::Location::Home)) {
Expand All @@ -277,6 +340,24 @@ impl narumincho_vdom_client::App<AppState> for DefinyApp {
};
}
}
if query_params.lang.is_none() {
if let Some(location) = &next.location {
let url = AppState::build_url(
location,
next.language.code,
filter_event_type,
);
if let Some(window) = web_sys::window() {
if let Ok(history) = window.history() {
let _ = history.replace_state_with_url(
&wasm_bindgen::JsValue::NULL,
"",
Some(url.as_str()),
);
}
}
}
}
return next;
}
state
Expand Down
59 changes: 58 additions & 1 deletion definy-server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,36 @@ async fn handler(
.is_some_and(|value| value.contains("text/html"));

if accepts_html {
if let Some(redirect_url) = lang_redirect_url(&request) {
return Response::builder()
.status(302)
.header("Location", redirect_url)
.body(Full::new(Bytes::from("Redirecting...")));
}
let accept_language = request
.headers()
.get("accept-language")
.and_then(|value| value.to_str().ok());
let language_resolution = definy_ui::language::resolve_language(uri.query(), accept_language);
let language_fallback_notice =
language_resolution
.unsupported_query_lang
.as_ref()
.map(|requested| definy_ui::LanguageFallbackNotice {
requested: requested.to_string(),
fallback_to_code: language_resolution.language.code,
});
let pool = state.pool.read().await.clone();
return match pool {
Some(pool) => handle_html(&uri, &pool).await,
Some(pool) => {
handle_html(
&uri,
&pool,
language_resolution.language,
language_fallback_notice,
)
.await
}
None => db_unavailable_response(true),
};
}
Expand Down Expand Up @@ -198,6 +225,8 @@ fn db_unavailable_response(wants_html: bool) -> Result<Response<Full<Bytes>>, hy
async fn handle_html(
uri: &hyper::Uri,
pool: &sqlx::postgres::PgPool,
language: definy_ui::language::Language,
language_fallback_notice: Option<definy_ui::LanguageFallbackNotice>,
) -> Result<Response<Full<Bytes>>, hyper::http::Error> {
let path = uri.path();
let query = uri.query();
Expand Down Expand Up @@ -254,6 +283,8 @@ async fn handle_html(
has_more,
None,
filter_event_type,
language,
language_fallback_notice,
),
&Some(definy_ui::ResourceHash {
js: JAVASCRIPT_HASH.to_string(),
Expand All @@ -263,3 +294,29 @@ async fn handle_html(
),
))))
}

fn lang_redirect_url(request: &Request<impl hyper::body::Body>) -> Option<String> {
if definy_ui::query::parse_query(request.uri().query())
.lang
.is_some()
{
return None;
}
let accept_language = request
.headers()
.get("accept-language")
.and_then(|value| value.to_str().ok());
let best = definy_ui::language::best_language_from_accept_language(accept_language);
Some(build_url_with_lang(request.uri(), best.code))
}

fn build_url_with_lang(uri: &hyper::Uri, lang_code: &str) -> String {
let mut params = definy_ui::query::parse_query(uri.query());
params.lang = Some(lang_code.to_string());
let mut url = uri.path().to_string();
if let Some(query) = definy_ui::query::build_query(params) {
url.push('?');
url.push_str(query.as_str());
}
url
}
36 changes: 28 additions & 8 deletions definy-ui/src/account_detail.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use narumincho_vdom::*;

use crate::{AppState, Location, fetch};
use crate::i18n;

pub fn account_detail_view(
state: &AppState,
Expand Down Expand Up @@ -36,7 +37,12 @@ pub fn account_detail_view(
.children([
Div::new()
.style(Style::new().set("font-weight", "600"))
.children([text("Change account name")])
.children([text(i18n::tr(
state,
"Change account name",
"アカウント名を変更",
"Ŝanĝi kontonomon",
))])
.into_node(),
Input::new()
.type_("text")
Expand Down Expand Up @@ -168,7 +174,7 @@ pub fn account_detail_view(
state
}));
}))
.children([text("Change Name")])
.children([text(i18n::tr(state, "Change Name", "名前を変更", "Ŝanĝi nomon"))])
.into_node(),
])
.into_node(),
Expand All @@ -183,7 +189,7 @@ pub fn account_detail_view(
.children([
A::<AppState, Location>::new()
.class("back-link")
.href(Href::Internal(Location::AccountList))
.href(state.href_with_lang(Location::AccountList))
.style(
Style::new()
.set("display", "inline-flex")
Expand All @@ -192,7 +198,12 @@ pub fn account_detail_view(
.set("color", "var(--primary)")
.set("font-weight", "500"),
)
.children([text("← Back to Accounts")])
.children([text(i18n::tr(
state,
"← Back to Accounts",
"← アカウント一覧へ戻る",
"← Reen al kontoj",
))])
.into_node(),
Div::new()
.class("event-detail-card")
Expand All @@ -219,7 +230,11 @@ pub fn account_detail_view(
.into_node(),
Div::new()
.style(Style::new().set("color", "var(--text-secondary)"))
.children([text(format!("{} events", account_events.len()))])
.children([text(format!(
"{} {}",
account_events.len(),
i18n::tr(state, "events", "イベント", "eventoj")
))])
.into_node(),
])
.into_node(),
Expand All @@ -236,7 +251,12 @@ pub fn account_detail_view(
.set("padding", "0.9rem")
.set("color", "var(--text-secondary)"),
)
.children([text("This account has not posted any events yet.")])
.children([text(i18n::tr(
state,
"This account has not posted any events yet.",
"このアカウントはまだイベントを投稿していません。",
"Ĉi tiu konto ankoraŭ ne afiŝis eventojn.",
))])
.into_node()
} else {
Div::new()
Expand All @@ -248,7 +268,7 @@ pub fn account_detail_view(
.map(|(hash, event)| {
A::<AppState, Location>::new()
.class("event-card")
.href(Href::Internal(Location::Event(hash)))
.href(state.href_with_lang(Location::Event(hash)))
.style(
Style::new()
.set("display", "grid")
Expand All @@ -268,7 +288,7 @@ pub fn account_detail_view(
.into_node(),
Div::new()
.children([text(
crate::event_presenter::event_summary_text(event),
crate::event_presenter::event_summary_text(state, event),
)])
.into_node(),
])
Expand Down
21 changes: 16 additions & 5 deletions definy-ui/src/account_list.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use narumincho_vdom::*;

use crate::{AppState, Location};
use crate::i18n;

struct AccountRow {
account_id: definy_event::event::AccountId,
Expand All @@ -19,13 +20,18 @@ pub fn account_list_view(state: &AppState) -> Node<AppState> {
.children([
H2::new()
.style(Style::new().set("font-size", "1.3rem"))
.children([text("Accounts")])
.children([text(i18n::tr(state, "Accounts", "アカウント", "Kontoj"))])
.into_node(),
if rows.is_empty() {
Div::new()
.class("event-detail-card")
.style(Style::new().set("padding", "0.9rem"))
.children([text("No accounts yet.")])
.children([text(i18n::tr(
state,
"No accounts yet.",
"まだアカウントがありません。",
"Ankoraŭ neniuj kontoj.",
))])
.into_node()
} else {
Div::new()
Expand All @@ -42,7 +48,7 @@ pub fn account_list_view(state: &AppState) -> Node<AppState> {
);
A::<AppState, Location>::new()
.class("event-card")
.href(Href::Internal(Location::Account(row.account_id)))
.href(state.href_with_lang(Location::Account(row.account_id)))
.style(
Style::new()
.set("display", "grid")
Expand All @@ -60,7 +66,11 @@ pub fn account_list_view(state: &AppState) -> Node<AppState> {
.set("font-size", "0.85rem")
.set("color", "var(--text-secondary)"),
)
.children([text(format!("{} events", row.event_count))])
.children([text(format!(
"{} {}",
row.event_count,
i18n::tr(state, "events", "イベント", "eventoj")
))])
.into_node(),
Div::new()
.style(
Expand All @@ -69,7 +79,8 @@ pub fn account_list_view(state: &AppState) -> Node<AppState> {
.set("color", "var(--text-secondary)"),
)
.children([text(format!(
"latest: {}",
"{} {}",
i18n::tr(state, "latest:", "最新:", "lasta:"),
row.latest_time.format("%Y-%m-%d %H:%M:%S")
))])
.into_node(),
Expand Down
Loading