@@ -92,7 +92,86 @@ def _validate_tools(value: Any, *, context: str) -> dict[str, dict[str, str]]:
9292 return result
9393
9494
95- def _load_catalog_file (path : Path ) -> dict [str , Any ]:
95+ def _warn_best_effort_catalog (locale : str , path : Path , exc : Exception ) -> None :
96+ _LOGGER .warning ("Skipping best-effort locale %s from %s: %s" , locale , path , exc )
97+
98+
99+ def _warn_best_effort_entry (locale : str , entry : str , exc : Exception ) -> None :
100+ _LOGGER .warning (
101+ "Ignoring invalid best-effort locale %s %s; using English fallback: %s" ,
102+ locale ,
103+ entry ,
104+ exc ,
105+ )
106+
107+
108+ def _validate_best_effort_string_map (
109+ value : Any ,
110+ * ,
111+ locale : str ,
112+ context : str ,
113+ entry_kind : str ,
114+ ) -> dict [str , str ]:
115+ """Drop invalid best-effort strings while preserving valid siblings."""
116+ if value is None :
117+ return {}
118+ if not isinstance (value , dict ):
119+ raise ValueError (f"{ context } must be an object" )
120+
121+ result : dict [str , str ] = {}
122+ for key , text in value .items ():
123+ try :
124+ result .update (_validate_string_map ({key : text }, context = context ))
125+ except ValueError as exc :
126+ _warn_best_effort_entry (locale , f"{ entry_kind } { key !r} " , exc )
127+ return result
128+
129+
130+ def _validate_best_effort_tools (
131+ value : Any , * , locale : str , context : str
132+ ) -> dict [str , dict [str , str ]]:
133+ """Drop invalid best-effort tool fields while preserving valid siblings."""
134+ if value is None :
135+ return {}
136+ if not isinstance (value , dict ):
137+ raise ValueError (f"{ context } must be an object" )
138+
139+ result : dict [str , dict [str , str ]] = {}
140+ for tool_name , tool_values in value .items ():
141+ if not isinstance (tool_name , str ) or not isinstance (tool_values , dict ):
142+ exc = ValueError (f"{ context } entries must be objects keyed by tool name" )
143+ _warn_best_effort_entry (locale , f"tool { tool_name !r} " , exc )
144+ continue
145+
146+ translated : dict [str , str ] = {}
147+ for field , field_value in tool_values .items ():
148+ if field not in ("title" , "description" ):
149+ exc = ValueError (
150+ f"{ context } .{ tool_name } has unsupported field: { field !r} "
151+ )
152+ _warn_best_effort_entry (
153+ locale , f"tool { tool_name !r} field { field !r} " , exc
154+ )
155+ continue
156+ try :
157+ validated = _validate_tools (
158+ {tool_name : {field : field_value }}, context = context
159+ )
160+ except ValueError as exc :
161+ _warn_best_effort_entry (
162+ locale , f"tool { tool_name !r} field { field !r} " , exc
163+ )
164+ continue
165+ if field in validated [tool_name ]:
166+ translated [field ] = validated [tool_name ][field ]
167+ if translated :
168+ result [tool_name ] = translated
169+ return result
170+
171+
172+ def _load_catalog_file (
173+ path : Path , * , best_effort_locale : str | None = None
174+ ) -> dict [str , Any ]:
96175 """Load and validate one catalog without coupling it to its siblings."""
97176 try :
98177 raw = json .loads (path .read_text (encoding = "utf-8" ))
@@ -118,29 +197,39 @@ def _load_catalog_file(path: Path) -> dict[str, Any]:
118197 f"{ sorted (unknown_sections )} "
119198 )
120199
121- return {
122- "meta" : {"native_name" : native_name , "dir" : direction },
123- "messages" : _validate_string_map (
200+ if best_effort_locale is None :
201+ messages = _validate_string_map (
124202 raw .get ("messages" ), context = f"{ path .name } .messages"
125- ),
126- " tool_groups" : _validate_string_map (
203+ )
204+ tool_groups = _validate_string_map (
127205 raw .get ("tool_groups" ), context = f"{ path .name } .tool_groups"
128- ),
129- "tools" : _validate_tools (raw .get ("tools" ), context = f"{ path .name } .tools" ),
130- }
131-
132-
133- def _warn_best_effort_catalog (locale : str , path : Path , exc : Exception ) -> None :
134- _LOGGER .warning ("Skipping best-effort locale %s from %s: %s" , locale , path , exc )
135-
206+ )
207+ tools = _validate_tools (raw .get ("tools" ), context = f"{ path .name } .tools" )
208+ else :
209+ messages = _validate_best_effort_string_map (
210+ raw .get ("messages" ),
211+ locale = best_effort_locale ,
212+ context = f"{ path .name } .messages" ,
213+ entry_kind = "message" ,
214+ )
215+ tool_groups = _validate_best_effort_string_map (
216+ raw .get ("tool_groups" ),
217+ locale = best_effort_locale ,
218+ context = f"{ path .name } .tool_groups" ,
219+ entry_kind = "tool group" ,
220+ )
221+ tools = _validate_best_effort_tools (
222+ raw .get ("tools" ),
223+ locale = best_effort_locale ,
224+ context = f"{ path .name } .tools" ,
225+ )
136226
137- def _warn_best_effort_entry (locale : str , entry : str , exc : Exception ) -> None :
138- _LOGGER .warning (
139- "Ignoring invalid best-effort locale %s %s; using English fallback: %s" ,
140- locale ,
141- entry ,
142- exc ,
143- )
227+ return {
228+ "meta" : {"native_name" : native_name , "dir" : direction },
229+ "messages" : messages ,
230+ "tool_groups" : tool_groups ,
231+ "tools" : tools ,
232+ }
144233
145234
146235def _catalog_fragment (
@@ -236,7 +325,10 @@ def load_catalogs(
236325 for path in paths :
237326 locale = path .stem .lower ().replace ("_" , "-" )
238327 try :
239- catalogs [locale ] = _load_catalog_file (path )
328+ catalogs [locale ] = _load_catalog_file (
329+ path ,
330+ best_effort_locale = (locale if is_best_effort_locale (locale ) else None ),
331+ )
240332 except (ImportError , ValueError ) as exc :
241333 if not is_best_effort_locale (locale ):
242334 raise
0 commit comments