Skip to content

Commit 6813dc9

Browse files
committed
Bug 1955387 - Add locale fallback handling to the Rust based search engine selector.
- Introduce a new record type for a list of availablelocales in the search configuration - Implement logic to use the user's locale directly, or strip the regional part for fallback - Map unsupported English locales to en-US
1 parent 93b8c7e commit 6813dc9

File tree

3 files changed

+440
-5
lines changed

3 files changed

+440
-5
lines changed

components/search/src/configuration_types.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,13 +277,22 @@ pub(crate) struct JSONEngineOrdersRecord {
277277
pub orders: Vec<JSONEngineOrder>,
278278
}
279279

280+
/// Represents the available locales record.
281+
#[derive(Debug, Deserialize, Clone)]
282+
#[serde(rename_all = "camelCase")]
283+
pub(crate) struct JSONAvailableLocalesRecord {
284+
/// The available locales in the search config v2.
285+
pub locales: Vec<String>,
286+
}
287+
280288
/// Represents an individual record in the raw search configuration.
281289
#[derive(Debug, Deserialize, Clone)]
282290
#[serde(tag = "recordType", rename_all = "camelCase")]
283291
pub(crate) enum JSONSearchConfigurationRecords {
284292
DefaultEngines(JSONDefaultEnginesRecord),
285293
Engine(Box<JSONEngineRecord>),
286294
EngineOrders(JSONEngineOrdersRecord),
295+
AvailableLocales(JSONAvailableLocalesRecord),
287296
// Include some flexibilty if we choose to add new record types in future.
288297
// Current versions of the application receiving the configuration will
289298
// ignore the new record types.

components/search/src/filter.rs

Lines changed: 114 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ use crate::{
1111
JSONEngineUrls, JSONEngineVariant, JSONSearchConfigurationRecords, RefinedSearchConfig,
1212
SearchEngineDefinition, SearchEngineUrl, SearchEngineUrls, SearchUserEnvironment,
1313
};
14-
use crate::{sort_helpers, JSONEngineOrdersRecord};
14+
use crate::{sort_helpers, JSONAvailableLocalesRecord, JSONEngineOrdersRecord};
1515
use remote_settings::RemoteSettingsRecord;
16+
use std::collections::HashSet;
1617

1718
impl From<JSONEngineUrl> for SearchEngineUrl {
1819
fn from(url: JSONEngineUrl) -> Self {
@@ -143,7 +144,7 @@ pub(crate) struct FilterRecordsResult {
143144
pub(crate) trait Filter {
144145
fn filter_records(
145146
&self,
146-
user_environment: &SearchUserEnvironment,
147+
user_environment: &mut SearchUserEnvironment,
147148
overrides: Option<Vec<JSONOverridesRecord>>,
148149
) -> Result<FilterRecordsResult, Error>;
149150
}
@@ -158,12 +159,48 @@ fn apply_overrides(engines: &mut [SearchEngineDefinition], overrides: &[JSONOver
158159
}
159160
}
160161

162+
fn negotiate_languages(user_environment: &mut SearchUserEnvironment, available_locales: &[String]) {
163+
let user_locale = user_environment.locale.to_lowercase();
164+
165+
let available_locales_set: HashSet<String> = available_locales
166+
.iter()
167+
.map(|locale| locale.to_lowercase())
168+
.collect();
169+
170+
if available_locales_set.contains(&user_locale) {
171+
return;
172+
}
173+
if user_locale.starts_with("en-") {
174+
user_environment.locale = "en-us".to_string();
175+
return;
176+
}
177+
if let Some(index) = user_locale.find('-') {
178+
let base_locale = &user_locale[..index];
179+
if available_locales_set.contains(base_locale) {
180+
user_environment.locale = base_locale.to_string();
181+
}
182+
}
183+
}
184+
161185
impl Filter for Vec<RemoteSettingsRecord> {
162186
fn filter_records(
163187
&self,
164-
user_environment: &SearchUserEnvironment,
188+
user_environment: &mut SearchUserEnvironment,
165189
overrides: Option<Vec<JSONOverridesRecord>>,
166190
) -> Result<FilterRecordsResult, Error> {
191+
let mut available_locales = Vec::new();
192+
for record in self {
193+
if let Some(val) = record.fields.get("recordType") {
194+
if *val == "availableLocales" {
195+
let stringified = serde_json::to_string(&record.fields)?;
196+
let locales_record: Option<JSONAvailableLocalesRecord> =
197+
serde_json::from_str(&stringified)?;
198+
available_locales = locales_record.unwrap().locales;
199+
}
200+
}
201+
}
202+
negotiate_languages(user_environment, &available_locales);
203+
167204
let mut engines = Vec::new();
168205
let mut default_engines_record = None;
169206
let mut engine_orders_record = None;
@@ -188,6 +225,9 @@ impl Filter for Vec<RemoteSettingsRecord> {
188225
Some(val) if *val == "engineOrders" => {
189226
engine_orders_record = serde_json::from_str(&stringified)?;
190227
}
228+
Some(val) if *val == "availableLocales" => {
229+
// Handled above
230+
}
191231
// These cases are acceptable - we expect the potential for new
192232
// record types/options so that we can be flexible.
193233
Some(_val) => {}
@@ -210,9 +250,17 @@ impl Filter for Vec<RemoteSettingsRecord> {
210250
impl Filter for Vec<JSONSearchConfigurationRecords> {
211251
fn filter_records(
212252
&self,
213-
user_environment: &SearchUserEnvironment,
253+
user_environment: &mut SearchUserEnvironment,
214254
overrides: Option<Vec<JSONOverridesRecord>>,
215255
) -> Result<FilterRecordsResult, Error> {
256+
let mut available_locales = Vec::new();
257+
for record in self {
258+
if let JSONSearchConfigurationRecords::AvailableLocales(locales_record) = record {
259+
available_locales = locales_record.locales.clone();
260+
}
261+
}
262+
negotiate_languages(user_environment, &available_locales);
263+
216264
let mut engines = Vec::new();
217265
let mut default_engines_record = None;
218266
let mut engine_orders_record = None;
@@ -229,6 +277,9 @@ impl Filter for Vec<JSONSearchConfigurationRecords> {
229277
JSONSearchConfigurationRecords::EngineOrders(engine_orders) => {
230278
engine_orders_record = Some(engine_orders)
231279
}
280+
JSONSearchConfigurationRecords::AvailableLocales(_) => {
281+
// Handled above
282+
}
232283
JSONSearchConfigurationRecords::Unknown => {
233284
// Prevents panics if a new record type is added in future.
234285
}
@@ -246,6 +297,7 @@ impl Filter for Vec<JSONSearchConfigurationRecords> {
246297
})
247298
}
248299
}
300+
249301
pub(crate) fn filter_engine_configuration_impl(
250302
user_environment: SearchUserEnvironment,
251303
configuration: &impl Filter,
@@ -256,7 +308,7 @@ pub(crate) fn filter_engine_configuration_impl(
256308
user_environment.region = user_environment.region.to_lowercase();
257309
user_environment.version = user_environment.version.to_lowercase();
258310

259-
let filtered_result = configuration.filter_records(&user_environment, overrides);
311+
let filtered_result = configuration.filter_records(&mut user_environment, overrides);
260312

261313
filtered_result.map(|result| {
262314
let (default_engine_id, default_private_engine_id) = determine_default_engines(
@@ -1327,4 +1379,61 @@ mod tests {
13271379
"Should have returned the specific default for private mode when using a wildcard match"
13281380
);
13291381
}
1382+
1383+
#[test]
1384+
fn test_locale_matched_exactly() {
1385+
let mut user_env = SearchUserEnvironment {
1386+
locale: "en-CA".into(),
1387+
..Default::default()
1388+
};
1389+
negotiate_languages(&mut user_env, &["en-CA".to_string(), "fr".to_string()]);
1390+
assert_eq!(
1391+
user_env.locale, "en-CA",
1392+
"Should return user locale unchanged if in available locales"
1393+
);
1394+
}
1395+
1396+
#[test]
1397+
fn test_locale_fallback_to_base_locale() {
1398+
let mut user_env = SearchUserEnvironment {
1399+
locale: "de-AT".into(),
1400+
..Default::default()
1401+
};
1402+
negotiate_languages(&mut user_env, &["de".to_string()]);
1403+
assert_eq!(
1404+
user_env.locale, "de",
1405+
"Should fallback to base locale if base is in available locales"
1406+
);
1407+
}
1408+
1409+
static ENGLISH_LOCALES: &[&str] = &["en-AU", "en-IE", "en-RU", "en-ZA"];
1410+
1411+
#[test]
1412+
fn test_english_locales_fallbacks_to_en_us() {
1413+
for user_locale in ENGLISH_LOCALES {
1414+
let mut user_env = SearchUserEnvironment {
1415+
locale: user_locale.to_string(),
1416+
..Default::default()
1417+
};
1418+
negotiate_languages(&mut user_env, &["en-US".to_string()]);
1419+
assert_eq!(
1420+
user_env.locale, "en-us",
1421+
"Should remap {} to en-us when en-us is available",
1422+
user_locale
1423+
);
1424+
}
1425+
}
1426+
1427+
#[test]
1428+
fn test_locale_unmatched() {
1429+
let mut user_env = SearchUserEnvironment {
1430+
locale: "fr-CA".into(),
1431+
..Default::default()
1432+
};
1433+
negotiate_languages(&mut user_env, &["de".to_string(), "en-US".to_string()]);
1434+
assert_eq!(
1435+
user_env.locale, "fr-CA",
1436+
"Should leave locale unchanged if no match or english locale fallback is not found"
1437+
);
1438+
}
13301439
}

0 commit comments

Comments
 (0)