Basic translation support for website

master
Tait Hoyem 1 year ago
parent 143df9b964
commit ffdea810ad

42
Cargo.lock generated

@ -795,11 +795,13 @@ dependencies = [
"chrono",
"ormx",
"serde",
"serde-xml-rs",
"serde_plain",
"serde_tuple",
"sql-builder",
"sqlx",
"static_assertions",
"strum",
"tera",
"tokio",
"tokio-test",
@ -1402,6 +1404,18 @@ dependencies = [
"serde_derive",
]
[[package]]
name = "serde-xml-rs"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb3aa78ecda1ebc9ec9847d5d3aba7d618823446a049ba2491940506da6e2782"
dependencies = [
"log",
"serde",
"thiserror",
"xml-rs",
]
[[package]]
name = "serde_derive"
version = "1.0.159"
@ -1673,6 +1687,28 @@ dependencies = [
"unicode-normalization",
]
[[package]]
name = "strum"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.24.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59"
dependencies = [
"heck",
"proc-macro2",
"quote",
"rustversion",
"syn 1.0.109",
]
[[package]]
name = "subtle"
version = "2.4.1"
@ -2341,3 +2377,9 @@ name = "windows_x86_64_msvc"
version = "0.47.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d6e62c256dc6d40b8c8707df17df8d774e60e39db723675241e7c15e910bce7"
[[package]]
name = "xml-rs"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3"

@ -20,6 +20,8 @@ tera = "1.18.1"
tokio = { version = "1.26.0", features = ["rt-multi-thread", "macros" ] }
sqlx = { version = "0.5", features = ["macros", "postgres", "runtime-tokio-rustls", "chrono"] }
ormx = { version = "0.10.0", features = ["postgres"] }
serde-xml-rs = "0.6.0"
strum = { version = "0.24.1", features = ["derive"] }
[dev-dependencies]
tokio-test = "0.4.2"

@ -4,6 +4,6 @@ use sqlx::postgres::PgPoolOptions;
pub async fn connect() -> Pool<Postgres> {
PgPoolOptions::new()
.max_connections(8)
.connect("postgres://ibihf2:ibihf@localhost/ibihf").await
.connect("postgres://ibihf:ibihf@localhost/ibihf").await
.unwrap()
}

@ -2,7 +2,12 @@ mod db;
mod model;
mod views;
mod filters;
mod translations;
use translations::{
TranslatedKey,
SupportedLanguage,
};
use crate::model::{
League,
Team,
@ -54,25 +59,28 @@ use askama::Template;
#[template(path="hello.html")]
struct HelloTemplate<'a> {
name: &'a str,
years: i32
years: i32,
}
#[derive(Template)]
#[template(path="partials/box_score_table.html")]
struct BoxScoreTemplate {
goals: Vec<GoalDetails>,
lang: SupportedLanguage,
}
#[derive(Template)]
#[template(path="partials/individual_game_points_table.html")]
struct IndividualGamePointsTableTemplate {
players: Vec<PlayerStats>,
lang: SupportedLanguage,
}
#[derive(Template)]
#[template(path="partials/team_stats_table.html")]
struct TeamGameStatsTemplate {
teams: Vec<TeamStats>,
lang: SupportedLanguage,
}
#[derive(Template)]
@ -80,12 +88,15 @@ struct TeamGameStatsTemplate {
struct DivisionListTemplate {
league: League,
divisions: Vec<Division>,
lang: SupportedLanguage,
}
#[derive(Template)]
#[template(path="league_list.html")]
struct LeagueListTemplate {
leagues: Vec<League>,
heading: String,
lang: SupportedLanguage,
}
#[derive(Template)]
@ -93,12 +104,14 @@ struct LeagueListTemplate {
struct GameListTemplate {
division: Division,
games: Vec<Game>,
lang: SupportedLanguage,
}
#[derive(Template)]
#[template(path="partials/play_by_play_table.html")]
struct ShotsTableTemplate {
shots: Vec<ShotDetails>,
lang: SupportedLanguage,
}
#[derive(Template)]
@ -110,6 +123,7 @@ struct GameScorePageTemplate {
team_stats: TeamGameStatsTemplate,
individual_stats: IndividualGamePointsTableTemplate,
play_by_play: ShotsTableTemplate,
lang: SupportedLanguage,
}
#[derive(Template)]
@ -119,6 +133,7 @@ pub struct PlayerPageTemplate {
league: League,
league_stats: PlayerStats,
lifetime_stats: PlayerStats,
lang: SupportedLanguage,
}
#[derive(Clone)]
@ -129,17 +144,19 @@ pub struct ServerState {
#[tokio::main]
async fn main() {
let pool = db::connect().await;
let xml_en = translations::en_lang();
let xml_fr = translations::fr_lang();
let state = ServerState {
db_pool: Arc::new(pool),
};
let router = Router::new()
.route("/", get(league_html))
.route("/shots/", get(shots_all))
.route("/test/", get(test_template))
.route("/league/:id/", get(divisions_for_league_html))
.route("/division/:id/", get(games_for_division_html))
.route("/game/:id/", get(score_for_game_html))
.route("/player/:name/", get(player_from_name))
.route("/:lang/", get(league_html))
.route("/:lang/shots/", get(shots_all))
.route("/:lang/test/", get(test_template))
.route("/:lang/league/:id/", get(divisions_for_league_html))
.route("/:lang/division/:id/", get(games_for_division_html))
.route("/:lang/game/:id/", get(score_for_game_html))
.route("/:lang/player/:name/", get(player_from_name))
.with_state(state);
let addr = SocketAddr::from(([127, 0, 0, 1], 8000));
println!("Listening on {}", addr);
@ -149,7 +166,7 @@ async fn main() {
.unwrap();
}
async fn player_from_name(State(server_config): State<ServerState>, Path(name): Path<String>) -> impl IntoResponse {
async fn player_from_name(State(server_config): State<ServerState>, Path((lang,name)): Path<(SupportedLanguage,String)>) -> impl IntoResponse {
let player = Player::from_name_case_insensitive(&server_config.db_pool, name)
.await
.unwrap();
@ -165,6 +182,7 @@ async fn player_from_name(State(server_config): State<ServerState>, Path(name):
.unwrap();
let html = PlayerPageTemplate {
player,
lang,
league: latest_league,
league_stats: latest_league_stats,
lifetime_stats,
@ -199,17 +217,23 @@ macro_rules! get_by_id {
}
}
async fn league_html(State(server_config): State<ServerState>) -> impl IntoResponse {
async fn league_html(State(server_config): State<ServerState>, Path(lang): Path<SupportedLanguage>) -> impl IntoResponse {
let leagues = League::all(&*server_config.db_pool)
.await
.unwrap();
let heading = match lang {
SupportedLanguage::English => "IBIHF Leagues",
SupportedLanguage::French => "League de FIDHS",
}.to_string();
let leagues_template = LeagueListTemplate {
leagues
leagues,
heading,
lang,
};
(StatusCode::OK, leagues_template)
}
async fn divisions_for_league_html(State(server_config): State<ServerState>, Path(league_id): Path<i32>) -> impl IntoResponse {
async fn divisions_for_league_html(State(server_config): State<ServerState>, Path(league_id): Path<i32>, Path(lang): Path<SupportedLanguage>) -> impl IntoResponse {
let league = League::get(&*server_config.db_pool, league_id)
.await
.unwrap();
@ -218,12 +242,13 @@ async fn divisions_for_league_html(State(server_config): State<ServerState>, Pat
.unwrap();
let html = DivisionListTemplate {
league,
divisions
divisions,
lang,
};
(StatusCode::OK, html)
}
async fn games_for_division_html(State(server_config): State<ServerState>, Path(division_id): Path<i32>) -> impl IntoResponse {
async fn games_for_division_html(State(server_config): State<ServerState>, Path(division_id): Path<i32>, Path(lang): Path<SupportedLanguage>) -> impl IntoResponse {
let division = Division::get(&*server_config.db_pool, division_id)
.await
.unwrap();
@ -232,11 +257,12 @@ async fn games_for_division_html(State(server_config): State<ServerState>, Path(
.unwrap();
let games_template = GameListTemplate {
division,
games
games,
lang,
};
(StatusCode::OK, games_template)
}
async fn score_for_game_html(State(server_config): State<ServerState>, Path(game_id): Path<i32>) -> impl IntoResponse {
async fn score_for_game_html(State(server_config): State<ServerState>, Path(game_id): Path<i32>, Path(lang): Path<SupportedLanguage>) -> impl IntoResponse {
let game = sqlx::query_as::<_, Game>(
"SELECT * FROM games WHERE id = $1;"
)
@ -249,17 +275,19 @@ async fn score_for_game_html(State(server_config): State<ServerState>, Path(game
.unwrap();
let pbp = get_play_by_play_from_game(&server_config.db_pool, &game).await.unwrap();
let score = get_score_from_game(&server_config.db_pool, &game).await.unwrap();
let score_html = TeamGameStatsTemplate { teams: score };
let score_html = TeamGameStatsTemplate { teams: score, lang };
let goal_details = get_box_score_from_game(&server_config.db_pool, &game).await.unwrap();
let goal_details_html = IndividualGamePointsTableTemplate { players: goal_details };
let goal_details_html = IndividualGamePointsTableTemplate { players: goal_details, lang };
let box_score = get_goals_from_game(&server_config.db_pool, &game).await.unwrap();
let box_score_html = BoxScoreTemplate { goals: box_score };
let box_score_html = BoxScoreTemplate { goals: box_score, lang };
let pbp_html = ShotsTableTemplate {
shots: pbp
shots: pbp,
lang,
};
let game_template = GameScorePageTemplate {
division,
game,
lang,
box_score: box_score_html,
team_stats: score_html,
individual_stats: goal_details_html,

@ -0,0 +1,79 @@
use serde::{Serialize, Deserialize};
use strum::{
EnumIter,
IntoEnumIterator,
};
#[derive(Serialize, Deserialize, Debug, PartialEq, Copy, Clone)]
pub enum SupportedLanguage {
#[serde(rename = "en")]
English,
#[serde(rename = "fr")]
French,
}
impl std::fmt::Display for SupportedLanguage {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let output = match self {
Self::English => "en",
Self::French => "fr",
};
write!(f, "{}", output)
}
}
#[derive(Serialize, Deserialize, Debug, PartialEq, EnumIter)]
#[serde(rename_all = "camelCase")]
pub enum TranslatedKey {
UrlGame,
UrlDivision,
UrlLeague,
IbihfLeagues,
Goals,
Assists,
Period,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
pub struct TranslatedString {
#[serde(rename = "name")]
pub key: TranslatedKey,
#[serde(rename = "$value")]
pub value: String,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
pub struct LanguageStrings {
#[serde(rename = "$value")]
pub kvs: Vec<TranslatedString>,
}
/// Verify that all keys are present for translations.
pub fn verify_resources(ls: &LanguageStrings) -> bool {
for key in TranslatedKey::iter() {
let mut is_available = false;
for strs in &ls.kvs {
if strs.key == key {
is_available = true;
}
}
if !is_available {
return false;
}
}
true
}
macro_rules! add_language {
($func_name:ident, $file_name:expr) => {
pub fn $func_name() -> LanguageStrings {
let strings = serde_xml_rs::from_str(include_str!($file_name)).unwrap();
if !verify_resources(&strings) {
panic!("The language XML for {} is not correct.", $file_name);
}
strings
}
}
}
add_language!(en_lang, "../translations/en.xml");
add_language!(fr_lang, "../translations/fr.xml");

@ -87,7 +87,7 @@ ORDER BY
goals DESC,
players.name;
"#;
sqlx::query_as::<_, PlayerStats>(&query)
sqlx::query_as::<_, PlayerStats>(query)
.bind(game.id)
.fetch_all(pool)
.await
@ -106,7 +106,7 @@ WHERE players.id=$1
ORDER BY leagues.end_date DESC
LIMIT 1;
"#;
sqlx::query_as::<_, League>(&query)
sqlx::query_as::<_, League>(query)
.bind(player.id)
.fetch_optional(pool)
.await
@ -155,7 +155,7 @@ SELECT
FROM players
WHERE id=$1;
"#;
sqlx::query_as::<_, PlayerStats>(&query)
sqlx::query_as::<_, PlayerStats>(query)
.bind(player.id)
.bind(league.id)
.fetch_one(pool)
@ -218,11 +218,10 @@ ORDER BY
shots.period_time ASC
LIMIT 5;
"#;
let x =sqlx::query_as::<_, GoalDetails>(&query)
.bind(player.id);
//.fetch_all(pool)
//.await
x.fetch_all(pool).await
sqlx::query_as::<_, GoalDetails>(query)
.bind(player.id)
.fetch_all(pool)
.await
}
pub async fn get_all_player_stats(pool: &PgPool, player: &Player) -> Result<PlayerStats, sqlx::Error> {
@ -253,7 +252,7 @@ SELECT
FROM players
WHERE id=$1;
"#;
sqlx::query_as::<_, PlayerStats>(&query)
sqlx::query_as::<_, PlayerStats>(query)
.bind(player.id)
.fetch_one(pool)
.await
@ -417,7 +416,7 @@ ORDER BY
}
pub async fn get_score_from_game(pool: &PgPool, game: &Game) -> Result<Vec<TeamStats>, sqlx::Error> {
let query = format!(r#"
let query = r#"
SELECT
(
SELECT COUNT(shots.id)
@ -438,8 +437,8 @@ SELECT
FROM games
JOIN teams ON teams.id=games.team_home OR teams.id=games.team_away
WHERE games.id=$1;
"#);
sqlx::query_as::<_, TeamStats>(&query)
"#;
sqlx::query_as::<_, TeamStats>(query)
.bind(game.id)
.fetch_all(pool)
.await
@ -473,10 +472,9 @@ ORDER BY
goals DESC,
players.name;
"#;
let result = sqlx::query_as::<_, PlayerStats>(query)
sqlx::query_as::<_, PlayerStats>(query)
.fetch_all(&pool)
.await;
result
.await
}
#[cfg(test)]

@ -1,6 +1,6 @@
<h1 id="leagues">IBIHF Leagues</h1>
<h1 id="leagues">{{ heading }}</h1>
<ol aria-labelledby="leagues">
{% for league in leagues %}
<li><a href="/league/{{ league.id }}/">{{ league.name }}</a></li>
<li><a href="/{{ lang }}/league/{{ league.id }}/">{{ league.name }}</a></li>
{% endfor %}
</ol>

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="urlGame">game</string>
<string name="urlDivision">division</string>
<string name="urlLeague">league</string>
<string name="ibihfLeagues">IBIHF Leagues</string>
<string name="goals">Goals</string>
<string name="assists">Assists</string>
<string name="period">Period</string>
</resources>

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="urlGame">match</string>
<string name="urlDivision">division</string>
<string name="urlLeague">league</string>
<string name="ibihfLeagues">Leagues de FIDHS</string>
<string name="goals">But</string>
<string name="assists">Assisté</string>
<string name="period">Période</string>
</resources>
Loading…
Cancel
Save