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

Validation #2

Merged
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
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