Skip to content

Commit 037a64c

Browse files
committed
Fix grammar-error localisation: $N substitutions and locale fallback
- cohort_errs now sets numeric Fluent args ("1" = error form; "2", "3", ... from relations named $2, $3 matched by MSG_TEMPLATE_REL, multiple targets joined with ", "), fixing the arg1/$1 name mismatch and enabling $2+. - €1, €2, ... are all replaced with the corresponding suggestions (was €1 only). - FluentLoader gains get_message_localized: message-level fallback across the requested locales, then default, then any loaded bundle, instead of resolving a single bundle and returning the raw id when it lacks the message. - Suggester carries the full locale priority list rather than a single resolved locale; error_preferences uses the encoded key and the same fallback. - FluentLoader::new keeps partially-parsed resources instead of dropping a whole file on parse error, and disables bidi isolating on interpolated values.
1 parent ffef9c8 commit 037a64c

2 files changed

Lines changed: 169 additions & 150 deletions

File tree

src/modules/divvun/suggest.rs

Lines changed: 73 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ use std::collections::HashSet;
1111
use std::hash::Hash;
1212
use std::io::Write;
1313
use std::ops::Deref;
14-
use std::{collections::HashMap, fs, sync::Arc};
14+
use std::{
15+
collections::{BTreeMap, HashMap},
16+
fs,
17+
sync::Arc,
18+
};
1519

1620
fn encode_unicode_identifier(s: &str) -> String {
1721
let mut result = String::new();
@@ -206,34 +210,16 @@ impl Suggest {
206210
let mut prefs = IndexMap::new();
207211

208212
for key in self.error_mappings.keys() {
209-
let mut best_msg: Option<String> = None;
210-
for lang in language_tags {
211-
match self.fluent_loader.get_message(Some(&lang), key, None) {
212-
Ok((title, _)) => {
213-
best_msg = Some(title);
214-
break;
215-
}
216-
Err(_) => {
217-
continue;
218-
}
219-
}
220-
}
221-
222-
if best_msg.is_none() {
223-
// Try default language
224-
match self.fluent_loader.get_message(None, key, None) {
225-
Ok((title, _)) => {
226-
best_msg = Some(title);
227-
}
228-
Err(_) => {}
229-
}
230-
}
231-
232-
if let Some(msg) = best_msg {
233-
prefs.insert(key.clone(), msg);
234-
} else {
235-
prefs.insert(key.clone(), key.clone());
236-
}
213+
// FTL keys are encoded the same way as in `cohort_errs`. Message
214+
// lookup falls back across `language_tags`, then the default locale,
215+
// then any loaded bundle, before finally using the raw key.
216+
let ftl_key = encode_unicode_identifier(key);
217+
let title = self
218+
.fluent_loader
219+
.get_message_localized(language_tags, &ftl_key, None)
220+
.map(|(title, _)| title)
221+
.unwrap_or_else(|| key.clone());
222+
prefs.insert(key.clone(), title);
237223
}
238224

239225
prefs
@@ -252,16 +238,9 @@ impl CommandRunner for Suggest {
252238
// Parse typed config
253239
let config: SuggestConfig = serde_json::from_value((*config).clone()).unwrap_or_default();
254240

255-
// Check config for locales array
256-
let locale = if let Some(locales) = config.locales.as_ref() {
257-
// Find the first available locale from the prioritized list
258-
self.fluent_loader
259-
.find_first_available_locale(locales)
260-
.unwrap_or_else(|| "en".to_string())
261-
} else {
262-
// No locales provided, use default
263-
"en".to_string()
264-
};
241+
// Requested locales in priority order; message lookup falls back across
242+
// these, then the default locale, then any loaded bundle.
243+
let locales = config.locales.clone().unwrap_or_default();
265244

266245
let fluent_loader = self.fluent_loader.clone();
267246
let generator = self.generator.clone();
@@ -283,7 +262,7 @@ impl CommandRunner for Suggest {
283262

284263
let suggester = Suggester::new(
285264
generator,
286-
locale,
265+
locales,
287266
false,
288267
&fluent_loader,
289268
error_mappings,
@@ -311,6 +290,10 @@ static DELETE_REL: Lazy<Regex> = Lazy::new(|| Regex::new(r#"^DELETE[0-9]*$"#).un
311290
static LEFT_RIGHT_DELETE_REL: Lazy<Regex> =
312291
Lazy::new(|| Regex::new(r#"^(LEFT|RIGHT|DELETE[0-9]*)$"#).unwrap());
313292

293+
// Relation names that fill numbered message-template placeholders ($2, $3, ...).
294+
// $1 is always the error cohort's own form.
295+
static MSG_TEMPLATE_REL: Lazy<Regex> = Lazy::new(|| Regex::new(r#"^\$[0-9]+$"#).unwrap());
296+
314297
#[derive(Debug, Default, Clone)]
315298
struct Reading {
316299
suggest: bool,
@@ -1071,7 +1054,7 @@ impl IdSet {
10711054
}
10721055

10731056
struct Suggester<'a> {
1074-
pub locale: String,
1057+
pub locales: Vec<String>, // requested locales in priority order
10751058
pub fluent_loader: &'a FluentLoader,
10761059

10771060
generator: Arc<hfst::Transducer>,
@@ -1094,15 +1077,15 @@ pub struct GrammarOutput {
10941077
impl<'a> Suggester<'a> {
10951078
pub fn new(
10961079
generator: Arc<hfst::Transducer>,
1097-
locale: String,
1080+
locales: Vec<String>,
10981081
generate_all_readings: bool,
10991082
fluent_loader: &'a FluentLoader,
11001083
error_mappings: Arc<IndexMap<String, Vec<Id>>>,
11011084
ignores: Option<IdSet>,
11021085
includes: Option<IdSet>,
11031086
) -> Self {
11041087
Suggester {
1105-
locale: locale.clone(),
1088+
locales,
11061089
generator,
11071090
error_mappings,
11081091
delimiters: default_delimiters(),
@@ -1176,30 +1159,53 @@ impl<'a> Suggester<'a> {
11761159
return None;
11771160
}
11781161

1179-
// Use FluentLoader for message resolution
1162+
// Build message-template args:
1163+
// {$1} -> the error cohort's own form
1164+
// {$2}+ -> wordform(s) of cohorts related via a relation named "$N"
1165+
// (matched by MSG_TEMPLATE_REL), multiple targets joined with
1166+
// ", ", across the readings carrying this error tag.
11801167
let mut args = FluentArgs::new();
1181-
args.set("arg1", c.form.as_str());
1168+
args.set("1", c.form.as_str());
1169+
1170+
let mut template_args: BTreeMap<String, Vec<String>> = BTreeMap::new();
1171+
for r in &c.readings {
1172+
if !r.errtypes.contains(cg3_tag) {
1173+
continue;
1174+
}
1175+
for (rel_name, target_id) in &r.rels {
1176+
// $1 is reserved for the error form, even if a "$1" relation exists.
1177+
if rel_name == "$1" || !MSG_TEMPLATE_REL.is_match(rel_name) {
1178+
continue;
1179+
}
1180+
let Some(&i_t) = sentence.ids_cohorts.get(target_id) else {
1181+
continue;
1182+
};
1183+
let Some(target) = sentence.cohorts.get(i_t) else {
1184+
continue;
1185+
};
1186+
// Relation "$2" fills Fluent variable "2".
1187+
let arg_key = rel_name.trim_start_matches('$').to_string();
1188+
template_args
1189+
.entry(arg_key)
1190+
.or_default()
1191+
.push(target.form.clone());
1192+
}
1193+
}
1194+
for (key, forms) in &template_args {
1195+
args.set(key.clone(), forms.join(", "));
1196+
}
11821197

11831198
// Mangle the error ID to match FTL keys
11841199
let ftl_key = encode_unicode_identifier(err_id);
11851200

1186-
let mut msg =
1187-
match self
1188-
.fluent_loader
1189-
.get_message(Some(&self.locale), &ftl_key, Some(&args))
1190-
{
1191-
Ok((title, desc)) => (title, desc),
1192-
Err(_) => {
1193-
// Fallback to default locale if message not found
1194-
match self.fluent_loader.get_message(None, &ftl_key, Some(&args)) {
1195-
Ok((title, desc)) => (title, desc),
1196-
Err(_) => {
1197-
tracing::debug!("WARNING: No Fluent message for \"{}\"", ftl_key);
1198-
(err_id.to_string(), err_id.to_string())
1199-
}
1200-
}
1201-
}
1202-
};
1201+
let locale_refs: Vec<&str> = self.locales.iter().map(String::as_str).collect();
1202+
let mut msg = self
1203+
.fluent_loader
1204+
.get_message_localized(&locale_refs, &ftl_key, Some(&args))
1205+
.unwrap_or_else(|| {
1206+
tracing::debug!("WARNING: No Fluent message for \"{}\"", ftl_key);
1207+
(err_id.to_string(), err_id.to_string())
1208+
});
12031209
// End set msg
12041210
// Begin set beg, end, form, rep:
12051211
let mut start = c.pos;
@@ -1226,9 +1232,11 @@ impl<'a> Suggester<'a> {
12261232
suggestions.retain(|r| r != form);
12271233
// No duplicates:
12281234
suggestions.dedup();
1229-
if !suggestions.is_empty() {
1230-
msg.0 = msg.0.replace("€1", &suggestions[0]);
1231-
msg.1 = msg.1.replace("€1", &suggestions[0]);
1235+
// Suggestion placeholders: €1, €2, ... -> 1st, 2nd, ... suggestion.
1236+
for (i, suggestion) in suggestions.iter().enumerate() {
1237+
let placeholder = format!("€{}", i + 1);
1238+
msg.0 = msg.0.replace(&placeholder, suggestion);
1239+
msg.1 = msg.1.replace(&placeholder, suggestion);
12321240
}
12331241
Some(GrammarErr {
12341242
form: form.to_string(),

0 commit comments

Comments
 (0)