@@ -11,7 +11,11 @@ use std::collections::HashSet;
1111use std:: hash:: Hash ;
1212use std:: io:: Write ;
1313use 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
1620fn 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
311290static 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 ) ]
315298struct Reading {
316299 suggest : bool ,
@@ -1071,7 +1054,7 @@ impl IdSet {
10711054}
10721055
10731056struct 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 {
10941077impl < ' 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