This is how the bug comes out:
It happens because internal keyword patronymic is a part of another, longer, external (i.e. visible to a user) keyword ‘Notpatronymic’ and vice versa. The same bug exists for Rawsurnames for the same reason: surname is an internal keyword too.
The bug appears to be fixed by mangling internal keywords patronymic, notpatronymic, surname and rawsurnames so they are not parts of each other anymore.
Keywords are duplicated (twice!) in gramps/gen/display/name.py , so they need to be changed there too.
Pull request has been submitted to the current maintenance branch. Detailed description of the bug and its solution follows.
Below is the fragment of the code processing string entered by the user into Format field, copied from gramps/gui/configure.py:
Fragment A
# build a pattern from translated pattern:
pattern = new_text
if len(new_text) > 2 and new_text[0] == '"' and new_text[-1] == '"':
passtag
else:
for key in get_translations():
if key in pattern:
pattern = pattern.replace(
key, get_keyword_from_translation(key)
)
# now build up a proper translation:
translation = pattern
if len(new_text) > 2 and new_text[0] == '"' and new_text[-1] == '"':
pass
else:
for key in get_keywords():
if key in translation:
translation = translation.replace(
key, get_translation_from_keyword(key)
)
Above code uses 4 functions from gramps/gen/utils/keyword.py, namely get_translations(), get_keyword_from_translation(key) , get_keywords() and get_translation_from_keyword(key)
Fragment B
KEYWORDS = [
("title", "t", _("Title", "Person"), _("TITLE", "Person")),
("given", "f", _("Given"), _("GIVEN")),
("surn2me", "l", _("Surname"), _("SURNAME")),
("call", "c", _("Call", "Name"), _("CALL", "Name")),
("common", "x", _("Common", "Name"), _("COMMON", "Name")),
("initials", "i", _("Initials"), _("INITIALS")),
("suffix", "s", _("Suffix"), _("SUFFIX")),
("primary", "m", _("Primary", "Name"), _("PRIMARY")),
("primary[pre]", "0m", _("Primary[pre]"), _("PRIMARY[PRE]")),
("primary[sur]", "1m", _("Primary[sur]"), _("PRIMARY[SUR]")),
("primary[con]", "2m", _("Primary[con]"), _("PRIMARY[CON]")),
("patr2nymic", "y", _("Patronymic"), _("PATRONYMIC")),
("patronymic[pre]", "0y", _("Patronymic[pre]"), _("PATRONYMIC[PRE]")),
("patronymic[sur]", "1y", _("Patronymic[sur]"), _("PATRONYMIC[SUR]")),
("patronymic[con]", "2y", _("Patronymic[con]"), _("PATRONYMIC[CON]")),
("rawsurn0mes", "q", _("Rawsurnames"), _("RAWSURNAMES")),
("notpatr0nymic", "o", _("Notpatronymic"), _("NOTPATRONYMIC")),
("prefix", "p", _("Prefix"), _("PREFIX")),
("nickname", "n", _("Nickname"), _("NICKNAME")),
("familynick", "g", _("Familynick"), _("FAMILYNICK")),
]
KEY_TO_TRANS = {}
TRANS_TO_KEY = {}
for key, code, standard, upper in KEYWORDS:
KEY_TO_TRANS[key] = standard
KEY_TO_TRANS[key.upper()] = upper
KEY_TO_TRANS["%" + ("%s" % code)] = standard
KEY_TO_TRANS["%" + ("%s" % code.upper())] = upper
TRANS_TO_KEY[standard.lower()] = key
TRANS_TO_KEY[standard] = key
TRANS_TO_KEY[upper] = key.upper()
def get_translation_from_keyword(keyword):
"""Return the translation of keyword"""
return KEY_TO_TRANS.get(keyword, keyword)
def get_keyword_from_translation(word):
"""Return the keyword of translation"""
return TRANS_TO_KEY.get(word, word)
def get_keywords():
"""Get all keywords, longest to shortest"""
keys = list(KEY_TO_TRANS.keys())
keys.sort(key=lambda a: len(a), reverse=True)
return keys
def get_translations():
"""Get all translations, longest to shortest"""
trans = list(TRANS_TO_KEY.keys())
trans.sort(key=lambda a: len(a), reverse=True)
return trans
The first loop in Fragment A uses get_translation() to replace all external keywords entered by a user with their internal equivalents.
The second loop in Fragment A uses get_keywords() to replace all internal keywords by their external equivalents.
As it can be seen from Fragment B, both get_translations() and get_keywords() starts from longer keywords and proceed towards shorter ones.
Suppose that a user entered “Notpatronymic” into new_text variable. Then first loop transform it first into “notpatronymic” internal keyword (as soon as it recognizes “Notpatronymic”) and then it will see external keyword “patronymic” and will replace it with *internal" keyword for external “patronymic”. (Currently they are the same, but if internal keyword for “patronymic” were “pAtRoNyMiC”, the stringu would transform into the wrong string “notpAtRoNyMiC”).
In a similar fashion, the second loop first translates “notpatronymic” into “Notpatronymic” (since Notpatronymic is longer than Patronymic) and then into the wrong “NotPatronymic”.
Mangling of internal keywords patronymic, notpatronymic, surname and rawsurnames into patr2nymic, notpatr0nymic, surn2me, rawsurn0mes respectively solves the issue.
Please let me know if you see any issues with the proposed solution.



