Skip to content

Commit

Permalink
Provide the frontend translations via the /po.js path (#1126)
Browse files Browse the repository at this point in the history
## Problem

- Reimplement the translation support for the web frontend

## Solution

- Route the `/po.js` path to the code which redirects to the requested
translations or returns an empty JS file if the requested language is
not found.
  • Loading branch information
lslezak committed Apr 4, 2024
1 parent 4be45d2 commit 11cda28
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 4 deletions.
12 changes: 12 additions & 0 deletions rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion rust/agama-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ utoipa = { version = "4.2.0", features = ["axum_extras"] }
config = "0.14.0"
rand = "0.8.5"
jsonwebtoken = "9.2.0"
axum-extra = { version = "0.9.2", features = ["typed-header"] }
axum-extra = { version = "0.9.2", features = ["cookie", "typed-header"] }
chrono = { version = "0.4.34", default-features = false, features = [
"now",
"std",
Expand Down
66 changes: 63 additions & 3 deletions rust/agama-server/src/web/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ use super::{
state::ServiceState,
};
use axum::{
body::Body,
extract::State,
http::{header::SET_COOKIE, HeaderMap},
http::{header, HeaderMap, HeaderValue, StatusCode},
response::IntoResponse,
Json,
};
use axum_extra::extract::cookie::CookieJar;
use pam::Client;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
Expand Down Expand Up @@ -62,7 +64,7 @@ pub async fn login(
let mut headers = HeaderMap::new();
let cookie = format!("agamaToken={}; HttpOnly", &token);
headers.insert(
SET_COOKIE,
header::SET_COOKIE,
cookie.parse().expect("could not build a valid cookie"),
);

Expand All @@ -76,7 +78,7 @@ pub async fn logout(_claims: TokenClaims) -> Result<impl IntoResponse, AuthError
let mut headers = HeaderMap::new();
let cookie = "agamaToken=deleted; HttpOnly; Expires=Thu, 01 Jan 1970 00:00:00 GMT".to_string();
headers.insert(
SET_COOKIE,
header::SET_COOKIE,
cookie.parse().expect("could not build a valid cookie"),
);
Ok(headers)
Expand All @@ -90,3 +92,61 @@ pub async fn logout(_claims: TokenClaims) -> Result<impl IntoResponse, AuthError
pub async fn session(_claims: TokenClaims) -> Result<(), AuthError> {
Ok(())
}

// builds a response tuple for translation redirection
fn redirect_to_file(file: &str) -> (StatusCode, HeaderMap, Body) {
tracing::info!("Redirecting to translation file {}", file);

let mut response_headers = HeaderMap::new();
// translation found, redirect to the real file
response_headers.insert(
header::LOCATION,
// if the file exists then the name is a valid value and unwrapping is safe
HeaderValue::from_str(file).unwrap(),
);

(
StatusCode::TEMPORARY_REDIRECT,
response_headers,
Body::empty(),
)
}

// handle the /po.js request
// the requested language (locale) is sent in the "agamaLang" HTTP cookie
// this reimplements the Cockpit translation support
pub async fn po(State(state): State<ServiceState>, jar: CookieJar) -> impl IntoResponse {
if let Some(cookie) = jar.get("agamaLang") {
tracing::info!("Language cookie: {}", cookie.value());
// try parsing the cookie
if let Some((lang, region)) = cookie.value().split_once('-') {
// first try language + country
let target_file = format!("po.{}_{}.js", lang, region.to_uppercase());
if state.public_dir.join(&target_file).exists() {
return redirect_to_file(&target_file);
} else {
// then try the language only
let target_file = format!("po.{}.js", lang);
if state.public_dir.join(&target_file).exists() {
return redirect_to_file(&target_file);
};
}
} else {
// use the cookie as is
let target_file = format!("po.{}.js", cookie.value());
if state.public_dir.join(&target_file).exists() {
return redirect_to_file(&target_file);
}
}
}

tracing::info!("Translation not found");
// fallback, return empty javascript translations if the language is not supported
let mut response_headers = HeaderMap::new();
response_headers.insert(
header::CONTENT_TYPE,
HeaderValue::from_static("text/javascript"),
);

(StatusCode::OK, response_headers, Body::empty())
}
4 changes: 4 additions & 0 deletions rust/agama-server/src/web/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ impl MainServiceBuilder {
let state = ServiceState {
config: self.config,
events: self.events,
public_dir: self.public_dir.clone(),
};

let api_router = self
Expand All @@ -84,9 +85,12 @@ impl MainServiceBuilder {
.route("/ping", get(super::http::ping))
.route("/auth", post(login).get(session).delete(logout));

tracing::info!("Serving static files from {}", self.public_dir.display());
let serve = ServeDir::new(self.public_dir);

Router::new()
.nest_service("/", serve)
.route("/po.js", get(super::http::po))
.nest("/api", api_router)
.layer(TraceLayer::new_for_http())
.layer(CompressionLayer::new().br(true))
Expand Down
2 changes: 2 additions & 0 deletions rust/agama-server/src/web/state.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Implements the web service state.

use super::{config::ServiceConfig, EventsSender};
use std::path::PathBuf;

/// Web service state.
///
Expand All @@ -9,4 +10,5 @@ use super::{config::ServiceConfig, EventsSender};
pub struct ServiceState {
pub config: ServiceConfig,
pub events: EventsSender,
pub public_dir: PathBuf,
}

0 comments on commit 11cda28

Please sign in to comment.