Replies: 10 comments 2 replies
-
|
Will the locale selector have both the English Exonym (since English is the lingua franca of Gramps) and the locale endonym ? While building a gramplet for a (Rosetta Stone style) 3-language triptych of Gramps Glossary terms, I realized that at least 1 of the languages was likely to be foreign to the user. (Finding the equivalent term in a foreign language was the main reason for the gramplet.) So the selection menu and titles needed to show the English exonym and endonym for each locale. And since so many features choose locale by the ISO 639 code that Gramps uses, it seemed reasonable to show that too and to use the ISO code as a reliable sorting index.
The experimental Environment Inspector could be used for sanity checks for the Locale and environmental variable values too. |
Beta Was this translation helpful? Give feedback.
-
Follow-up: Locale Settings Are the Wrong Grain for a Genealogy AppStatusFollow-up to Where this came fromWhile implementing the language-preference restart mechanism, a colleague pointed out:
Chasing that down surfaced a real bug in the initial implementation (fixed): the That fix is sufficient for the current feature. But working through it raised a The three-bucket reframing
1. Per-record — already solved correctlyCalendar type (Gregorian, Julian, Hebrew, French Republican, Islamic, ...) is a 2. Per-tree — an existing pattern, underusedName display format already works this way. Collation ( Date input/display format ( Moving both to the per-tree metadata pattern would sidestep the restart problem for 3. Per-app-session — genuinely restart-appropriateUI message language ( A fourth, cheaper category (not urgent, just noted)GTK text direction (RTL for Arabic/Hebrew, Recommendation (for when we pick this up)
|
Beta Was this translation helpful? Give feedback.
-
Sure, why not? |
Beta Was this translation helpful? Give feedback.
-
|
Any thoughts on adding a command line override for language settings at the same time? This would ensure users and developers can set the language at runtime without modifying their preferences.
|
Beta Was this translation helpful? Give feedback.
-
Yes, that is a good idea. |
Beta Was this translation helpful? Give feedback.
-
|
@emyoulation, how is this?
|
Beta Was this translation helpful? Give feedback.
-
Beta Was this translation helpful? Give feedback.
-
|
Whatever icon is used in Grampsm we'll want to carry it through to the Wordpress entry page on the website, the MediaWiki language selection (and translator pages), and the Discourse International pages. (Right now, our WordPress home page is not inviting to non-English speakers. It does not even suggest a path to discovering whether Gramps supports other languages.)
|
Beta Was this translation helpful? Give feedback.
-
|
@Nick-Hall said:
Checked it — you are right, and I can show exactly where. gui/spell.py:97 — Spell.init picks the Gspell dictionary via: locale_code = glocale.locale_code()
...
spell_language = Gspell.language_lookup(locale_code[:5])And glocale.locale_code() (gen/utils/grampslocale.py:658-662) is just return self.lang — the value derived from LANG/LC_MESSAGES/setlocale(), not self.language (the list derived from LANGUAGE, used for gettext message translation). These are two genuinely separate attributes on GrampsLocale, fed by two separate env vars, and Gspell's dictionary selection in Gramps only ever looks at the LANG-derived one. What this means for the restart feature: since the relaunch only sets LANGUAGE and deliberately leaves LANG untouched, the restart now correctly changes UI message language but the spell-check dictionary stays on whatever the OS's real LANG is — completely decoupled from the preferences.language the user just picked. If they set Gramps to French, notes still get spell-checked against whatever dictionary their system LANG implies (probably English). That's very likely not what a user expects — picking a UI language reasonably implies "I'm about to type in this language," which spell-check should follow. The fix doesn't reopen an earlier issue that I encountered: don't touch LANG again (that would risk setlocale() falling back to "C" and taking LC_TIME/LC_COLLATE down with it). Instead, fix it at the one call site that actually cares — gui/spell.py — so it prefers glocale.language (the LANGUAGE-derived list, already in the right short-code shape Gspell.language_lookup expects, e.g. "fr") over glocale.locale_code(), falling back to the latter only if no language override is set. That's a small, self-contained change to one file, unrelated to the os.execv/restore-state machinery. Fixed! |
Beta Was this translation helpful? Give feedback.
-
|
(1) Will this work for Mac and Windows (does Windows really have an execv)? (2) As well as the normal COLLATE language, there are also special collation variant options, for example in German there is the Phonebook variant, and also I think there is one for one of the scandinavian languages (might be Finnish I think), as well as some far eastern languages. Will this also work in these cases, and at least not prevent the variants being chosen. (I have a special interest in this, having implemented the ICU collation in Narrative Web). Otherwise, a very interesting enhancement, just so long as the advantage is not overweighed by what seems like a very complicated change. |
Beta Was this translation helpful? Give feedback.









Uh oh!
There was an error while loading. Please reload this page.
-
Motivation
Gramps currently determines its UI language purely from
LANG/LANGUAGEenvironment variables, resolved once at process-import time
(
gramps/gen/const.py:252, viaGrampsLocale). There is no config/preference forit, and no
--langCLI flag either — the only documented way to change language isLANGUAGE=fr_FR grampsfrom a shell (see usage example atcli/argparser.py:140).We want to make language a real Preferences-dialog setting. Doing so surfaced a
broader, pre-existing problem worth solving at the same time: Gramps already has 4
settings in the Preferences dialog marked
* Requires Restart(
preferences.date-format,preferences.age-display-precision,preferences.age-after-death,interface.sidebar-text), but today that label ispurely cosmetic — changing them just pops a warning telling the user to restart
manually. A language preference would be a natural 5th member of this family, and a
naive "just restart" flow has its own failure mode:
behavior.autoloaddefaults toFalse(gen/config.py:153), so a plain restart would land most users on thewelcome screen instead of the tree/view they were just working in — arguably more
jarring than the current do-nothing warning.
So the scope grew from "add a language preference" to "add a language preference,
and build a real restart-and-restore mechanism that all 5 restart-required settings
can share."
Considerations and rejected alternatives
Live in-process reload (no restart at all), Blender-style. Rejected. Blender can
hot-swap its UI language because it's an immediate-mode UI that re-translates every
string on every redraw. Gramps's GTK3 widget tree is retained-mode: once a
Gtk.Label.set_text()orGtk.Builder-parsed.gladeXML string has been resolved,the widget holds a plain
str, not a reference back to gettext. Making this workwould require two independent, both-necessary pieces of new infrastructure:
_()proxy (à la Django'sgettext_lazy()) so cached translated strings —e.g.
gen/plug/_pluginreg.py:68-132's module-level dicts, orgen/datehandler/__init__.py:84's locale binding, all resolved once at importtime today — re-resolve against the current locale on each read instead of being
frozen forever. This alone doesn't touch already-built GTK widgets.
tree (every open window, all ~50 gramplets, sidebar plugins,
GLib.timeout_addcallbacks, signal handlers) — none of which was ever written with "may be
destroyed and rebuilt mid-session" in mind. Getting this wrong produces
leaked GObject refs and stale-object crashes that are much harder to diagnose
than a clean process restart.
Both pieces are real, substantial engineering efforts with a long tail of
third-party-plugin risk. Not pursued further here.
os.execvself-relaunch. Chosen instead. Gets a guaranteed clean slate from theOS/interpreter (no leaked refs, no half-updated caches) at the cost of having to
explicitly re-establish only the state that's deliberately passed forward across the
restart boundary — a much smaller, auditable surface than "correctly tear down the
entire live object graph." There's no existing
os.execv/relaunch precedentanywhere in the Gramps codebase, but there is a directly analogous precedent for
"read a flag from raw
sys.argvbefore any Gramps module is imported":grampsapp.py:41-46already does this for-S/--safemode, settingos.environ["SAFEMODE"]beforegen/const.py(and thereforeGrampsLocale) isever imported. The language part of this plan is a direct copy of that pattern.
How to carry session state across the restart. A JSON file (path passed via a
new
--restore-state <path>CLI flag) rather than inline CLI text, and rather thanrouting it through the existing
ConfigManager/ini system (gen/config.py) — thestate is a one-shot process-handoff blob, not a durable user preference, so it
doesn't belong in the persistent config file.
os.execvbuildsargvdirectly withno shell involved, so there's no quoting/injection risk either way; a file is chosen
mainly because it scales better than argv length if the captured state grows, and is
easy to inspect while debugging.
What's actually worth restoring. Framed against what Gramps already persists
continuously and durably, independent of this feature:
gui/viewmanager.py:884,:888-890, restored at:304-305) and full gramplet/sidebar layout (~/.gramps/gramplets.ini,gui/widgets/grampletpane.py) are already saved to disk as you use the app —no need to duplicate these in the restore-state JSON.
open, which view/category was active, which record was selected, which
Person/Family/etc. editors were open.
low value relative to the new machinery it would require, and dropped from scope.
incrementally — saves only happen on OK click
(
gui/editors/editperson.py:914-986, inside aDbTxn). An open editor'sin-memory edits are the entire form, not a few keystrokes, and no restart
mechanism can recover typed-but-unsaved field values. The plan handles this by
reusing Gramps's existing per-editor unsaved-changes prompt
(
gui/editors/editprimary.py:244-274,SaveDialog) — forcing a save/discarddecision before the restart happens, exactly as already happens when a user
closes one editor manually. Never silently lost, never silently carried forward.
Scope decisions confirmed with the team:
changes one of the 5 flagged settings and closes the dialog.
language preference), not just language — same mechanism, same prompt.
Implementation plan
1. New
preferences.languageconfig keygramps/gen/config.py, in thepreferences.*block nearpreferences.date-format(around line 299):
Empty string = use system default (no env override) — consistent with
gen/utils/grampslocale.py:244-258, which only overridesLANG/LC_MESSAGESwhenLANGUAGEis actually present in the environment.2. Preferences UI: language combo + restart-tracking on all 5 settings
All in
gramps/gui/configure.py.Language widget — add to
add_general_panel(configure.py:2140-2397, the"General"tab, which already hostssidebar-text), near the top of the"Environment Settings" section (
:2149-2160). Mirror the existingdate-formatcombo pattern (
:1711-1721,Gtk.ComboBoxText), populated fromglocale.get_language_dict()(gen/utils/grampslocale.py:750-759, returns{display_name: code}) — the same API already used for report-language pickers ingen/plug/report/stdoptions.py:64,80. Build a sorted list of(display_name, code),track codes in parallel to map
get_active()back to a code, set active index fromconfig.get("preferences.language"). Label:_("Language *"), tooltip matching theexisting
sidebar-textwording (configure.py:2230-2234,"Requires Gramps restart to apply.").Restart-change tracking —
ConfigureDialog(configure.py:173-243) issingle-response (
Gtk.ResponseType.CLOSE, wired at:218toself.done), allsettings apply immediately (tooltip confirms:
"Any changes are saved immediately",:219-220).done(self, obj, value)(:238-243) is the one choke point all 5settings' changes eventually flow through — this is where the restart offer belongs.
GrampsPreferences.__init__(configure.py:701area): addself._restart_settings = set()before the baseConfigureDialog.__init__call.GrampsPreferences.mark_restart_required(self, key, *_args):self._restart_settings.add(key). Compatible withadd_checkbox'sextra_callbacksignature (callback(widget)), called via a small lambda persite.
date-formatcombo (:1718,date_format_changed,:2008-2021): addself._restart_settings.add("preferences.date-format"), and remove theexisting passive
OkDialogpopup at:2014-2021— the new dialog-level promptsupersedes it.
age-display-precisioncombo (:1735-1740, currently a bare lambda): convertto a bound method that both
config.set(...)s and marks the key.age-after-deathcheckbox (:1758-1765) andsidebar-textcheckbox(
:2223-2235): both useadd_checkbox— addextra_callback=lambda o: self.mark_restart_required("interface.sidebar-text")(and the
age-after-deathequivalent).languagecombo: sameextra_callbackpattern.done()inGrampsPreferences:done()afterward regardless of the user's restart choice,so
on_close=update_constants(existing "apply immediately" behavior,:701)still runs — declining a restart should not block the normal apply-and-close flow.
3.
offer_restart()— orchestrationNew method on
GrampsPreferences, plus a new modulegramps/gui/restartstate.pyfor the reusable, non-GTK pieces (state capture/serialize, so it's independently
testable).
QuestionDialog2(gui/dialog.py:139-177) confirmed: takes(msg1, msg2, label_msg1, label_msg2, parent=None),.run()shows the dialog synchronously andreturns
True/False(response == Gtk.ResponseType.ACCEPT) — exactly what'sneeded here, no subclassing required.
Unsaved-editor resolution,
_resolve_unsaved_editors:uistate.gwm.id2item(gui/managedwindow.py:121) is the existing windowregistry,
window_id → ManagedWindow. Snapshot withlist(...)first sinceclose()mutates this dict during iteration (managedwindow.py:215).EditPrimary.data_has_changed()/.close()(gui/editors/editprimary.py:244-274)are reused as-is —
close()already checksconfig.get("interface.dont-ask")and, if there are changes, pops the existing
SaveDialog(gui/dialog.py:62-96,blocking
.run()) offering save-or-discard. By the time this loop finishes, everyeditor is either closed-and-committed, closed-and-discarded, or (if
interface.dont-askis set) force-closed per existing app-wide behavior — never anew prompt style, just reusing what already exists for individual-editor close.
State capture,
gramps/gui/restartstate.py:language:config.get("preferences.language")(already set by this point).tree:dbstate.db.get_save_path()ifdbstate.is_open()elseNone— samedirectory-path shape
-O/arg_h.openalready accepts(
cli/arghandler.py:354-359).active_view: add a small newViewManager.get_active_view_ids()helper next togoto_page/get_category(gui/viewmanager.py:1084-1115) that mirrors theexisting reverse-lookup
view_changed()already does (:1184-1198) to return(category_id, view_id)strings — the same id vocabulary already stored inpreferences.last-view/last-views.selected:{nav_type: history.present() for (nav_type, nav_group), history in uistate.history_lookup.items() if nav_group == 0}(gui/displaystate.py:573-577).open_editors: iterateuistate.gwm.id2item.values(), filter toEditPrimaryinstances with a real (non-
None)obj.get_handle()— skip brand-new/unsavedobjects (
window_id == id(self)pereditprimary.py:131-135, nothing meaningfulto restore). Record
{"object_type": obj.__class__.__name__, "handle": handle}.write_state_file(state):json.dumpto atempfile.NamedTemporaryFile(delete=False, suffix=".json"), return its path. Use stdlibjson, notorjson— this is a tiny,one-shot handoff blob, not a fit for
gen/lib/json_utils.py's object-serializationuse case, and not a fit for
ConfigManager/ini (gen/config.py) either, sincethat's for durable persistent prefs, not a one-shot process-handoff.
restart_gramps(path):Use
sys.argv(not a hardcodedGramps.py/grampspath) so this works whetherlaunched via
python Gramps.py, an installedgrampsscript, orpython -m gramps—
sys.executable+sys.argvreproduces however this process was actuallyinvoked.
4. Consuming
--restore-state: three separate points, three separate times(a) Language — before
gen/const.pyis even importedgramps/grampsapp.py, immediately after the existing-S/--saferaw-sys.argvscan (
:41-46) and beforefrom .gen.const import APP_GRAMPS, ...(:53) — thisis the exact precedent to copy, since
gen/const.py:252constructs theGRAMPS_LOCALEsingleton at import time, before normalgetoptparsing(
cli/argparser.py) ever runs:Confirmed
gen/utils/grampslocale.py:244-258readsLANGUAGE(overridesLANG/LC_MESSAGES) duringGrampsLocale.__init__.(b) Tree — flows through the existing
-Omachinery, zero new tree-opening codegen/const.py:310LONGOPTS: add"restore-state=". No short opt needed(
SHORTOPTS,:356, untouched).cli/argparser.py,__init__defaults (nearself.open = None): addself.restore_state_path = None.cli/argparser.py, options loop (:287-289, alongside the existing-O/--openhandling), add:self.openhere reusesArgHandler.__init__(cli/arghandler.py:188,self.open = self.__handle_open_option(parser.open, parser.create)) unchanged,and
gui/grampsgui.py:605'sif arg_h.open or arg_h.imp_db_path:branchnaturally wins over the
paths.recent-file/behavior.autoloadfallback(
:608-619) — identical priority to a manually-passed-O, no new branchingrequired.
--restore-state=PATHto the two help-text blocks (argparser.pyaround:74and:161) for consistency.(c) Active view / selection / open editors — after the tree is fully loaded
gui/grampsgui.py,Gramps.__init__(:568-625), afterarg_h.handle_args_gui()(
:604) has returned and the tree-opening branch has resolved (confirmedDbLoader.read_file, invoked viaCLIManager._read_recent_file,cli/grampscli.py:274, blocks synchronously until the tree is loaded):apply_post_init_state(path, dbstate, viewmanager):which run at different times/processes-of-the-startup-sequence).
active_view:cat_num = viewmanager.get_category(state["active_view"]["category"])(
viewmanager.py:1108-1115); findview_numby scanningviewmanager.views[cat_num]for matchingpdata.id; callviewmanager.goto_page(cat_num, view_num)(viewmanager.py:1084) — the exactcall
views_to_show()-driven startup already makes (viewmanager.py:1996-2023).selected: for each(nav_type, handle), calldbstate.db.method("has_%s_handle", nav_type)(handle)to verify it still exists(tree may have changed since the snapshot was taken — e.g. edited from another
Gramps instance), then
viewmanager.uistate.set_active(handle, nav_type)(
displaystate.py:596-603) — pushes intoHistory; for the active view thisimmediately triggers
goto_handlevia the"active-changed"signal(
navigationview.py:184); for other nav_types it resolves next time that viewbecomes active, matching existing behavior.
open_editors: for each{object_type, handle}, verify existence via the samehas_<type>_handlecheck, then call the existing generic editor-openinghelper —
EditObject(dbstate, uistate, [], object_type, prop="handle", value=handle)(gui/editors/__init__.py:86-123). This is a direct reusediscovery:
EditObjectalready does the handle→object lookup(
dbstate.db.method("get_%s_from_%s", obj_class, prop)(value)) and editorconstruction generically for all primary types, and already logs-and-skips
missing objects instead of raising — no bespoke per-type dispatch needed.
HandleError/missing, per existingprecedent
gui/displaystate.py:721-748) — never block startup on stale state.Verification
Manual end-to-end
that person's editor and edit a field (don't save).
Data tab → Date format, and General tab → sidebar-text) — confirm the
SaveDialog appears for the dirty editor, and confirm both the Save and Discard
paths work.
closes Preferences normally (setting still applied, just not restarted).
os.getpid()before/after,or
ps), and that the new process: reopens the same tree without hitting thewelcome/Tree Manager screen, lands on the same category/view, has the same
person selected/highlighted, reopens that person's editor automatically, and
shows the new UI language in menu strings.
should produce a single restart prompt, not one per setting —
_restart_settingsis aset()accumulated across the whole dialog session.cleanly on the welcome screen, no crash); a selected/open-editor handle deleted
from another Gramps instance between capture and restore (should silently
skip, not crash); a brand-new unsaved Person editor open at restart time (must
not attempt to reopen — no valid handle to restore by).
Automated tests
gramps/cli/test/argparser_test.py(existing pattern at:39-80): add casesfor
--restore-state <path>settingap.openfrom the JSON's"tree"key,setting
ap.restore_state_pathregardless of contents, tolerating amissing/invalid file without raising, and leaving
ap.openuntouched when"tree"is absent.gramps/gen/utils/test/grampslocale_test.py: add a case constructingGrampsLocalewithLANGUAGEpre-set inos.environand asserting it's pickedup — validates the underlying mechanism consumption point (a) relies on,
independent of the
grampsapp.pymodule-level scan itself (which is a startupside effect, not practically unit-testable in isolation).
gramps/gui/test/restartstate_test.py: pure-function tests forcapture_state/write_state_fileschema round-tripping with mockeddbstate/uistate/viewmanagerobjects (no live GTK needed for the JSONshape).
Run via the project's standard single-module test invocation:
export GDK_BACKEND=- GRAMPS_RESOURCES=build/share LANGUAGE= LANG=en_US.utf-8 python3 -m unittest gramps.cli.test.argparser_test python3 -m unittest gramps.gen.utils.test.grampslocale_test python3 -m unittest gramps.gui.test.restartstate_testDo not run the full
unittest discoversuite locally — let CI handle it.Critical files
gramps/gen/config.py— newpreferences.languagekeygramps/gui/configure.py— language widget, restart-tracking,offer_restart(),_resolve_unsaved_editors()gramps/gui/restartstate.py— new file:capture_state,write_state_file,restart_gramps,apply_post_init_stategramps/gen/const.py—LONGOPTSadditiongramps/cli/argparser.py—--restore-stateparsing, help textgramps/grampsapp.py— pre-import language env-var scangramps/gui/grampsgui.py— post-init call toapply_post_init_stategramps/gui/viewmanager.py— newget_active_view_ids()helpergui/editors/__init__.py(EditObject),gui/editors/editprimary.py(data_has_changed/close),gui/managedwindow.py(
gwm.id2item),gui/displaystate.py(set_active,history_lookup),gui/dialog.py(QuestionDialog2,SaveDialog)Beta Was this translation helpful? Give feedback.
All reactions