Skip to content

Fix that hotkey file required mixed-language definitions#284

Open
lionkmp wants to merge 1 commit into
realh:masterfrom
lionkmp:fix-hotkeyfile
Open

Fix that hotkey file required mixed-language definitions#284
lionkmp wants to merge 1 commit into
realh:masterfrom
lionkmp:fix-hotkeyfile

Conversation

@lionkmp
Copy link
Copy Markdown
Contributor

@lionkmp lionkmp commented May 16, 2026

The main menus were required in English, but submenus were needed in localized language of the menu label. This made it very difficult to figure out how to make hotkeys work. This patch makes that main menus are also localized, thus the GUI can be used to visually inspect and write in. The patch was developed with help by Claude Code - fully reviewed and tested, except that I cannot get the "Profiles" third level do anything, despite the menu item displays the assigned hotkey. I'll separately submit a Default Hungarian hotkey file.

The main menus were required in English, but submenus
were needed in localized language of the menu label. This
made it very difficult to figure out how to make hotkeys work.
This patch makes that main menus are also localized, thus
the GUI can be used to visually inspect and write in.
The patch was developed with help by Claude Code - fully reviewed
and tested, except that I cannot get the "Profiles" third level do
anything, despite the menu item displays the assigned hotkey.
I'll separately submit a Default Hungarian hotkey file.
@lionkmp
Copy link
Copy Markdown
Contributor Author

lionkmp commented May 16, 2026

Here is the Claude Code session if necessary, including alternative approaches for later.

❯ I tracked down a bug in this RoxTerm program in this directory, it's an open source terminal. (Now I just try if
shift enter starts a new line here.)

● Let me check the memory index and get some context on what you've found.

Recalled 1 memory (ctrl+o to expand)

● Bash(git -C /home/lion/tmp/roxterm log --oneline -10 && git -C /home/lion/tmp/roxterm diff master..HEAD --stat)
⎿  8e48a8e Updated README.md wrt manpage translations
b6a5bdb Change man pages' references to common-licenses/GPL-2
564a6ba Add mangpages Hungarian translation (#283)
… +7 lines (ctrl+o to expand)

● The branch fix-hotkeyfile is ahead of master by 0 commits — looks like you haven't committed the fix yet. What's the
bug you tracked down? Feel free to share the details or any files you've already modified.

✻ Sautéed for 8s

❯ How can I write mutliple lines here? Shift enter seems to submit the prompt.

● In Claude Code, you can write multiple lines using Option+Enter (on Mac) or Alt+Enter (on Linux/Windows) to insert a
newline without submitting.

Alternatively, you can paste multi-line text directly into the prompt.

✻ Worked for 2s

❯ I put a file here called "my-test-hotkeys.txt", this is a copy of the hotkey file RoxTerm currently loads, a standard
GKeyFile. I noticed that hotkey definitions require that menu name is in English but the menu item name is in the
language the program runs. Test file contains my attempts "File/Új lap", "File/New tab[hu]", "Fűjl/Új lap" only
"File/Új lap" works to really define the hotkey, which can probabyl be confirmed by this call chain, with hard coded
"File" item in it. Do not change anything in code, rather, please come up with suggestions what would be a good
behavior for such a "shortcuts" key file.

     roxterm.c:2357:                                                             
      shortcuts = shortcuts_open(scheme_name, TRUE);                                                                    
      multi_win_set_shortcut_scheme(win, shortcuts);                                                                    
      
  multitab.c:2485:                                                                                                      
  multi_win_set_shortcut_scheme()                                                                                       
  menutree_apply_shortcuts(win->menu_bar, shortcuts);                                                                   

  menutree.c:236:                                                                                                       
  menutree_apply_shortcuts()                                                                                            
  menutree_set_accel_path_for_submenu(tree, MENUTREE_FILE, "File");                                                     

● Let me read the relevant code sections before making suggestions.

Searched for 5 patterns, read 3 files (ctrl+o to expand)

● Now I have a clear understanding of the mechanism. Let me trace exactly what's happening:

How it works:

  • menutree_set_accel_path_for_submenu(..., "File") sets the accel path prefix //File on the
    File menu widget.
  • GTK automatically appends each child item's translated label to that prefix. So in a Hungarian locale the item "New
    _Tab" → _("New _Tab") = "Új _lap" → accel path becomes //File/Új lap.
  • The shortcuts loader reads the file key File/Új lap, builds //File/Új lap, and that matches
    → shortcut fires.

The inconsistency:

  • Section names are hardcoded English ("File", "Edit", …) in menutree_apply_shortcuts — locale-independent.
  • Item names come from the translated widget label — locale-dependent.
  • Result: a shortcuts file is a hybrid: English sections + locale-specific item names, and it breaks silently if you
    move the file to another locale.

Design options for the file format:

Option 1 — All English (recommended for portability)
Document (and enforce) that both section and item names must always be English — i.e. the msgid, not the translation.
This would require registering each menu item under two accel paths: the current translated one (so GTK bookkeeping
still works) and an English canonical one (so the file can match). Files become locale-independent and shareable.

Option 2 — All localized (consistent, simple rule)
Make section names also come from the translated section label (pass _("File") etc. instead of the hardcoded "File"
string into menutree_set_accel_path_for_submenu). The rule then becomes: "write paths exactly as shown in the running
menu." Simple to explain, but files are locale-specific and non-portable.

Option 3 — Symbolic internal IDs (most robust, breaking change)
Replace label-based keys with stable internal IDs, e.g.:
[roxterm shortcuts scheme]
file/new-tab=t
tabs/next-tab=Page_Down
These never change with locale or label renames. Requires a migration from the current format.

Option 4 — Accept both English and localized (best UX, hardest)
When loading, try matching the key as-is; if it doesn't register to any existing accel path, also try looking it up
via a reverse-translation table (msgid → msgstr). Files written in either language would work. The hard part is
building and maintaining that reverse lookup at load time.


Pragmatic recommendation: Option 2 is the smallest, cleanest fix for consistency. Option 1 is better for multi-locale
deployments. Given the current codebase style (small C program, GKeyFile-based), Option 2 is the easiest one-line
change and makes the documented rule simple: "the path is exactly what you read in the menu."

✻ Crunched for 2m 27s

❯ This feature currently has no GUI (it just opens this file in text editor), so a user with a translated user
interface won't have any (easy) way to figure out the keyfile menu names. That makes me tend towards the localized
menu file, makes it at least possible to maintain, but that makes it impossible to deliver a keyfile by default
(which is already broken anyway, these default hotkeys do not work already). But if the feature had a GUI, then
English names would work best, do you agree? So for now I'd go with localized main menu names, and keep the option
for later to make a GUI and THEN use English config file?

● Yes, that reasoning is sound. The two-phase approach makes sense:

  • Now (no GUI): localized names are the only discoverable option — the user reads the menu and types what they see.
    Fixing section names to use _("File") etc. makes the rule uniform and the file self-consistent.
  • Later (with GUI): the GUI can abstract away the name format entirely. It presents a tree of menu items, user picks
    one, the GUI writes the internal English ID. Users never see the file format at all, so switching to English keys is
    painless on the writing side.

One thing worth noting now so it doesn't bite later: if you ever make that switch, existing hand-edited localized
files will break silently (wrong key names, shortcuts just stop working). When you add the GUI, you'll want a
migration step — either a one-time converter, or a load-time fallback that tries localized form if the English lookup
fails. Keeping that in mind during the GUI design will save pain.

So the short-term plan is: fix the section names to use the translated strings (pass _("File") instead of "File" in
menutree_apply_shortcuts), document that the file format follows the displayed locale, and mark the "switch to English
IDs with GUI" as a future TODO.

✻ Crunched for 15s

❯ Excellent, thanks. Please implement the fix with localized names.

● Let me read the full menutree.c to find the top-level menu item labels and the menutree_labels array.

Searched for 1 pattern, read 1 file (ctrl+o to expand)

● Let me check the Tabs menu labels and the menutree header for the full list of IDs.

Searched for 1 pattern, read 1 file (ctrl+o to expand)

● Let me read the exact macro definition to confirm Tabs and Search are in TOP_LEVEL_MENU_ITEMS.

Searched for 1 pattern, read 1 file (ctrl+o to expand)

● Good — menutree_set_accel_path_for_item is only used inside menutree_apply_shortcuts. I now have everything I need.
Here are the changes I'll make to menutree.c:

  1. Add menutree_label_for_id helper after strip_underscore
  2. Remove the menu_branch param from menutree_set_accel_path_for_submenu, derive from labels
  3. Replace menutree_set_accel_path_for_item (no longer needed) with two new _ids helpers
  4. Rewrite menutree_apply_shortcuts to use localized names everywhere, also removing the duplicate MENUTREE_HELP call

● Update(src/menutree.c)
⎿  Added 5 lines
52 return out;
53 }
54
55 +static char menutree_label_for_id(MenuTreeID id)
56 +{
57 + return strip_underscore(menutree_labels[id]);
58 +}
59 +
60 /
Builds a menu; va_list is a series of pairs of labels and IDs, terminated
61 * by a NULL label; use "_" for a separator, and MENUTREE_NULL_ID for any item
62 * you don't want stored in item_widgets */

● Update(src/menutree.c)
⎿  Added 38 lines, removed 8 lines
180 }
181
182 static void
183 -menutree_set_accel_path_for_submenu(MenuTree *mtree, MenuTreeID id,
184 - const char *menu_branch)
183 +menutree_set_accel_path_for_submenu(MenuTree *mtree, MenuTreeID id)
184 {
185 GtkMenu *menu = menutree_submenu_from_id(mtree, id);
186 + char *menu_name;
187 char *accel_path;
188
189 if (!menu)
190 return;
191 - accel_path = get_accel_path(mtree->shortcuts, menu_branch);
191 + menu_name = menutree_label_for_id(id);
192 + accel_path = get_accel_path(mtree->shortcuts, menu_name);
193 + g_free(menu_name);
194 gtk_menu_set_accel_path(menu, accel_path);
195 g_free(accel_path);
196 gtk_menu_set_accel_group(menu, mtree->accel_group);
197 }
198
199 static void
198 -menutree_set_accel_path_for_item(MenuTree * tree, MenuTreeID id,
199 - const char *path_leaf)
200 +menutree_set_accel_path_for_item_ids(MenuTree *tree,
201 + MenuTreeID section_id, MenuTreeID item_id)
202 {
201 - GtkWidget *item = tree->item_widgets[id];
202 - char *full_path;
203 + GtkWidget *item = tree->item_widgets[item_id];
204 + char *section, *leaf, *path, *full_path;
205
206 if (!item)
207 return;
206 - full_path = get_accel_path(tree->shortcuts, path_leaf);
208 + section = menutree_label_for_id(section_id);
209 + leaf = menutree_label_for_id(item_id);
210 + path = g_strjoin("/", section, leaf, NULL);
211 + g_free(section);
212 + g_free(leaf);
213 + full_path = get_accel_path(tree->shortcuts, path);
214 + g_free(path);
215 gtk_menu_item_set_accel_path(GTK_MENU_ITEM(item), full_path);
216 g_free(full_path);
217 }
218
219 static void
220 +menutree_set_accel_path_for_item_ids3(MenuTree *tree,
221 + MenuTreeID section_id, MenuTreeID mid_id, MenuTreeID item_id)
222 +{
223 + GtkWidget *item = tree->item_widgets[item_id];
224 + char *section, *mid, *leaf, *path, *full_path;
225 +
226 + if (!item)
227 + return;
228 + section = menutree_label_for_id(section_id);
229 + mid = menutree_label_for_id(mid_id);
230 + leaf = menutree_label_for_id(item_id);
231 + path = g_strjoin("/", section, mid, leaf, NULL);
232 + g_free(section);
233 + g_free(mid);
234 + g_free(leaf);
235 + full_path = get_accel_path(tree->shortcuts, path);
236 + g_free(path);
237 + gtk_menu_item_set_accel_path(GTK_MENU_ITEM(item), full_path);
238 + g_free(full_path);
239 +}
240 +
241 +static void
242 menutree_set_accel_path_for_tab(MenuTree * tree, int tab)
243 {
244 char *leaf = NULL;

● Now replace menutree_apply_shortcuts:

● Update(src/menutree.c)
⎿  Added 43 lines, removed 35 lines
271 void menutree_apply_shortcuts(MenuTree *tree, Options *shortcuts)
272 {
273 GtkMenu *submenu;
274 - char *accel_path;
274 + char *file, *item, *path, *accel_path;
275
276 tree->shortcuts = shortcuts;
277 shortcuts_enable_signal_handler(FALSE);
278 - menutree_set_accel_path_for_submenu(tree, MENUTREE_FILE, "File");
279 - menutree_set_accel_path_for_submenu(tree, MENUTREE_EDIT, "Edit");
280 - menutree_set_accel_path_for_submenu(tree, MENUTREE_VIEW, "View");
281 - menutree_set_accel_path_for_submenu(tree, MENUTREE_SEARCH, "Search");
282 - menutree_set_accel_path_for_submenu(tree, MENUTREE_PREFERENCES,
283 - "Preferences");
284 - menutree_set_accel_path_for_submenu(tree, MENUTREE_HELP, "Help");
278 + menutree_set_accel_path_for_submenu(tree, MENUTREE_FILE);
279 + menutree_set_accel_path_for_submenu(tree, MENUTREE_EDIT);
280 + menutree_set_accel_path_for_submenu(tree, MENUTREE_VIEW);
281 + menutree_set_accel_path_for_submenu(tree, MENUTREE_SEARCH);
282 + menutree_set_accel_path_for_submenu(tree, MENUTREE_PREFERENCES);
283 + menutree_set_accel_path_for_submenu(tree, MENUTREE_HELP);
284
285 submenu = GTK_MENU(tree->new_win_profiles_menu);
286 if (submenu)
287 {
289 - accel_path = get_accel_path(tree->shortcuts,
290 - "File/New Window With Profile");
288 + file = menutree_label_for_id(MENUTREE_FILE);
289 + item = menutree_label_for_id(MENUTREE_FILE_NEW_WINDOW_WITH_PROFILE);
290 + path = g_strjoin("/", file, item, NULL);
291 + g_free(file);
292 + g_free(item);
293 + accel_path = get_accel_path(tree->shortcuts, path);
294 + g_free(path);
295 gtk_menu_set_accel_path(submenu, accel_path);
296 g_free(accel_path);
297 gtk_menu_set_accel_group(submenu, tree->accel_group);
...
299 submenu = GTK_MENU(tree->new_tab_profiles_menu);
300 if (submenu)
301 {
298 - accel_path = get_accel_path(tree->shortcuts,
299 - "File/New Tab With Profile");
302 + file = menutree_label_for_id(MENUTREE_FILE);
303 + item = menutree_label_for_id(MENUTREE_FILE_NEW_TAB_WITH_PROFILE);
304 + path = g_strjoin("/", file, item, NULL);
305 + g_free(file);
306 + g_free(item);
307 + accel_path = get_accel_path(tree->shortcuts, path);
308 + g_free(path);
309 gtk_menu_set_accel_path(submenu, accel_path);
310 g_free(accel_path);
311 gtk_menu_set_accel_group(submenu, tree->accel_group);
...
316 submenu = menutree_submenu_from_id(tree, MENUTREE_TABS);
317 if (submenu)
318 gtk_menu_set_accel_group(submenu, tree->accel_group);
310 - menutree_set_accel_path_for_item(tree,
311 - MENUTREE_FILE_NEW_WINDOW_WITH_PROFILE_HEADER,
312 - "File/New Window With Profile/Profiles");
313 - menutree_set_accel_path_for_item(tree,
314 - MENUTREE_FILE_NEW_TAB_WITH_PROFILE_HEADER,
315 - "File/New Tab With Profile/Profiles");
316 - menutree_set_accel_path_for_item(tree, MENUTREE_TABS_DETACH_TAB,
317 - "Tabs/Detach Tab");
318 - menutree_set_accel_path_for_item(tree, MENUTREE_TABS_CLOSE_TAB,
319 - "Tabs/Close Tab");
320 - menutree_set_accel_path_for_item(tree, MENUTREE_TABS_CLOSE_OTHER_TABS,
321 - "Tabs/Close Other Tabs");
322 - menutree_set_accel_path_for_item(tree, MENUTREE_TABS_NAME_TAB,
323 - "Tabs/Name Tab...");
324 - menutree_set_accel_path_for_item(tree, MENUTREE_TABS_NEXT_TAB,
325 - "Tabs/Next Tab");
326 - menutree_set_accel_path_for_item(tree, MENUTREE_TABS_PREVIOUS_TAB,
327 - "Tabs/Previous Tab");
328 - menutree_set_accel_path_for_item(tree, MENUTREE_TABS_MOVE_TAB_LEFT,
329 - "Tabs/Move Tab Left");
330 - menutree_set_accel_path_for_item(tree, MENUTREE_TABS_MOVE_TAB_RIGHT,
331 - "Tabs/Move Tab Right");
332 - menutree_set_accel_path_for_submenu(tree, MENUTREE_HELP, "Help");
319 + menutree_set_accel_path_for_item_ids3(tree,
320 + MENUTREE_FILE, MENUTREE_FILE_NEW_WINDOW_WITH_PROFILE,
321 + MENUTREE_FILE_NEW_WINDOW_WITH_PROFILE_HEADER);
322 + menutree_set_accel_path_for_item_ids3(tree,
323 + MENUTREE_FILE, MENUTREE_FILE_NEW_TAB_WITH_PROFILE,
324 + MENUTREE_FILE_NEW_TAB_WITH_PROFILE_HEADER);
325 + menutree_set_accel_path_for_item_ids(tree,
326 + MENUTREE_TABS, MENUTREE_TABS_DETACH_TAB);
327 + menutree_set_accel_path_for_item_ids(tree,
328 + MENUTREE_TABS, MENUTREE_TABS_CLOSE_TAB);
329 + menutree_set_accel_path_for_item_ids(tree,
330 + MENUTREE_TABS, MENUTREE_TABS_CLOSE_OTHER_TABS);
331 + menutree_set_accel_path_for_item_ids(tree,
332 + MENUTREE_TABS, MENUTREE_TABS_NAME_TAB);
333 + menutree_set_accel_path_for_item_ids(tree,
334 + MENUTREE_TABS, MENUTREE_TABS_NEXT_TAB);
335 + menutree_set_accel_path_for_item_ids(tree,
336 + MENUTREE_TABS, MENUTREE_TABS_PREVIOUS_TAB);
337 + menutree_set_accel_path_for_item_ids(tree,
338 + MENUTREE_TABS, MENUTREE_TABS_MOVE_TAB_LEFT);
339 + menutree_set_accel_path_for_item_ids(tree,
340 + MENUTREE_TABS, MENUTREE_TABS_MOVE_TAB_RIGHT);
341 menutree_apply_tab_shortcuts(tree);
342 shortcuts_enable_signal_handler(TRUE);
343 }

● Now let's verify it compiles:

● Bash(cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug 2>&1 | tail -5 && cmake --build build --target roxterm 2>&1 | grep
-E "error:|warning:|menutree" | head -30)
⎿  -- Languages for main translation: ca;de;es;fr;fr_BE;hu;pt;pt_BR;ru;uk
-- Languages for xhtml translation:
-- Configuring done (0.1s)
… +3 lines (ctrl+o to expand)

● Bash(cmake --build build --target roxterm 2>&1 | tail -5)
⎿  [ 42%] Built target rtlib
[100%] Built target roxterm

● Clean build, no warnings. The changes:

  • menutree_label_for_id(id) — new helper, strips mnemonic underscores from menutree_labels[id]
  • menutree_set_accel_path_for_submenu — no longer takes a hardcoded English menu_branch string; derives the name from
    the translated label instead
  • menutree_set_accel_path_for_item_ids / _ids3 — new helpers for building 2- and 3-level localized paths for Tabs
    items and profile headers
  • menutree_apply_shortcuts — all paths now come from translated labels; also removed the stray duplicate MENUTREE_HELP
    call that was at the end of the function

In an English locale the behaviour is identical to before. In a Hungarian (or any other translated) locale, users can
now write the section name as it appears in the menu — Fájl/Új lap, Lapok/Következő lap, etc.

✻ Churned for 7m 17s

❯ /rename roxterm-hotkeys
⎿ Session renamed to: roxterm-hotkeys

@realh
Copy link
Copy Markdown
Owner

realh commented May 17, 2026

I don't agree with using option 2 (use the local language in the keyfile). The main problem with it is that the supplied Default file would only work in English. Instead I think we should use option 1 (the keyfile uses untranslated English). The strings can be translated after reading them from the keyfile to match the translated label used by GTK.

To make the UX easier, roxterm could regenerate the file just before opening the text editor. It could add all the available menu items to the file, commenting out the ones that aren't bound. When the locale isn't English it could add an additional comment above each one, showing the translated version.

@lionkmp
Copy link
Copy Markdown
Contributor Author

lionkmp commented May 18, 2026

I absolutely agree with your explanation. Thank you for fast feedback!

I was also worried about this approach, but this was the smallest change, only 20-25 lines. Getting the original English name of the menu might take a bit more. Please do not close this PR, I'll see if Claude wants to build on this branch or start over from "master". (I think some of these changes are already a good start.) (I think adding more commits to this branch will expand the PR, we will see.)

Your ideas are quite good, they fully solve all problems. I'll get back soon with a better solution.

@realh
Copy link
Copy Markdown
Owner

realh commented May 18, 2026

It does make sense for you to continue working with Claude Code, thanks for offering. TBH it's so long since I worked on this that I can't really remember how all this works. You're right, looking up the untranslated names is harder than I realised. I don't know why GTK is translating the accel paths for menu items but not the top-level menus. It shouldn't be translating either, because the accel path strings aren't flagged with () or N(), but perhaps there are matching strings tagged for translation elsewhere. But it doesn't make sense that GTK translates accel paths at all.

There is probably a better way to do this, based on GAction, or even with builder XML, but that might need more major refactoring. The reason I didn't do it in a more standard way like that in the first place is probably because it originally started as a GTK 2 app and I took the path of least resistance when upgrading it to GTK 3.

Maybe, instead of applying the English strings as accel paths, we could hash them, or use a look-up table, to make sure they are all unique strings that aren't translated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants