Skip to main content

lib_humus/language/
manifest.rs

1// SPDX-FileCopyrightText: 2026 Slatian <baschdel@disroot.org>
2//
3// SPDX-License-Identifier: AGPL-3.0-or-later
4
5use std::collections::{HashMap, HashSet};
6
7use serde::{Deserialize, Serialize};
8
9use crate::language::{UnicodeLanguageIdentifier, variable_description::VariableDescription};
10
11use crate::headers::AcceptLanguageHeader;
12use crate::headers::AcceptLanguageHeaderLanguage;
13use crate::templating::HumusFormatIdentifier;
14
15/// Description of available languages for a [LanguageEngine][crate::language::LanguageEngine].
16///
17/// It is typically read from the `languages/manifest.toml` in the templates directory.
18#[derive(Debug, Clone, Serialize, Deserialize)]
19#[serde(deny_unknown_fields)]
20pub struct LanguageManifest {
21	/// A map of available languages
22	#[serde(rename = "language")]
23	pub languages: HashMap<UnicodeLanguageIdentifier, LanguageDescriptor>,
24	/// The default fallback language that **must** also a valid entry in `languages`.
25	pub default_language: UnicodeLanguageIdentifier,
26	/// Lists all ids that are expected to be in the translation files
27	pub available_messages: HashSet<String>,
28	/// Decribes messages that are more than just an id to language mapping further.
29	#[serde(rename = "message", default)]
30	pub message_descriptions: HashMap<String, MessageDescription>,
31	/// Describes how the language engine should behave regarding different output formats.
32	#[serde(rename = "format", default)]
33	pub formats: HashMap<HumusFormatIdentifier, FormatDescriptor>,
34}
35
36impl LanguageManifest {
37	/// Creates a new language manifest with an undefined language.
38	pub fn new_empty() -> Self {
39		Self {
40			default_language: unic_langid::langid!("und").into(),
41			languages: [(
42				unic_langid::langid!("und").into(),
43				LanguageDescriptor {
44					name: "Undefined".to_string(),
45					abbreviation: "UND".to_string(),
46					is_hidden: false,
47				},
48			)]
49			.into_iter()
50			.collect(),
51			available_messages: Default::default(),
52			message_descriptions: Default::default(),
53			formats: Default::default(),
54		}
55	}
56
57	/// Returns wheather the given language is defined as non-hidden in the manifest
58	pub fn has_visible_language(&self, language: &UnicodeLanguageIdentifier) -> bool {
59		self.languages
60			.get(language)
61			.map(|desc| !desc.is_hidden)
62			.unwrap_or(false)
63	}
64
65	/// Returns the best fitting language that is in the manifest for a given `Accept-Language` header.
66	///
67	/// It returns `None` if there is no explicit match.
68	pub fn get_best_fitting_language(
69		&self,
70		header: &AcceptLanguageHeader,
71	) -> Option<UnicodeLanguageIdentifier> {
72		let mut best_fitting: Option<UnicodeLanguageIdentifier> = None;
73		for (l, _) in &header.languages {
74			match l {
75				AcceptLanguageHeaderLanguage::Wildcard => {
76					// At this point we should have found a matching language, everything after is lower prefeerence
77					break;
78				}
79				AcceptLanguageHeaderLanguage::Code(code) => {
80					if let Ok(mut lang) = code.parse::<UnicodeLanguageIdentifier>() {
81						if self.has_visible_language(&lang) {
82							return Some(lang);
83						}
84						lang.script = None;
85						lang.region = None;
86						if self.has_visible_language(&lang) {
87							best_fitting = Some(lang);
88						}
89					}
90				}
91			}
92		}
93		best_fitting
94	}
95
96	/// Returns the safe suffix for the given formaqt if configured
97	pub fn get_safe_suffix_for_format(&self, format: HumusFormatIdentifier) -> Option<String> {
98		self.formats.get(&format)?.safe_suffix.clone()
99	}
100}
101
102/// Part of the [LanguageManifest], describes a single language.
103#[derive(Debug, Clone, Serialize, Deserialize)]
104#[serde(deny_unknown_fields)]
105pub struct LanguageDescriptor {
106	/// Human readable name of the language to show in language selectors
107	///
108	/// Best practice is to write the language down with it's own name.
109	///
110	/// Example: `English`, `Deutsch`
111	pub name: String,
112
113	/// Human readable abbreviation to show in language selectors
114	pub abbreviation: String,
115
116	/// Weather the entry should be hidden in language selectors, defaults to `false`.
117	///
118	/// Hidden languages are not consideredd when evaluating the `Acept-Language` header.
119	///
120	/// This is useful for testing locales.
121	#[serde(default)]
122	pub is_hidden: bool,
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
126#[serde(deny_unknown_fields)]
127pub struct MessageDescription {
128	#[serde(default, rename = "arg")]
129	pub arguments: HashMap<String, VariableDescription>,
130}
131
132/// Describes an output format for the template
133#[derive(Debug, Clone, Serialize, Deserialize)]
134#[serde(deny_unknown_fields)]
135pub struct FormatDescriptor {
136	/// When a message_id has this suffix its output will automatically be arked as safe and all string arguments will be encoded unless they also have the `safe_suffix`.
137	#[serde(default)]
138	pub safe_suffix: Option<String>,
139}