|
14 | 14 |
|
15 | 15 | logger = get_logger() |
16 | 16 |
|
| 17 | +# Clean native language names for screen reader pronunciation. |
| 18 | +# Maps the 2-letter lang prefix to a short, clear native name. |
| 19 | +_NATIVE_LANG_NAMES = { |
| 20 | + "be": "беларуская", |
| 21 | + "bg": "български", |
| 22 | + "cs": "čeština", |
| 23 | + "da": "dansk", |
| 24 | + "de": "Deutsch", |
| 25 | + "el": "ελληνικά", |
| 26 | + "en": "English", |
| 27 | + "es": "español", |
| 28 | + "et": "eesti", |
| 29 | + "fi": "suomi", |
| 30 | + "fr": "français", |
| 31 | + "he": "עברית", |
| 32 | + "hr": "hrvatski", |
| 33 | + "hu": "magyar", |
| 34 | + "is": "Íslenska", |
| 35 | + "it": "italiano", |
| 36 | + "ja": "日本語", |
| 37 | + "ko": "한국어", |
| 38 | + "nb": "norsk bokmål", |
| 39 | + "nl": "Nederlands", |
| 40 | + "nn": "norsk nynorsk", |
| 41 | + "pl": "polski", |
| 42 | + "pt": "Português", |
| 43 | + "ro": "română", |
| 44 | + "ru": "русский", |
| 45 | + "sk": "slovenčina", |
| 46 | + "sl": "slovenščina", |
| 47 | + "sv": "Svenska", |
| 48 | + "tr": "Türkçe", |
| 49 | + "uk": "українська", |
| 50 | + "zh": "中文", |
| 51 | +} |
| 52 | + |
17 | 53 |
|
18 | 54 | def normalize_string(s: str) -> str: |
19 | 55 | """Normalizes a string by converting to lowercase and removing diacritics.""" |
@@ -122,8 +158,8 @@ def _load_languages(self): |
122 | 158 | raw_data = json.load(f) |
123 | 159 |
|
124 | 160 | language_data = [LanguageListItem(**item) for item in raw_data] |
125 | | - favorites = ["pt_BR", "en_US", "es_ES"] |
126 | | - language_data.sort(key=lambda x: (x.code not in favorites, x.name)) |
| 161 | + favorites_order = {"en_US": 0, "pt_BR": 1, "es_ES": 2} |
| 162 | + language_data.sort(key=lambda x: (x.code not in favorites_order, favorites_order.get(x.code, 999), x.name)) |
127 | 163 | self._store.splice(0, 0, language_data) |
128 | 164 | GLib.idle_add(self._post_load_setup) |
129 | 165 |
|
@@ -200,70 +236,80 @@ def _on_factory_setup(self, factory, list_item): |
200 | 236 | halign=Gtk.Align.START, |
201 | 237 | valign=Gtk.Align.CENTER, |
202 | 238 | ) |
203 | | - root_box.append(content_box) |
204 | | - list_item.set_child(root_box) |
205 | | - |
206 | | - try: |
207 | | - cursor = Gdk.Cursor.new_from_name("pointer", None) |
208 | | - root_box.set_cursor(cursor) |
209 | | - except Exception: |
210 | | - pass |
211 | | - |
212 | | - motion_controller = Gtk.EventControllerMotion.new() |
213 | | - motion_controller.connect("motion", self._on_mouse_motion_item, list_item) |
214 | | - root_box.add_controller(motion_controller) |
215 | | - |
216 | | - def _on_mouse_motion_item(self, controller, x, y, list_item): |
217 | | - position = list_item.get_position() |
218 | | - if position != Gtk.INVALID_LIST_POSITION: |
219 | | - selection_model = self.grid_view.get_model() |
220 | | - if selection_model and selection_model.get_selected() != position: |
221 | | - selection_model.set_selected(position) |
222 | | - |
223 | | - def _on_factory_bind(self, factory, list_item): |
224 | | - item = list_item.get_item() |
225 | | - root_box = list_item.get_child() |
226 | | - content_box = root_box.get_first_child() |
227 | | - |
228 | | - child = content_box.get_first_child() |
229 | | - while child: |
230 | | - content_box.remove(child) |
231 | | - child = content_box.get_first_child() |
232 | 239 |
|
233 | | - # Accessible label for screen readers |
234 | | - root_box.update_property( |
235 | | - [Gtk.AccessibleProperty.LABEL], |
236 | | - [f"{item.name} ({item.name_orig})"], |
| 240 | + flag_widget = Gtk.Image( |
| 241 | + pixel_size=36, |
| 242 | + accessible_role=Gtk.AccessibleRole.PRESENTATION, |
237 | 243 | ) |
238 | 244 |
|
239 | | - flag_widget = Gtk.Image.new_from_icon_name(item.flag_icon_name) |
240 | | - flag_widget.set_pixel_size(36) |
241 | | - |
242 | 245 | vbox = Gtk.Box( |
243 | 246 | orientation=Gtk.Orientation.VERTICAL, |
244 | 247 | spacing=2, |
245 | 248 | halign=Gtk.Align.START, |
246 | 249 | valign=Gtk.Align.CENTER, |
247 | 250 | ) |
| 251 | + # Heading: ORCA reads this (native language name) |
248 | 252 | name_label = Gtk.Label( |
249 | 253 | halign=Gtk.Align.START, |
250 | | - label=item.name, |
251 | 254 | wrap=True, |
252 | 255 | justify=Gtk.Justification.LEFT, |
253 | 256 | ) |
254 | 257 | name_label.add_css_class("heading") |
| 258 | + # Caption: hidden from ORCA (English name for visual reference only) |
255 | 259 | orig_name_label = Gtk.Label( |
256 | 260 | halign=Gtk.Align.START, |
257 | | - label=item.name_orig, |
258 | 261 | wrap=True, |
259 | 262 | justify=Gtk.Justification.LEFT, |
| 263 | + accessible_role=Gtk.AccessibleRole.PRESENTATION, |
260 | 264 | ) |
261 | 265 | orig_name_label.add_css_class("caption") |
262 | 266 | vbox.append(name_label) |
263 | 267 | vbox.append(orig_name_label) |
264 | 268 |
|
265 | 269 | content_box.append(flag_widget) |
266 | 270 | content_box.append(vbox) |
| 271 | + root_box.append(content_box) |
| 272 | + |
| 273 | + # Store references for _on_factory_bind |
| 274 | + root_box._flag = flag_widget |
| 275 | + root_box._name_label = name_label |
| 276 | + root_box._orig_label = orig_name_label |
| 277 | + |
| 278 | + list_item.set_child(root_box) |
| 279 | + |
| 280 | + try: |
| 281 | + cursor = Gdk.Cursor.new_from_name("pointer", None) |
| 282 | + root_box.set_cursor(cursor) |
| 283 | + except Exception: |
| 284 | + pass |
| 285 | + |
| 286 | + motion_controller = Gtk.EventControllerMotion.new() |
| 287 | + motion_controller.connect("motion", self._on_mouse_motion_item, list_item) |
| 288 | + root_box.add_controller(motion_controller) |
| 289 | + |
| 290 | + def _on_mouse_motion_item(self, controller, x, y, list_item): |
| 291 | + position = list_item.get_position() |
| 292 | + if position != Gtk.INVALID_LIST_POSITION: |
| 293 | + selection_model = self.grid_view.get_model() |
| 294 | + if selection_model and selection_model.get_selected() != position: |
| 295 | + selection_model.set_selected(position) |
| 296 | + |
| 297 | + def _on_factory_bind(self, factory, list_item): |
| 298 | + item = list_item.get_item() |
| 299 | + root_box = list_item.get_child() |
| 300 | + |
| 301 | + # Build native name heading: e.g. "Português, Brazil" or "English, United States" |
| 302 | + parts = item.name.split(" - ", 1) |
| 303 | + country = parts[1] if len(parts) > 1 else "" |
| 304 | + native_name = _NATIVE_LANG_NAMES.get(item.code[:2], item.name_orig) |
| 305 | + heading_text = f"{native_name}, {country}" if country else native_name |
| 306 | + |
| 307 | + # Heading: native name (ORCA reads this) |
| 308 | + root_box._name_label.set_label(heading_text) |
| 309 | + # Caption: English name (hidden from ORCA via PRESENTATION role) |
| 310 | + root_box._orig_label.set_label(item.name) |
| 311 | + |
| 312 | + root_box._flag.set_from_icon_name(item.flag_icon_name) |
267 | 313 |
|
268 | 314 | click_gesture = Gtk.GestureClick.new() |
269 | 315 | click_gesture.connect("released", self._on_item_clicked, item) |
|
0 commit comments