(#256): Fix matching of the systemLanguage attribute with the user's locale

The SVG spec says that the systemLanguage attribute is a
comma-separated list of BCP47 language tags, but librsvg was assuming
that it was a list of X/Open locale names (e.g. what one would set in
the LC_MESSAGES or LANG environment variables).

Now, we convert the output of g_get_language_names() to BCP47 language
tags using the locale_config crate.

Then, we use the language_tags crate to actually match those tags
against the values from the systemLanguage attribute.

#256
parent 12926090
use error::*;
use std::marker::PhantomData;
#[allow(unused_imports, deprecated)]
use std::ascii::AsciiExt;
use std::str::FromStr;
use glib;
use itertools::{FoldWhile, Itertools};
use language_tags::LanguageTag;
use locale_config::{LanguageRange, Locale};
use error::*;
use parsers::ParseError;
// No extensions at the moment.
static IMPLEMENTED_EXTENSIONS: &[&str] = &[];
......@@ -61,35 +68,103 @@ impl RequiredFeatures {
}
#[derive(Debug, PartialEq)]
pub struct SystemLanguage<'a>(pub bool, pub PhantomData<&'a i8>);
impl<'a> SystemLanguage<'a> {
// Parse a systemLanguage attribute
// http://www.w3.org/TR/SVG/struct.html#SystemLanguageAttribute
pub fn from_attribute(
s: &str,
system_languages: &[String],
) -> Result<SystemLanguage<'a>, ValueErrorKind> {
Ok(SystemLanguage(
s.split(',')
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.any(|l| {
system_languages.iter().any(|sl| {
if sl.eq_ignore_ascii_case(l) {
return true;
}
pub struct SystemLanguage(pub bool);
if let Some(offset) = l.find('-') {
return sl.eq_ignore_ascii_case(&l[..offset]);
impl SystemLanguage {
/// Parse a `systemLanguage` attribute and match it against a given `Locale`
///
/// The [`systemLanguage`] conditional attribute is a
/// comma-separated list of [BCP47] Language Tags. This function
/// parses the attribute and matches the result against a given
/// `locale`. If there is a match, i.e. if the given locale
/// supports one of the languages listed in the `systemLanguage`
/// attribute, then the `SystemLanguage.0` will be `true`;
/// otherwise it will be `false`.
///
/// Normally, calling code will pass `&Locale::current()` for the
/// `locale` attribute; this is the user's current locale.
///
/// [`systemLanguage`]: https://www.w3.org/TR/SVG/struct.html#ConditionalProcessingSystemLanguageAttribute
/// [BCP47]: http://www.ietf.org/rfc/bcp/bcp47.txt
pub fn from_attribute(s: &str, locale: &Locale) -> Result<SystemLanguage, ValueErrorKind> {
s.split(',')
.map(LanguageTag::from_str)
.fold_while(
// start with no match
Ok(SystemLanguage(false)),
// The accumulator is Result<SystemLanguage, ValueErrorKind>
|acc, tag_result| match tag_result {
Ok(language_tag) => {
let have_match = acc.unwrap().0;
if have_match {
FoldWhile::Continue(Ok(SystemLanguage(have_match)))
} else {
locale_accepts_language_tag(locale, &language_tag)
.map(|matches| FoldWhile::Continue(Ok(SystemLanguage(matches))))
.unwrap_or_else(|e| FoldWhile::Done(Err(e)))
}
}
false
})
}),
PhantomData,
))
Err(e) => FoldWhile::Done(Err(ValueErrorKind::Parse(ParseError::new(
&format!("invalid language tag: \"{}\"", e),
)))),
},
)
.into_inner()
}
}
/// Gets the user's preferred locale from the environment and
/// translates it to a `Locale` with `LanguageRange` fallbacks.
///
/// The `Locale::current()` call only contemplates a single language,
/// but glib is smarter, and `g_get_langauge_names()` can provide
/// fallbacks, for example, when LC_MESSAGES="en_US.UTF-8:de" (USA
/// English and German). This function converts the output of
/// `g_get_language_names()` into a `Locale` with appropriate
/// fallbacks.
pub fn locale_from_environment() -> Result<Locale, String> {
let mut locale = Locale::invariant();
for name in glib::get_language_names() {
let range = LanguageRange::from_unix(&name).map_err(|e| format!("{}", e))?;
locale.add(&range);
}
Ok(locale)
}
fn locale_accepts_language_tag(
locale: &Locale,
language_tag: &LanguageTag,
) -> Result<bool, ValueErrorKind> {
for locale_range in locale.tags_for("messages") {
if locale_range == LanguageRange::invariant() {
continue;
}
let str_locale_range = locale_range.as_ref();
let locale_tag = LanguageTag::from_str(str_locale_range).map_err(|e| {
ValueErrorKind::Parse(ParseError::new(&format!(
"invalid language tag \"{}\" in locale: {}",
str_locale_range, e
)))
})?;
if !locale_tag.is_language_range() {
return Err(ValueErrorKind::Value(format!(
"language tag \"{}\" is not a language range",
locale_tag
)));
}
if locale_tag.matches(language_tag) {
return Ok(true);
}
}
Ok(false)
}
#[cfg(test)]
......@@ -135,41 +210,50 @@ mod tests {
#[test]
fn system_language() {
let system_languages = vec![String::from("de"), String::from("en_US")];
let user_prefers = Locale::new("de,en-US").unwrap();
assert!(SystemLanguage::from_attribute("", &user_prefers).is_err());
assert!(SystemLanguage::from_attribute("12345", &user_prefers).is_err());
assert_eq!(
SystemLanguage::from_attribute("fr", &user_prefers),
Ok(SystemLanguage(false))
);
assert_eq!(
SystemLanguage::from_attribute("", &system_languages),
Ok(SystemLanguage(false, PhantomData))
SystemLanguage::from_attribute("en", &user_prefers),
Ok(SystemLanguage(false))
);
assert_eq!(
SystemLanguage::from_attribute("fr", &system_languages),
Ok(SystemLanguage(false, PhantomData))
SystemLanguage::from_attribute("de", &user_prefers),
Ok(SystemLanguage(true))
);
assert_eq!(
SystemLanguage::from_attribute("de", &system_languages),
Ok(SystemLanguage(true, PhantomData))
SystemLanguage::from_attribute("en-US", &user_prefers),
Ok(SystemLanguage(true))
);
assert_eq!(
SystemLanguage::from_attribute("en_US", &system_languages),
Ok(SystemLanguage(true, PhantomData))
SystemLanguage::from_attribute("en-GB", &user_prefers),
Ok(SystemLanguage(false))
);
assert_eq!(
SystemLanguage::from_attribute("DE", &system_languages),
Ok(SystemLanguage(true, PhantomData))
SystemLanguage::from_attribute("DE", &user_prefers),
Ok(SystemLanguage(true))
);
assert_eq!(
SystemLanguage::from_attribute("de-LU", &system_languages),
Ok(SystemLanguage(true, PhantomData))
SystemLanguage::from_attribute("de-LU", &user_prefers),
Ok(SystemLanguage(true))
);
assert_eq!(
SystemLanguage::from_attribute("fr, de", &system_languages),
Ok(SystemLanguage(true, PhantomData))
SystemLanguage::from_attribute("fr, de", &user_prefers),
Ok(SystemLanguage(true))
);
}
}
......@@ -10,7 +10,9 @@ extern crate gdk_pixbuf;
extern crate glib;
extern crate glib_sys;
extern crate itertools;
extern crate language_tags;
extern crate libc;
extern crate locale_config;
extern crate nalgebra;
extern crate num_traits;
extern crate owning_ref;
......
use cairo::{Matrix, MatrixTrait};
use downcast_rs::*;
use glib;
use glib::translate::*;
use glib_sys;
use std::cell::{Cell, Ref, RefCell};
......@@ -8,7 +7,7 @@ use std::ptr;
use std::rc::{Rc, Weak};
use attributes::Attribute;
use cond::{RequiredExtensions, RequiredFeatures, SystemLanguage};
use cond::{locale_from_environment, RequiredExtensions, RequiredFeatures, SystemLanguage};
use drawing_ctx::DrawingCtx;
use error::*;
use handle::{self, RsvgHandle};
......@@ -390,8 +389,11 @@ impl Node {
}
Attribute::SystemLanguage if cond => {
cond = SystemLanguage::from_attribute(value, &glib::get_language_names())
.map(|SystemLanguage(res, _)| res)?;
cond = SystemLanguage::from_attribute(
value,
&(locale_from_environment().map_err(|e| ValueErrorKind::Value(e))?),
)
.map(|SystemLanguage(res)| res)?;
}
_ => {}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment