Skip to content

Commit

Permalink
Merge pull request #2 from LukasKalbertodt/logo-language-switch
Browse files Browse the repository at this point in the history
Validation
  • Loading branch information
owi92 authored Dec 19, 2024
2 parents 1fb48ca + 3aa3a60 commit e379375
Show file tree
Hide file tree
Showing 11 changed files with 102 additions and 28 deletions.
4 changes: 2 additions & 2 deletions .deployment/templates/config.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[general]
site_title.en = "Tobira Test Deployment"
site_title.default = "Tobira Test Deployment"
tobira_url = "https://{% if id != 'main' %}{{id}}.{% endif %}tobira.opencast.org"
users_searchable = true

Expand All @@ -25,7 +25,7 @@ unix_socket_permissions = 0o777
[auth]
source = "tobira-session"
session.from_login_credentials = "login-callback:http+unix://[/opt/tobira/{{ id }}/socket/auth.sock]/"
login_page.note.en = 'Dummy users: "jose", "morgan", "björk" and "sabine". Password for all: "tobira".'
login_page.note.default = 'Dummy users: "jose", "morgan", "björk" and "sabine". Password for all: "tobira".'
login_page.note.de = 'Testnutzer: "jose", "morgan", "björk" und "sabine". Passwort für alle: "tobira".'

trusted_external_key = "tobira"
Expand Down
7 changes: 4 additions & 3 deletions backend/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,12 @@ const TOBIRA_CONFIG_PATH_ENV: &str = "TOBIRA_CONFIG_PATH";
/// units: 'ms', 's', 'min', 'h' and 'd'.
///
/// All user-facing texts you can configure here have to be specified per
/// language, with two letter language key. Only English ('en') is required.
/// Take `general.site_title` for example:
/// language, with two letter language key. The special key 'default' is
/// required and used as fallback for languages that are not specified
/// explicitly. Take `general.site_title` for example:
///
/// [general]
/// site_title.en = "My university"
/// site_title.default = "My university"
/// site_title.de = "Meine Universität"
///
#[derive(Debug, confique::Config)]
Expand Down
83 changes: 77 additions & 6 deletions backend/src/config/theme.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::{fmt, path::PathBuf};
use std::{collections::HashMap, fmt, path::PathBuf};
use serde::{Deserialize, Serialize};

use super::{color::ColorConfig, translated_string::LangKey};
Expand All @@ -20,14 +20,14 @@ pub(crate) struct ThemeConfig {
///
/// ```
/// logos = [
/// { path = "logo-large.svg", resolution = [425, 182] },
/// { path = "logo-large-en.svg", lang = "en", resolution = [425, 182] },
/// { path = "logo-large-dark.svg", mode = "dark", resolution = [425, 182] },
/// { path = "logo-wide-light.svg", mode = "light", size = "wide", resolution = [425, 182] },
/// { path = "logo-wide-dark.svg", mode = "dark", size = "wide", resolution = [425, 182] },
/// { path = "logo-small.svg", size = "narrow", resolution = [212, 182] },
/// ]
/// ```
///
/// See the documentation on theming/logos for more info and additional examples!
#[config(validate = validate_logos)]
pub(crate) logos: Vec<LogoDef>,

/// Colors used in the UI. Specified in sRGB.
Expand All @@ -47,7 +47,7 @@ pub(crate) struct LogoDef {
pub(crate) resolution: LogoResolution,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)]
#[serde(rename_all = "lowercase")]
pub(crate) enum LogoSize {
Wide,
Expand All @@ -60,7 +60,7 @@ impl fmt::Display for LogoSize {
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)]
#[serde(rename_all = "lowercase")]
pub(crate) enum LogoMode {
Light,
Expand Down Expand Up @@ -146,3 +146,74 @@ impl ThemeConfig {
out
}
}

fn validate_logos(logos: &Vec<LogoDef>) -> Result<(), String> {
#[derive()]
enum LangLogo {
Universal(usize),
LangSpecific(HashMap<LangKey, usize>),
}

let all_modes = [LogoMode::Light, LogoMode::Dark];
let all_sizes = [LogoSize::Wide, LogoSize::Narrow];

let mut cases = HashMap::new();
for (i, logo) in logos.iter().enumerate() {
let modes = logo.mode.map(|m| vec![m]).unwrap_or(all_modes.to_vec());
let sizes = logo.size.map(|s| vec![s]).unwrap_or(all_sizes.to_vec());

for &mode in &modes {
for &size in &sizes {
let key = (mode, size);

if let Some(entry) = cases.get_mut(&key) {
let conflicting = match (entry, &logo.lang) {
(LangLogo::LangSpecific(m), Some(lang)) => m.insert(lang.clone(), i),
(LangLogo::LangSpecific(m), None) => m.values().next().copied(),
(LangLogo::Universal(c), _) => Some(*c),
};

if let Some(conflicting) = conflicting {
return Err(format!(
"ambiguous logo definition: \
entry {i} (path: '{curr_path}') conflicts with \
entry {prev_index} (path: '{prev_path}'). \
Both define a {mode} {size} logo, which is only allowed \
if both have different 'lang' keys! Consider adding 'mode' \
or 'size' fields to make entries more specific.",
i = i + 1,
prev_index = conflicting + 1,
curr_path = logo.path.display(),
prev_path = logos[conflicting].path.display(),
));
}
} else {
cases.insert(key, match &logo.lang {
Some(lang) => LangLogo::LangSpecific(HashMap::from([(lang.clone(), i)])),
None => LangLogo::Universal(i),
});
}
}
}
}

// Check that all cases are defined
for mode in all_modes {
for size in all_sizes {
match cases.get(&(mode, size)) {
None => return Err(format!(
"incomplete logo configuration: no {mode} {size} logo defined",
)),
Some(LangLogo::LangSpecific(m)) if !m.contains_key(&LangKey::Default) => {
return Err(format!(
"incomplete logo configuration: {mode} {size} logo is \
missing `lang = '*'` entry",
));
}
_ => {}
}
}
}

Ok(())
}
10 changes: 6 additions & 4 deletions backend/src/config/translated_string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,17 @@ use anyhow::{anyhow, Error};
pub(crate) struct TranslatedString(HashMap<LangKey, String>);

impl TranslatedString {
pub(crate) fn en(&self) -> &str {
&self.0[&LangKey::En]
pub(crate) fn default(&self) -> &str {
&self.0[&LangKey::Default]
}
}

impl TryFrom<HashMap<LangKey, String>> for TranslatedString {
type Error = Error;

fn try_from(map: HashMap<LangKey, String>) -> Result<Self, Self::Error> {
if !map.contains_key(&LangKey::En) {
return Err(anyhow!("Translated string must include 'en' as a language."));
if !map.contains_key(&LangKey::Default) {
return Err(anyhow!("Translated string must include 'default' entry."));
}

Ok(Self(map))
Expand All @@ -36,6 +36,8 @@ impl fmt::Debug for TranslatedString {
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Debug)]
#[serde(rename_all = "lowercase")]
pub(crate) enum LangKey {
#[serde(alias = "*")]
Default,
En,
De,
}
Expand Down
2 changes: 1 addition & 1 deletion backend/src/http/assets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ impl Assets {

builder.add_embedded(INDEX_FILE, &EMBEDS[INDEX_FILE]).with_modifier(deps, {
let frontend_config = frontend_config(config);
let html_title = config.general.site_title.en().to_owned();
let html_title = config.general.site_title.default().to_owned();
let global_style = config.theme.to_css();
let matomo_code = config.matomo.js_code().unwrap_or_default();

Expand Down
12 changes: 6 additions & 6 deletions docs/docs/setup/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
# units: 'ms', 's', 'min', 'h' and 'd'.
#
# All user-facing texts you can configure here have to be specified per
# language, with two letter language key. Only English ('en') is required.
# Take `general.site_title` for example:
# language, with two letter language key. The special key 'default' is
# required and used as fallback for languages that are not specified
# explicitly. Take `general.site_title` for example:
#
# [general]
# site_title.en = "My university"
# site_title.default = "My university"
# site_title.de = "Meine Universität"
#

Expand Down Expand Up @@ -539,9 +540,8 @@
#
# ```
# logos = [
# { path = "logo-large.svg", resolution = [425, 182] },
# { path = "logo-large-en.svg", lang = "en", resolution = [425, 182] },
# { path = "logo-large-dark.svg", mode = "dark", resolution = [425, 182] },
# { path = "logo-wide-light.svg", mode = "light", size = "wide", resolution = [425, 182] },
# { path = "logo-wide-dark.svg", mode = "dark", size = "wide", resolution = [425, 182] },
# { path = "logo-small.svg", size = "narrow", resolution = [212, 182] },
# ]
# ```
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ type SyncConfig = {

type MetadataLabel = "builtin:license" | "builtin:source" | TranslatedString;

export type TranslatedString = { en: string } & Record<"de", string | undefined>;
export type TranslatedString = { default: string } & Record<"en" | "de", string | undefined>;

const CONFIG: Config = parseConfig();
export default CONFIG;
2 changes: 1 addition & 1 deletion frontend/src/ui/InitialConsent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const InitialConsent: React.FC<Props> = ({ consentGiven: initialConsentGi
const currentLanguage = i18n.resolvedLanguage ?? "en";
const usedLang = currentLanguage in notNullish(CONFIG.initialConsent).text
? currentLanguage
: "en";
: "default";

const hash = await calcHash(usedLang);
localStorage.setItem(LOCAL_STORAGE_KEY, `${usedLang}:${hash}`);
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/util/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ export const translatedConfig = (s: TranslatedString, i18n: i18n): string =>
getTranslatedString(s, i18n.resolvedLanguage);

export const getTranslatedString = (s: TranslatedString, lang: string | undefined): string => {
const l = lang ?? "en";
return (l in s ? s[l as keyof TranslatedString] : undefined) ?? s.en;
const l = lang ?? "default";
return (l in s ? s[l as keyof TranslatedString] : undefined) ?? s.default;
};

export const useOnOutsideClick = (
Expand Down
2 changes: 1 addition & 1 deletion frontend/tests/util/isolation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ const tobiraConfig = ({ index, port, dbName, rootPath }: {
rootPath: string;
}) => `
[general]
site_title.en = "Tobira Videoportal"
site_title.default = "Tobira Videoportal"
tobira_url = "http://localhost:${port}"
users_searchable = true
Expand Down
2 changes: 1 addition & 1 deletion util/dev-config/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# developer, you are not interested in this file.

[general]
site_title.en = "Tobira Videoportal"
site_title.default = "Tobira Videoportal"
tobira_url = "http://localhost:8030"
users_searchable = true

Expand Down

0 comments on commit e379375

Please sign in to comment.