forked from mirrors/gecko-dev
This passes the profile directory as a crash annotation (which is not sent in the crash report). The profile directory is already indirectly passed to the crash reporter through `MOZ_CRASHREPORTER_EVENTS_DIRECTORY`, however I don't think that's something to rely upon. The pref parsing and langpack extension search are done on a best-effort basis; there may be some odd configurations for which it does not account. However, it should cover the vast majority of cases. While we don't want to underserve niche populations that may have an odd configuration, we do always have fallback behaviors that are likely still reasonable for most (like using the installation locale). We also first try the `useragent_locale` annotation before getting locales from the prefs. This only looks in the profile and the installation for langpack extensions. There are a few other system-wide locations that Firefox looks in. However, the assumptions are: 1) overwhelmingly users install langpacks using the UI rather than manually downloading the file, and 2) if installed with a package manager (mostly applicable to linux), langpacks are put in the installation. This is the case for the few major linux distros I checked. If we think this is not enough, I can also add the system-wide directories. Original Revision: https://phabricator.services.mozilla.com/D222356 Differential Revision: https://phabricator.services.mozilla.com/D234210
190 lines
6.2 KiB
Rust
190 lines
6.2 KiB
Rust
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
use super::language_info::LanguageInfo;
|
|
use super::zip::{read_archive_file_as_string, read_zip, Archive};
|
|
use crate::config::installation_resource_path;
|
|
use crate::std::path::{Path, PathBuf};
|
|
use anyhow::Context;
|
|
|
|
const LOCALE_PREF_KEY: &str = r#""intl.locale.requested""#;
|
|
|
|
/// Use the profile language preferences to determine the localization to use.
|
|
pub fn read(profile_dir: &Path, locale: Option<&str>) -> anyhow::Result<Option<LanguageInfo>> {
|
|
let Some((identifier, langpack)) = select_langpack(profile_dir, locale)? else {
|
|
return Ok(None);
|
|
};
|
|
|
|
let mut zip = read_zip(&langpack)?;
|
|
|
|
let ftl_definitions = read_archive_ftl_definitions(&mut zip, &identifier)?;
|
|
let ftl_branding = read_archive_ftl_branding(&mut zip, &identifier).unwrap_or_else(|e| {
|
|
log::warn!(
|
|
"failed to read branding for {identifier} from {}: {e:#}",
|
|
langpack.display()
|
|
);
|
|
log::info!("using fallback branding info");
|
|
LanguageInfo::default().ftl_branding
|
|
});
|
|
|
|
Ok(Some(LanguageInfo {
|
|
identifier,
|
|
ftl_definitions,
|
|
ftl_branding,
|
|
}))
|
|
}
|
|
|
|
/// Select the langpack to use based on profile settings, the useragent locale, and the existence
|
|
/// of langpack files.
|
|
fn select_langpack(
|
|
profile_dir: &Path,
|
|
locale: Option<&str>,
|
|
) -> anyhow::Result<Option<(String, PathBuf)>> {
|
|
if let Some(path) = locale.and_then(|l| find_langpack_extension(profile_dir, l)) {
|
|
Ok(Some((locale.unwrap().to_string(), path)))
|
|
} else {
|
|
let Some(locales) = locales_from_prefs(profile_dir)? else {
|
|
return Ok(None);
|
|
};
|
|
// Use the first language for which we can find a langpack extension.
|
|
locales
|
|
.iter()
|
|
.find_map(|lang| {
|
|
find_langpack_extension(profile_dir, lang).map(|path| (lang.to_string(), path))
|
|
})
|
|
.with_context(|| {
|
|
format!("couldn't locate langpack for requested locales ({locales:?})")
|
|
})
|
|
.map(Some)
|
|
}
|
|
}
|
|
|
|
fn locales_from_prefs(profile_dir: &Path) -> anyhow::Result<Option<Vec<String>>> {
|
|
let prefs = profile_dir.join("prefs.js");
|
|
if !prefs.exists() {
|
|
log::debug!(
|
|
"no prefs.js exists in {}; not reading localization info",
|
|
profile_dir.display()
|
|
);
|
|
return Ok(None);
|
|
}
|
|
|
|
let prefs_contents = std::fs::read_to_string(&prefs)
|
|
.with_context(|| format!("while reading {}", prefs.display()))?;
|
|
|
|
// Logic dictated by https://firefox-source-docs.mozilla.org/intl/locale.html#requested-locales
|
|
|
|
let Some(langs) = parse_requested_locales(&prefs_contents) else {
|
|
// If the pref is unset, the installation locale should be used.
|
|
log::debug!(
|
|
"no locale pref set in {}; using installation locale",
|
|
prefs.display()
|
|
);
|
|
return Ok(None);
|
|
};
|
|
|
|
Ok(Some(if langs.is_empty() {
|
|
// If the pref is an empty string, use the OS locale settings.
|
|
let os_locales: Vec<String> = sys_locale::get_locales().collect();
|
|
if os_locales.is_empty() {
|
|
log::debug!("no OS locales found");
|
|
return Ok(None);
|
|
}
|
|
log::debug!(
|
|
"locale pref blank in {}; using OS locale settings ({os_locales:?})",
|
|
prefs.display()
|
|
);
|
|
os_locales
|
|
} else {
|
|
langs.into_iter().map(|s| s.to_owned()).collect()
|
|
}))
|
|
}
|
|
|
|
/// Parse the language pref (if any) from the prefs file.
|
|
///
|
|
/// This finds the first string match for the regex `"intl.locale.requested"[ \t\n\r\f\v,]*"(.*)"`,
|
|
/// and splits and trims the first match group, returning the set of strings that results.
|
|
///
|
|
/// For example:
|
|
/// ```rust
|
|
/// let input = r#""intl.locale.requested", "foo , bar,,""#;
|
|
/// let expected_output = Some(vec!["foo","bar"]);
|
|
/// assert_eq!(parse_requested_locales(input), output);
|
|
/// ```
|
|
///
|
|
/// This will parse the locales out of the user prefs file contents, which looks like
|
|
/// `user_pref("intl.locale.requested", "<LOCALE LIST>")`.
|
|
fn parse_requested_locales(prefs_content: &str) -> Option<Vec<&str>> {
|
|
let (_, s) = prefs_content.split_once(LOCALE_PREF_KEY)?;
|
|
let s = s.trim_start_matches(|c: char| c.is_whitespace() || c == ',');
|
|
let s = s.strip_prefix('"')?;
|
|
let (v, _) = s.split_once('"')?;
|
|
Some(
|
|
v.split(",")
|
|
.map(|s| s.trim())
|
|
.filter(|s| !s.is_empty())
|
|
.collect(),
|
|
)
|
|
}
|
|
|
|
/// Find the langpack extension for the given locale.
|
|
fn find_langpack_extension(profile_dir: &Path, locale: &str) -> Option<PathBuf> {
|
|
let path_tail = format!("extensions/langpack-{locale}@firefox.mozilla.org.xpi");
|
|
|
|
// Check profile extensions
|
|
let profile_path = profile_dir.join(&path_tail);
|
|
if profile_path.exists() {
|
|
return Some(profile_path);
|
|
}
|
|
|
|
// Check installation extensions
|
|
let install_path = installation_resource_path().join(format!("browser/{}", &path_tail));
|
|
if install_path.exists() {
|
|
return Some(install_path);
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
fn read_archive_ftl_definitions(
|
|
langpack: &mut Archive,
|
|
language_identifier: &str,
|
|
) -> anyhow::Result<String> {
|
|
let path = format!("localization/{language_identifier}/crashreporter/crashreporter.ftl");
|
|
read_archive_file_as_string(langpack, &path)
|
|
}
|
|
|
|
fn read_archive_ftl_branding(
|
|
langpack: &mut Archive,
|
|
language_identifier: &str,
|
|
) -> anyhow::Result<String> {
|
|
let path = format!("browser/localization/{language_identifier}/branding/brand.ftl");
|
|
read_archive_file_as_string(langpack, &path)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn parse_locales_none() {
|
|
assert_eq!(parse_requested_locales(""), None);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_locales_empty() {
|
|
assert_eq!(
|
|
parse_requested_locales(r#"user_pref("intl.locale.requested","")"#),
|
|
Some(vec![])
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_locales() {
|
|
assert_eq!(
|
|
parse_requested_locales(r#"user_pref("intl.locale.requested", "fr,en-US")"#),
|
|
Some(vec!["fr", "en-US"])
|
|
);
|
|
}
|
|
}
|