wip: localization

This commit is contained in:
Hannaeko 2025-04-01 20:03:15 +02:00
parent 7cee790c85
commit 1fd5ce890b
12 changed files with 383 additions and 21 deletions

124
Cargo.lock generated
View file

@ -283,6 +283,17 @@ dependencies = [
"crypto-common", "crypto-common",
] ]
[[package]]
name = "displaydoc"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "domain" name = "domain"
version = "0.10.3" version = "0.10.3"
@ -334,6 +345,40 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "fluent-bundle"
version = "0.15.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fe0a21ee80050c678013f82edf4b705fe2f26f1f9877593d13198612503f493"
dependencies = [
"fluent-langneg",
"fluent-syntax",
"intl-memoizer",
"intl_pluralrules",
"rustc-hash",
"self_cell 0.10.3",
"smallvec",
"unic-langid",
]
[[package]]
name = "fluent-langneg"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c4ad0989667548f06ccd0e306ed56b61bd4d35458d54df5ec7587c0e8ed5e94"
dependencies = [
"unic-langid",
]
[[package]]
name = "fluent-syntax"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a530c4694a6a8d528794ee9bbd8ba0122e779629ac908d15ad5a7ae7763a33d"
dependencies = [
"thiserror 1.0.69",
]
[[package]] [[package]]
name = "fnv" name = "fnv"
version = "1.0.7" version = "1.0.7"
@ -573,6 +618,25 @@ dependencies = [
"winapi-util", "winapi-util",
] ]
[[package]]
name = "intl-memoizer"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe22e020fce238ae18a6d5d8c502ee76a52a6e880d99477657e6acc30ec57bda"
dependencies = [
"type-map",
"unic-langid",
]
[[package]]
name = "intl_pluralrules"
version = "7.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972"
dependencies = [
"unic-langid",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.14" version = "1.0.14"
@ -707,12 +771,15 @@ dependencies = [
"axum", "axum",
"bb8", "bb8",
"domain", "domain",
"fluent-bundle",
"rusqlite", "rusqlite",
"serde", "serde",
"serde_json", "serde_json",
"tera", "tera",
"tokio", "tokio",
"tower",
"tower-http", "tower-http",
"unic-langid",
] ]
[[package]] [[package]]
@ -1004,6 +1071,12 @@ version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]] [[package]]
name = "rustc_version" name = "rustc_version"
version = "0.4.1" version = "0.4.1"
@ -1040,6 +1113,21 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "self_cell"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e14e4d63b804dc0c7ec4a1e52bcb63f02c7ac94476755aa579edac21e01f915d"
dependencies = [
"self_cell 1.1.0",
]
[[package]]
name = "self_cell"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2fdfc24bc566f839a2da4c4295b82db7d25a24253867d5c64355abb5799bdbe"
[[package]] [[package]]
name = "semver" name = "semver"
version = "1.0.23" version = "1.0.23"
@ -1246,6 +1334,15 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "tinystr"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
dependencies = [
"displaydoc",
]
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.42.0" version = "1.42.0"
@ -1376,6 +1473,15 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "859eb650cfee7434994602c3a68b25d77ad9e68c8a6cd491616ef86661382eb3" checksum = "859eb650cfee7434994602c3a68b25d77ad9e68c8a6cd491616ef86661382eb3"
[[package]]
name = "type-map"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "deb68604048ff8fa93347f02441e4487594adc20bb8a084f9e564d2b827a0a9f"
dependencies = [
"rustc-hash",
]
[[package]] [[package]]
name = "typenum" name = "typenum"
version = "1.17.0" version = "1.17.0"
@ -1409,6 +1515,24 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc"
[[package]]
name = "unic-langid"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23dd9d1e72a73b25e07123a80776aae3e7b0ec461ef94f9151eed6ec88005a44"
dependencies = [
"unic-langid-impl",
]
[[package]]
name = "unic-langid-impl"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a5422c1f65949306c99240b81de9f3f15929f5a8bfe05bb44b034cc8bf593e5"
dependencies = [
"tinystr",
]
[[package]] [[package]]
name = "unic-segment" name = "unic-segment"
version = "0.9.0" version = "0.9.0"

View file

@ -20,8 +20,11 @@ tokio = {version = "1", default-features = false, features = [ "macros", "rt-mul
#rand = "0.8" #rand = "0.8"
tera = { version = "1", default-features = false } tera = { version = "1", default-features = false }
domain = { version = "0.10.3", features = [ "tsig", "unstable-client-transport" ]} domain = { version = "0.10.3", features = [ "tsig", "unstable-client-transport" ]}
axum = { version = "0.8.1", default-features = false, features = [ "http1", "json", "form", "query", "tokio" ]} axum = { version = "0.8.1", default-features = false, features = [ "http1", "json", "form", "query", "tokio", "original-uri" ]}
bb8 = { version = "0.9" } bb8 = { version = "0.9" }
rusqlite = { version = "0.32"} rusqlite = { version = "0.32"}
async-trait = { version = "0.1" } async-trait = { version = "0.1" }
tower-http = { version = "0.6", default-features = false, features = [ "fs" ]} tower-http = { version = "0.6", default-features = false, features = [ "fs" ]}
fluent-bundle = "0.15.3"
unic-langid = "*"
tower = "*"

5
locales/en.ftl Normal file
View file

@ -0,0 +1,5 @@
zone_records_title = Zone { $zone_name } records
record_type_address = Addresses
record_type_mailserver = E-mail servers
record_type_nameserver = Name servers

5
locales/fr.ftl Normal file
View file

@ -0,0 +1,5 @@
zone_records_title = Enregistrements de la zone { $zone_name }
record_type_address = Adresses
record_type_mailserver = Serveurs de courriel
record_type_nameserver = Serveurs de noms

View file

@ -372,11 +372,19 @@ impl From<TemplateError> for Error {
fn from(value: TemplateError) -> Self { fn from(value: TemplateError) -> Self {
match value { match value {
TemplateError::RenderError { name, reason } => { TemplateError::RenderError { name, reason } => {
let mut this_reason = reason.as_ref();
let mut cause = format!("{}", this_reason);
while let Some(source) = this_reason.source() {
cause.push_str(&format!(": {source}"));
this_reason = source;
}
Error::new("template:render", "Failed to render the template") Error::new("template:render", "Failed to render the template")
.with_details(json!({ .with_details(json!({
"name": name "name": name
})) }))
.with_cause(&reason.to_string()) .with_cause(&cause)
}, },
TemplateError::SerializationError { reason } => { TemplateError::SerializationError { reason } => {
Error::new("template:serialization", "Failed to serialize context") Error::new("template:serialization", "Failed to serialize context")

192
src/locale.rs Normal file
View file

@ -0,0 +1,192 @@
use std::{collections::HashMap, str::FromStr};
use fluent_bundle::{concurrent::FluentBundle, FluentArgs, FluentResource, FluentValue};
use unic_langid::LanguageIdentifier;
const SOURCES: &[(&str, &str)] = &[
("en", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/locales/en.ftl"))),
("fr", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/locales/fr.ftl"))),
];
#[derive(Clone)]
pub struct Localization {
bundles: std::sync::Arc<HashMap<LanguageIdentifier, FluentBundle<FluentResource>>>,
}
impl Localization {
pub fn init() -> Self {
let mut bundles = HashMap::new();
for (locale, translations) in SOURCES {
let res = FluentResource::try_new(translations.to_string()).expect("Failed to parse an FTL string.");
let langid: LanguageIdentifier = locale.parse().expect("Parsing failed");
let mut bundle = FluentBundle::new_concurrent(vec![langid.clone()]);
bundle
.add_resource(res)
.expect("Failed to add FTL resources to the bundle.");
bundles.insert(langid, bundle);
}
Localization {
bundles: std::sync::Arc::new(bundles),
}
}
pub fn language_middleware(&self, default_locale: LanguageIdentifier) -> ExtractLanguageLayer {
ExtractLanguageLayer {
default_locale,
supported_locales: std::sync::Arc::new(self.bundles.keys().cloned().collect())
}
}
}
impl tera::Function for Localization {
fn call(&self, args: &HashMap<String, tera::Value>) -> tera::Result<tera::Value> {
let locale = args.get("lang")
.and_then(|lang| lang.as_str())
.and_then(|lang| LanguageIdentifier::from_str(lang).ok())
.ok_or(tera::Error::msg("localize: Missing lang parameter"))?;
let msg = args.get("msg")
.and_then(|lang| lang.as_str())
.ok_or(tera::Error::msg("localize: Missing msg parameter"))?;
let bundle = self.bundles.get(&locale)
.ok_or_else(|| tera::Error::msg("localize: Could not find locale {locale}"))?;
let message = bundle.get_message(msg)
.ok_or_else(|| tera::Error::msg("localize: Could not find message {msg}"))?;
let pattern = message.value()
.ok_or_else(|| tera::Error::msg("localize: Message {msg} has no value"))?;
let mut msg_args = FluentArgs::new();
for (key, value) in args {
if key != "msg" && key != "lang" {
msg_args.set(key, fluent_value_from_tera(value, key)?);
}
}
let mut errors = Vec::new();
let localized_message = bundle.format_pattern(pattern, Some(&msg_args), &mut errors);
for err in errors {
eprint!("[warn] localization error: {err}");
}
Ok(tera::Value::from(localized_message))
}
}
fn fluent_value_from_tera<'s>(value: &'s tera::Value, name: &str) -> tera::Result<FluentValue<'s>> {
match value {
tera::Value::String(string) => Ok(FluentValue::from(string)),
tera::Value::Number(number) => Ok(FluentValue::from(number.as_f64())),
_ => Err(tera::Error::msg(format!("localize: Argument {name} can only be a string or a number"))),
}
}
#[derive(Clone)]
pub struct ExtractLanguageLayer {
default_locale: LanguageIdentifier,
supported_locales: std::sync::Arc<Vec<LanguageIdentifier>>,
}
impl<S> tower::Layer<S> for ExtractLanguageLayer {
type Service = ExtractLanguageService<S>;
fn layer(&self, inner: S) -> Self::Service {
ExtractLanguageService {
inner,
default_locale: self.default_locale.clone(),
supported_locales: self.supported_locales.clone(),
}
}
}
#[derive(Clone)]
pub struct ExtractLanguageService<S> {
inner: S,
default_locale: LanguageIdentifier,
supported_locales: std::sync::Arc<Vec<LanguageIdentifier>>,
}
impl<S> ExtractLanguageService<S> {
// https://httpwg.org/specs/rfc9110.html#field.accept-language
pub fn language_from_header(&self, headers: &axum::http::HeaderMap) -> LanguageIdentifier {
let lang_preferences = headers
.get("Accept-Language")
.and_then(|val| val.to_str().ok());
if let Some(lang_preferences) = lang_preferences {
let mut languages = Vec::new();
for lang_item in lang_preferences.split(",") {
let lang_item = lang_item.trim();
let lang_config = lang_item.split_once(';');
if let Some((lang_id, config)) = lang_config {
let lang_id = lang_id.trim();
let preference = config.trim().strip_prefix("q=")
.and_then(|value| value.parse::<f32>().ok());
if let Some(preference) = preference {
languages.push((lang_id.trim(), preference));
}
} else {
languages.push((lang_item, 1f32));
}
}
languages.sort_by(|l1, l2| l1.1.total_cmp(&l2.1));
let iter = languages.into_iter()
.rev()
.filter(|l| l.1 >= 0.001 && l.1 <= 1f32)
.filter_map(|(lang, _)| LanguageIdentifier::from_str(lang).ok());
for lang in iter {
for supported_lang in self.supported_locales.iter() {
if lang == *supported_lang {
return lang;
}
}
}
self.default_locale.clone()
} else {
self.default_locale.clone()
}
}
}
impl<S> tower::Service<axum::extract::Request> for ExtractLanguageService<S>
where
S: tower::Service<axum::extract::Request, Response = axum::response::Response> + Send + 'static,
S::Future: Send + 'static,
{
type Response = S::Response;
type Error = S::Error;
// `BoxFuture` is a type alias for `Pin<Box<dyn Future + Send + 'a>>`
type Future = std::pin::Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send + 'static>>;
fn poll_ready(&mut self, cx: &mut std::task::Context<'_>) -> std::task::Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, mut req: axum::extract::Request) -> Self::Future {
let headers = req.headers();
let lang = self.language_from_header(headers);
req.extensions_mut().insert(lang);
Box::pin(self.inner.call(req))
}
}

View file

@ -7,6 +7,7 @@ mod validation;
mod macros; mod macros;
mod template; mod template;
mod proto; mod proto;
mod locale;
use std::sync::Arc; use std::sync::Arc;
@ -32,7 +33,12 @@ pub struct AppState {
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
let template_engine = TemplateEngine::new(std::path::Path::new("./templates")); let localization = locale::Localization::init();
let template_engine = TemplateEngine::new(
std::path::Path::new("./templates"),
localization.clone()
);
let dns_driver = dns::dns_driver::DnsDriver::from_config(DnsDriverConfig { let dns_driver = dns::dns_driver::DnsDriver::from_config(DnsDriverConfig {
address: "127.0.0.1:5353".parse().unwrap(), address: "127.0.0.1:5353".parse().unwrap(),
@ -61,7 +67,8 @@ async fn main() {
.route("/zones/{zone_name}/records", routing::get(routes::ui::zones::get_records_page)) .route("/zones/{zone_name}/records", routing::get(routes::ui::zones::get_records_page))
.route("/zones/{zone_name}/records/new", routing::get(routes::ui::zones::get_new_record_page)) .route("/zones/{zone_name}/records/new", routing::get(routes::ui::zones::get_new_record_page))
.nest_service("/assets", ServeDir::new("assets")) .nest_service("/assets", ServeDir::new("assets"))
.with_state(app_state); .with_state(app_state)
.layer(localization.language_middleware("en".parse().unwrap()));
let listener = tokio::net::TcpListener::bind("127.0.0.1:8000").await.unwrap(); let listener = tokio::net::TcpListener::bind("127.0.0.1:8000").await.unwrap();
axum::serve(listener, app).await.unwrap(); axum::serve(listener, app).await.unwrap();

View file

@ -1,7 +1,9 @@
use axum::extract::Request; use axum::extract::Request;
use axum::extract::{Query, Path, State}; use axum::extract::{Query, Path, State, OriginalUri};
use axum::Extension;
use serde::Deserialize; use serde::Deserialize;
use serde_json::{Value, json}; use serde_json::{Value, json};
use unic_langid::LanguageIdentifier;
use crate::validation; use crate::validation;
use crate::AppState; use crate::AppState;
@ -12,19 +14,23 @@ use crate::resources::dns::friendly;
pub async fn get_records_page( pub async fn get_records_page(
Path(zone_name): Path<String>, Path(zone_name): Path<String>,
State(app): State<AppState>, State(app): State<AppState>,
request: Request, OriginalUri(url): OriginalUri,
Extension(lang): Extension<LanguageIdentifier>,
) -> Result<Template<'static, Value>, Error> { ) -> Result<Template<'static, Value>, Error> {
let zone = app.db.get_zone_by_name(&zone_name).await?; let zone = app.db.get_zone_by_name(&zone_name).await?;
let records = zone.get_records(app.records).await?; let records = zone.get_records(app.records).await?;
let records = friendly::FriendlyRecords::from(records.clone()); let records = friendly::FriendlyRecords::from(records.clone());
println!("{}", lang);
Ok(Template::new( Ok(Template::new(
"pages/records.html", "pages/records.html",
app.template_engine, app.template_engine,
json!({ json!({
"current_zone": zone.name, "current_zone": zone.name,
"records": records, "records": records,
"url": request.uri().to_string(), "url": url.to_string(),
"lang": lang.to_string(),
}) })
)) ))
} }
@ -40,7 +46,8 @@ pub async fn get_new_record_page(
Path(zone_name): Path<String>, Path(zone_name): Path<String>,
State(app): State<AppState>, State(app): State<AppState>,
Query(params): Query<NewRecordQuery>, Query(params): Query<NewRecordQuery>,
request: Request, OriginalUri(url): OriginalUri,
Extension(lang): Extension<LanguageIdentifier>,
) -> Result<Template<'static, Value>, Error> { ) -> Result<Template<'static, Value>, Error> {
let zone = app.db.get_zone_by_name(&zone_name).await?; let zone = app.db.get_zone_by_name(&zone_name).await?;
@ -60,6 +67,8 @@ pub async fn get_new_record_page(
(None, None) (None, None)
}; };
println!("{}", lang);
Ok(Template::new( Ok(Template::new(
"pages/new_record.html", "pages/new_record.html",
app.template_engine, app.template_engine,
@ -69,7 +78,8 @@ pub async fn get_new_record_page(
"domain_error": domain_error, "domain_error": domain_error,
"config": params.config, "config": params.config,
"rtype": params.rtype, "rtype": params.rtype,
"url": request.uri().to_string(), "url": url.to_string(),
"lang": lang.to_string(),
}) })
)) ))
} }

View file

@ -6,24 +6,28 @@ use axum::response::{Html, IntoResponse};
use serde::Serialize; use serde::Serialize;
use tera::{Tera, Context}; use tera::{Tera, Context};
use crate::errors::Error; use crate::{errors::Error, locale::Localization};
#[derive(Clone)] #[derive(Clone)]
pub struct TemplateEngine { pub struct TemplateEngine {
tera: Arc<Tera>, pub tera: Arc<Tera>,
} }
#[derive(Debug)]
pub enum TemplateError { pub enum TemplateError {
SerializationError { reason: Box<dyn std::error::Error> }, SerializationError { reason: Box<dyn std::error::Error> },
RenderError { name: String, reason: Box<dyn std::error::Error> }, RenderError { name: String, reason: Box<dyn std::error::Error> },
} }
impl TemplateEngine { impl TemplateEngine {
pub fn new(template_directory: &Path) -> Self { pub fn new(template_directory: &Path, localization: Localization) -> Self {
let template_glob = template_directory.join("**").join("*"); let template_glob = template_directory.join("**").join("*");
match Tera::new(template_glob.to_str().expect("valid glob path string")) { match Tera::new(template_glob.to_str().expect("valid glob path string")) {
Ok(tera) => TemplateEngine { tera: Arc::new(tera) }, Ok(mut tera) => {
tera.register_function("tr", localization);
TemplateEngine { tera: Arc::new(tera) }
},
Err(e) => { Err(e) => {
println!("Loading templates failed: {}", e); println!("Loading templates failed: {}", e);
exit(1) exit(1)

View file

@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="fr"> <html lang="{{ lang }}">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">

View file

@ -1,7 +1,7 @@
{% macro rrset(rtype, ttl, data, zone) %} {% macro rrset(rtype, ttl, data, zone, lang) %}
<li class="rrset"> <li class="rrset">
<div class="rtype"> <div class="rtype">
{{ rtype }} {{ tr(msg="record_type_" ~ rtype, lang=lang) }}
<div class="action"> <div class="action">
<button class="icon"> <button class="icon">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">

View file

@ -4,7 +4,7 @@
{% block title %}Records - {{ current_zone }} - {% endblock title %} {% block title %}Records - {{ current_zone }} - {% endblock title %}
{% block main %} {% block main %}
<h1>Zone <strong>{{ current_zone }}</strong> records</h1> <h1>{{ tr(msg="zone_records_title", lang=lang, zone_name="<strong>" ~ current_zoned ~ "</strong>") | safe }}</h1>
<svg width="0" height="0" aria-hidden="true" style="position: absolute;"> <svg width="0" height="0" aria-hidden="true" style="position: absolute;">
<defs> <defs>
<clipPath id="corner-folder-tab-right" clipPathUnits="objectBoundingBox"> <clipPath id="corner-folder-tab-right" clipPathUnits="objectBoundingBox">
@ -36,7 +36,8 @@
rtype=group.web.addresses.rtype, rtype=group.web.addresses.rtype,
ttl=group.web.addresses.ttl, ttl=group.web.addresses.ttl,
data=group.web.addresses.data, data=group.web.addresses.data,
zone=current_zone) }} zone=current_zone,
lang=lang) }}
{% endif %} {% endif %}
</ul> </ul>
{% endif %} {% endif %}
@ -49,14 +50,16 @@
rtype=group.mail.servers.rtype, rtype=group.mail.servers.rtype,
ttl=group.mail.servers.ttl, ttl=group.mail.servers.ttl,
data=group.mail.servers.data, data=group.mail.servers.data,
zone=current_zone) }} zone=current_zone,
lang=lang) }}
{% endif %} {% endif %}
{% if group.mail.spf %} {% if group.mail.spf %}
{{ macros::rrset( {{ macros::rrset(
rtype=group.mail.spf.rtype, rtype=group.mail.spf.rtype,
ttl=group.mail.spf.ttl, ttl=group.mail.spf.ttl,
data=group.mail.spf.data, data=group.mail.spf.data,
zone=current_zone) }} zone=current_zone,
lang=lang) }}
{% endif %} {% endif %}
</ul> </ul>
{% endif %} {% endif %}
@ -69,7 +72,8 @@
rtype=rrset.rtype, rtype=rrset.rtype,
ttl=rrset.ttl, ttl=rrset.ttl,
data=rrset.data, data=rrset.data,
zone=current_zone) }} zone=current_zone,
lang=lang) }}
{% endfor %} {% endfor %}
</ul> </ul>
{% endif %} {% endif %}