What Fuzzy international name matching system should be added to Gramps?

Gramps is bundled with support for the phonetic indexing system: SoundEx. This fuzzy name matching system was adopted Census Bureau and the Social Security Administration in the 1930s. But is is not well-suited to non-English phonetics. And the implementation is the original 4-character “Russell” (NARA), not the the 6-digit Daitch–Mokotoff Soundex, a specialized refinement for Slavic and Yiddish surnames. (The 2 variants are compared here.)

If we wanted a more globally applicable phonetic ‘fuzzy’ name matching system, what should it be?

Here are a couple systems:

  • Metaphone and Double Metaphone (Metaphone 3 is the professional version.) But are still western culture oriented.
  • Double Metaphone
  • Beider-Morse (BMPM) was developed specifically for Genealogical Use.
  • phonex for French culture oriented
  • Cologne Phonetic (Kölner Phonetik) — the German-language equivalent of Soundex,
  • Daitch-Mokotoff Soundex — for on Slavic/Yiddish surnames

Any others to consider?

Maybe minor adjustments for French?

From a superficial look, Phonex appears to be a French dialect of the 4-character “Russell” (NARA) Soundex. It does not seem to build on the 6-digit Daitch–Mokotoff Soundex base. So you get one or the other?

By adjustments, I only thought on the replacement sections on code (oriented sounds into french locale), not really on a complete soundex alternative.

    # 1 remplacer les y par des i
    r = r.replace('Y','I')
    
    # voir 7    
    r = r.replace(u'É','Y')
    r = r.replace(u'È','Y')
    r = r.replace(u'Ê','Y')
                
    r = unicodedata.normalize('NFKD', 
                    unicode(strval.upper().strip())).encode('ASCII', 'ignore')
  
    # 2 supprimer les h qui ne sont pas précédées de c ou de s ou de p
    r = re.sub(r'([^P|C|S])H', r'\1', r)

    # 3 remplacement du ph par f
    r = r.replace(r'PH', r'F')
  
    # 4 remplacer les groupes de lettres suivantes :
    r = re.sub(r'G(AI?[N|M])',r'K\1', r)
  
    # 5 remplacer les occurrences suivantes, si elles sont suivies par 
    # une lettre a, e, i, o, ou u :
    r = re.sub(r'[A|E]I[N|M]([A|E|I|O|U])',r'YN\1', r)
    
    # 6 remplacement de groupes de 3 lettres (sons 'o', 'oua', 'ein') :
    r = r.replace('EAU','O')
    r = r.replace('OUA','2')
    r = r.replace('EIN','4')
    r = r.replace('AIN','4')
    r = r.replace('EIM','4')
    r = r.replace('AIM','4')
  
    # 7 remplacement du son É:
    # voir plus haut
    r = r.replace('AI','Y')
    r = r.replace('EI','Y')
    r = r.replace('ER','YR')
    r = r.replace('ESS','YS')
    r = r.replace('ET','YT')
    r = r.replace('EZ','YZ')

    # 8 remplacer les groupes de 2 lettres suivantes (son â..anâ.. 
    # et â..inâ..), sauf sâ..il sont suivi par une lettre a, e, i o, 
    # u ou un son 1 Ã 4 :
    r = re.sub(r'AN([^A|E|I|O|U|1|2|3|4])',r'1\1', r)
    r = re.sub(r'ON([^A|E|I|O|U|1|2|3|4])',r'1\1', r)
    r = re.sub(r'AM([^A|E|I|O|U|1|2|3|4])',r'1\1', r)
    r = re.sub(r'EN([^A|E|I|O|U|1|2|3|4])',r'1\1', r)
    r = re.sub(r'EM([^A|E|I|O|U|1|2|3|4])',r'1\1', r)
    r = re.sub(r'IN([^A|E|I|O|U|1|2|3|4])',r'4\1', r)

    # 9 remplacer les s par des z sâ..ils sont suivi et précédés des 
    # lettres a, e, i, o,u ou dâ..un son 1 Ã  4
    r = re.sub(r'([A|E|I|O|U|Y|1|2|3|4])S([A|E|I|O|U|Y|1|2|3|4])',r'\1Z\2',r)

    # 10 remplacer les groupes de 2 lettres suivants :
    r = r.replace('OE','E')
    r = r.replace('EU','E')
    r = r.replace('AU','O')
    r = r.replace('OI','2')
    r = r.replace('OY','2')
    r = r.replace('OU','3')  

    # 11 remplacer les groupes de lettres suivants
    r = r.replace('CH','5')
    r = r.replace('SCH','5')
    r = r.replace('SH','5')
    r = r.replace('SS','S')
    r = r.replace('SC','S')

    # 12 remplacer le c par un s s'il est suivi d'un e ou d'un i
    r = re.sub(r'C([E|I])',r'S\1',r)
  
    # 13 remplacer les lettres ou groupe de lettres suivants :
    r = r.replace('C','K')
    r = r.replace('Q','K')
    r = r.replace('QU','K')
    r = r.replace('GU','K')
    r = r.replace('GA','KA')
    r = r.replace('GO','KO')
    r = r.replace('GY','KY')

    # 14 remplacer les lettres suivante :
    r = r.replace('A','O')
    r = r.replace('D','T')
    r = r.replace('P','T')
    r = r.replace('J','G')
    r = r.replace('B','F')
    r = r.replace('V','F')
    r = r.replace('M','N')
 
    # 15 Supprimer les lettres dupliquées
    oldc='#'
    newr=''
    for c in r:
        if oldc != c:
            newr=newr+c
        oldc=c
    r = newr

    #16 Supprimer les terminaisons suivantes : t, x
    r = re.sub(r'(.*)[T|X]$',r'\1', r)

My old genealogy program offers the following phonetic search options:

Sondex
Extended Soundex
Kölner Phonetik
PHONEM
dMetaphone 1
dMetaphone 2
Daitch-Mokotoff
Phonet 1
Phonet 2

I never tested the differences.

I was originally became motivated to discuss the Soundex feature after having Claude add a Fuzzy Search option to Relationship Filter gramplet, an expansion of @dsblank’s rework of the Flat Filter gramplet by @bugbear.

A bit of searching only surfaced a few features with Soundex options:

And the built-in Soundex gramplet was unusable on large trees. (It tries to add pop-up menu ALL known surnames. A menu with thousands of surnames causes the GUI to freeze.) And didn’t seem useful when it did work.

Reworked it to be a “Fuzzy Matching” gramplet that does not add as much burden but still gives more features. And has groundwork that hopefully will support adding other Phonetic encoding systems.

True, for the pasted code, which looks like a NARA Soundex extended.

By looking at common use and alternative, we can quickly find some basic rules or an alternate return.

I do not know what is this algo. name:

Consonant mappings:
B,P,V,F → B
C,K,Q,G,J → C
S,Z,X → S
D,T → D
L → L
M,N → M
R → R

Special combinations:
PH → F, KN → N
GH → removed
WR → R

Examples:
STEPHEN → SDBM0
  S-T[D]-[e]-PH[F→B]-[e]-N[M]

ASHCRAFT → ASCRF0
  A-S[S]-H-C[C]-R[R]-A-F[B]-T[D]

KNIGHT → NCD0
  KN[N]-I-GH-T[D]

Accuracy benchmarks (Pfeifer et al. 1996, information retrieval studies on English surname recall):
• Soundex: ~85% recall, ~30% precision — too many collisions.
• NYSIIS: ~88% recall, ~45% precision — better consonant handling.
• Phonex: ~90% recall, ~55% precision — preserves more phonetic structure.
• Phonix: ~92% recall, ~60% precision — highest precision among classical algorithms.
• Double Metaphone: ~94% recall, ~70% precision — current gold standard for English.

When to choose Phonex specifically:
• You need a simpler algorithm than Double Metaphone but better than Soundex.
• You’re working on a legacy system that specifies Phonex (e.g., UK NHS patient matching historically used Phonex-family algorithms).
• Your dataset is dominated by English/Celtic/Welsh names (where Phonex was tuned and tested).

When NOT to use Phonex:
• Multilingual name matching (immigrant records, global customer data) → use Double Metaphone or Beider-Morse.
• Jewish/Eastern European surnames → use Daitch-Mokotoff.
• German-only data → use Kölner Phonetik (Cologne Phonetic).

It also matches fine (or better than NARA Soundex), for some “french names and sounds”.

Experimental and Expert

Crashes and data loss should be expected with “proof of concept” code.
Please backup before use and work on a test tree data.

This 0.2.1 testing version of the Fuzzy Match gramplet (for People categories) has Gdk-CRITICAL and Gtk-CRITICAL warnings that still have to be tracked down. Features are in flux. (Particularly, the system for making extensible Encoding systems is being re-imagined.)

It was built to be a testbed for new Phonetic matching systems. (But it turned out to be very useful for navigating People in Gramps.)

Features

• Matching people, not just a code — see every surname and every person that phonetically matches what you type
• Working with a match — click a person to make them active, double-click to edit them
• Staying in sync — the gramplet follows along as you navigate the tree elsewhere
• Creating a filter from a match — turn a code into a reusable People filter
• Finding a surname to search — nearby-relative suggestions, or browse anyone in the tree
• Staying fast on large trees — indexes in the background with visible progress
• Adjusting the layout — resizable results panel that remembers your layout
• Help — one click to this document, or to the online guide (if you have the markdown viewer installed)
• Encoding systems — Soundex today, with room to add more
• What is inherited from the original gramplet — the name field and code display still work the way they always did

Available as an Addon Manager install from the emyoulation “Curated addons” GitHub repo. (You must change the Audience and Status filters to All)

  • https://raw.githubusercontent.com/emyoulation/CuratedGrampsPlugins/main/gramps60

(Change the URL’s last 2 digits as appropriate for 5.2 and 6.1)

 File ".gramps/gramps52/plugins/Fuzzy/FuzzyMatchingGramplet.py", line 148, in <module>
    from phonetic_codes import ALGORITHM_LABELS, ALGORITHMS, DEFAULT_ALGORITHM
  File ".gramps/gramps52/plugins/Fuzzy/phonetic_codes.py", line 71, in <module>
    def _discover_encoder_modules() -> list[ModuleType]:
TypeError: 'type' object is not subscriptable
2026-09-08 21:06:49.433: WARNING: grampletpane.py: line 193: Error loading gramplet 'Fuzzy Matching': skipping content

Maybe not a “real” issue as I should not run Gramps 5.2.x (custom branch) with python 3.6…

$ gramps -v
Gramps Settings:
----------------
 gramps    : 5.2.5
 o.s.      : Linux

Required:
---------
 Python    : 3.6.9
 Gtk++     : 3.22.30
 pygobject : 3.26.1

Thanks for the detailed feedback! I’ll run them through Claude when my session limit resets in 3 hours.

FWIW, my primary test box is running:

$ gramps -v
Gramps Settings:
----------------
 gramps    : 5.2.2
 o.s.      : linux

Required:
---------
 Python    : 3.11.6
 Gtk++     : 3.24.38
 pygobject : 3.42.2

update 0.2.2 posted for compatibility with different Python versions.
(2 additional Encoding systems were bundled too. Leaning towards changing to a Rule plugin structure.)

Thanks. I will give a try.
In fact gramps 5.2.x branch should not run on python 3.6. Tests on versions (gtk, python) have just been disabled. Except one or two features, there is no major limitations. Otherwise on python 3.6, there is a light typing module, but with Optional, Tuple, etc. So, still some workaround for a large [Type support](https://documentation.help/Python-3.6.8/typing.html) cf. associationstool, or 3rd party libs like typing-extensions · PyPI.

I am still a little bit lazzy for planning a major OS upgrade as there is some local customizations and stability on this current old dated OS (desktop version, offline without AI).

For fun, Claude (and myself), an extra (overkilled) patch for python 3.6 support.

-# Deferred annotation evaluation (PEP 563), required for Python 3.8/3.9
-# compatibility: this module's type hints use `list[X]`, `dict[K, V]`,
-# `X | None`, etc. (PEP 585/604 syntax), which those Python versions
-# cannot evaluate at runtime even though they parse it fine - Gramps
-# 5.2's own official minimum is Python 3.8, which predates both PEPs
-# (585 needs 3.9+, 604 needs 3.10+). This import makes every
-# annotation in this file a deferred string, never evaluated at
-# runtime at all, restoring compatibility with the full (5.2.0,
-# 6.2.0) Gramps range this addon's own .gpr.py declares, without
-# giving up the modern annotation syntax itself.
-from __future__ import annotations

+# Type hints compatibility: this module uses `Dict`, `List`, `Optional`
+# from the `typing` module to support Python 3.6, which does not support
+# PEP 585 (built-in generics as types) or PEP 604 (union types with `|`).
+# This maintains compatibility with Jérôme's minimum Python version.

 import contextlib
 import logging
 import os
 import time
+from typing import Dict, List, Optional, Set
Modification Status
:white_check_mark: Removal of from __future__ import annotations Done
:white_check_mark: Addition of from typing import Dict, List, Optional, Set Done
:white_check_mark: dict[str, list[str]] → Dict[str, List[str]] Done (6 occurrences)
:white_check_mark: list[str] → List[str] Done (2 occurrences)
:white_check_mark: set[str] → Set[str] Done (5 occurrences)
:white_check_mark: str | None → Optional[str] Done (1 occurrence)

Oh, AI also provided a script (I did not ask for)!

#!/bin/bash
FUZZY_DIR="$HOME/.gramps/gramps52/plugins/Fuzzy"

# 1. Corriger FuzzyMatchingGramplet.py
sed -i '/^from __future__ import annotations$/d' "$FUZZY_DIR/FuzzyMatchingGramplet.py"
sed -i 's/from __future__ import annotations//' "$FUZZY_DIR/FuzzyMatchingGramplet.py"
sed -i 's/from typing import /from typing import Dict, List, Optional, Set, /' "$FUZZY_DIR/FuzzyMatchingGramplet.py"
sed -i 's/dict\[str, list\[str\]\]/Dict[str, List[str]]/g' "$FUZZY_DIR/FuzzyMatchingGramplet.py"
sed -i 's/list\[str\]/List[str]/g' "$FUZZY_DIR/FuzzyMatchingGramplet.py"
sed -i 's/set\[str\]/Set[str]/g' "$FUZZY_DIR/FuzzyMatchingGramplet.py"
sed -i 's/str | None/Optional[str]/g' "$FUZZY_DIR/FuzzyMatchingGramplet.py"

# 2. Corriger phonetic_codes.py
sed -i '/^from __future__ import annotations$/d' "$FUZZY_DIR/phonetic_codes.py"
sed -i 's/from typing import .*/from types import ModuleType\\nfrom typing import Dict, List, Set, Callable, Iterable, Optional, Tuple, Any/' "$FUZZY_DIR/phonetic_codes.py"
sed -i 's/list\[ModuleType\]/List[ModuleType]/g' "$FUZZY_DIR/phonetic_codes.py"
sed -i 's/dict\[str, Callable\[/Dict[str, Callable[/g' "$FUZZY_DIR/phonetic_codes.py"
sed -i 's/\] , set\[str\]/], Set[str]/g' "$FUZZY_DIR/phonetic_codes.py"
sed -i 's/dict\[str, str\]/Dict[str, str]/g' "$FUZZY_DIR/phonetic_codes.py"
sed -i 's/tuple\[/Tuple[/g' "$FUZZY_DIR/phonetic_codes.py"
sed -i 's/str | None/Optional[str]/g' "$FUZZY_DIR/phonetic_codes.py"

# 3. Corriger TOUS les fichiers dans encoders/
for file in "$FUZZY_DIR"/encoders/*.py; do
    if [ -f "$file" ]; then
        sed -i '/^from __future__ import annotations$/d' "$file"
        sed -i 's/from __future__ import annotations//' "$file"
        sed -i 's/list\[/List[/g' "$file"
        sed -i 's/dict\[/Dict[/g' "$file"
        sed -i 's/set\[/Set[/g' "$file"
        sed -i 's/tuple\[/Tuple[/g' "$file"
        sed -i 's/str | None/Optional[str]/g' "$file"
        # Ajouter l'import typing si nécessaire
        if ! grep -q \"from typing import\" "$file"; then
            sed -i '/^import /a from typing import Dict, List, Optional, Set, Tuple' "$file"
        fi
    fi
done

echo "✅ Tous les fichiers corrigés !"

Also asked for a custom “set of rules” (above one):

#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2025  Custom Soundex Implementation
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#

"""
Custom Soundex encoder with specific consonant mappings.

Consonant mappings:
B,P,V,F -> B
C,K,Q,G,J -> C
S,Z,X -> S
D,T -> D
L -> L
M,N -> M
R -> R

Special combinations:
PH -> F
KN -> N
GH -> removed
WR -> R
"""

# Compatible with Python 3.6
from typing import Set

LOG = None  # Will be set by the importing module

#: Registry key
ALGORITHM_ID = "soundex_custom"

#: Display label
ALGORITHM_LABEL = "Soundex Custom"

# Consonant mapping rules
_CONSONANT_MAP = {
    'B': 'B', 'P': 'B', 'V': 'B', 'F': 'B',
    'C': 'C', 'K': 'C', 'Q': 'C', 'G': 'C', 'J': 'C',
    'S': 'S', 'Z': 'S', 'X': 'S',
    'D': 'D', 'T': 'D',
    'L': 'L',
    'M': 'M', 'N': 'M',
    'R': 'R'
}

# Special combinations (applied first)
_SPECIAL_COMBINATIONS = [
    ('PH', 'F'),
    ('KN', 'N'),
    ('GH', ''),  # removed
    ('WR', 'R')
]

def _clean_and_upper(name: str) -> str:
    """Keep only letters, convert to uppercase."""
    return ''.join(c for c in name if c.isalpha()).upper()

def _apply_special_combinations(name: str) -> str:
    """Apply special multi-character combinations first."""
    for old, new in _SPECIAL_COMBINATIONS:
        name = name.replace(old, new)
    return name

def _map_consonants(name: str) -> str:
    """Map individual consonants according to the rules."""
    return ''.join(_CONSONANT_MAP.get(c, c) for c in name)

def _remove_duplicates(name: str) -> str:
    """Remove consecutive duplicate letters."""
    if not name:
        return name
    result = [name[0]]
    for c in name[1:]:
        if c != result[-1]:
            result.append(c)
    return ''.join(result)

def _pad_to_four(code: str) -> str:
    """Pad code with zeros to 4 characters."""
    return (code + '000')[:4]

def _soundex_custom(name: str) -> str:
    """
    Compute custom Soundex code.

    Steps:
    1. Keep first letter
    2. Apply special combinations
    3. Map consonants
    4. Remove duplicates
    5. Remove vowels (except first)
    6. Pad to 4 characters
    """
    if not name:
        return '0000'

    cleaned = _clean_and_upper(name)
    if not cleaned:
        return '0000'

    # Keep first letter
    first_letter = cleaned[0]
    rest = cleaned[1:]

    # Apply special combinations to the rest
    rest = _apply_special_combinations(rest)

    # Map consonants
    rest = _map_consonants(rest)

    # Remove vowels from rest
    rest = ''.join(c for c in rest if c not in 'AEIOUY')

    # Remove duplicates
    rest = _remove_duplicates(rest)

    # Combine and pad
    code = first_letter + rest
    return _pad_to_four(code)

def encode(name: str) -> Set[str]:
    """
    Return the Soundex code as a set (for compatibility with phonetic_codes.py).

    :param name: The surname to encode.
    :returns: A one-element set containing the 4-character Soundex code.
    """
    try:
        code = _soundex_custom(name)
        return {code}
    except Exception:
        if LOG:
            LOG.debug("Soundex custom encoding failed for %r", name)
        return {'0000'}

# Test cases
if __name__ == "__main__":
    # Test the examples
    tests = [
        ("STEPHEN", "SDBM"),  # S-T[D]-[e]-PH[F->B]-[e]-N[M]
        ("ASHCRAFT", "ASCR"),  # A-S[S]-H-C[C]-R[R]-A-F[B]-T[D]
        ("KNIGHT", "NCD"),    # KN[N]-I-GH-T[D]
    ]

    for name, expected in tests:
        result = _soundex_custom(name)
        print(f"{name} -> {result} (expected: {expected}) {'✓' if result == expected else '✗'}")

or a pseudo phonetic_french one:

#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2025  Custom French Phonetic Encoder
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#

"""
Custom French phonetic encoder for the Fuzzy Matching Gramplet.

Implements a 16-step French phonetic transformation algorithm.
"""

# Compatible with Python 3.6
import re
import unicodedata
from typing import Set

LOG = None  # Will be set by the importing module

#: Registry key
ALGORITHM_ID = "phonetic_french"

#: Display label
ALGORITHM_LABEL = "French Phonetic"

def encode(name: str) -> Set[str]:
    """
    Apply the 16-step French phonetic transformation.

    :param name: The surname to encode.
    :returns: A one-element set containing the phonetic code.
    """
    try:
        # Step 0: Convert to string and normalize
        strval = str(name)
        r = unicodedata.normalize('NFKD', strval.upper().strip()).encode('ASCII', 'ignore').decode('ASCII')

        # Step 1: Replace Y by I
        r = r.replace('Y', 'I')

        # Step 2: Replace accented E by Y
        r = r.replace(u'É', 'Y')
        r = r.replace(u'È', 'Y')
        r = r.replace(u'Ê', 'Y')

        # Step 3: Remove H not preceded by C, S, or P
        r = re.sub(r'([^CSP])H', r'\1', r)

        # Step 4: Replace PH by F
        r = r.replace('PH', 'F')

        # Step 5: Replace G followed by A, AI, AM, AN by K
        r = re.sub(r'G(AI?[N|M])', r'K\1', r)

        # Step 6: Replace vowel+I+N/M followed by vowel
        r = re.sub(r'([A|E]I[N|M])([A|E|I|O|U])', r'YN\2', r)

        # Step 7: Replace specific groups (water sounds)
        r = r.replace('EAU', 'O')
        r = r.replace('OUA', '2')
        r = r.replace('EIN', '4')
        r = r.replace('AIN', '4')
        r = r.replace('EIM', '4')
        r = r.replace('AIM', '4')

        # Step 8: Replace AI, EI, ER, ESS, ET, EZ
        r = r.replace('AI', 'Y')
        r = r.replace('EI', 'Y')
        r = r.replace('ER', 'YR')
        r = r.replace('ESS', 'YS')
        r = r.replace('ET', 'YT')
        r = r.replace('EZ', 'YZ')

        # Step 9: Replace AN, ON, AM, EN, EM, IN (not followed by vowel or sound 1-4)
        r = re.sub(r'AN([^A|E|I|O|U|1|2|3|4])', r'1\1', r)
        r = re.sub(r'ON([^A|E|I|O|U|1|2|3|4])', r'1\1', r)
        r = re.sub(r'AM([^A|E|I|O|U|1|2|3|4])', r'1\1', r)
        r = re.sub(r'EN([^A|E|I|O|U|1|2|3|4])', r'1\1', r)
        r = re.sub(r'EM([^A|E|I|O|U|1|2|3|4])', r'1\1', r)
        r = re.sub(r'IN([^A|E|I|O|U|1|2|3|4])', r'4\1', r)

        # Step 10: Replace S by Z (between vowels or sounds 1-4)
        r = re.sub(r'([A|E|I|O|U|Y|1|2|3|4])S([A|E|I|O|U|Y|1|2|3|4])', r'\1Z\2', r)

        # Step 11: Replace specific groups
        r = r.replace('OE', 'E')
        r = r.replace('EU', 'E')
        r = r.replace('AU', 'O')
        r = r.replace('OI', '2')
        r = r.replace('OY', '2')
        r = r.replace('OU', '3')

        # Step 12: Replace CH, SCH, SH, SS, SC
        r = r.replace('CH', '5')
        r = r.replace('SCH', '5')
        r = r.replace('SH', '5')
        r = r.replace('SS', 'S')
        r = r.replace('SC', 'S')

        # Step 13: Replace C by S (before E or I)
        r = re.sub(r'C([E|I])', r'S\1', r)

        # Step 14: Replace letters
        r = r.replace('C', 'K')
        r = r.replace('Q', 'K')
        r = r.replace('QU', 'K')
        r = r.replace('GU', 'K')
        r = r.replace('GA', 'KA')
        r = r.replace('GO', 'KO')
        r = r.replace('GY', 'KY')

        # Step 15: Replace remaining letters
        r = r.replace('A', 'O')
        r = r.replace('D', 'T')
        r = r.replace('P', 'T')
        r = r.replace('J', 'G')
        r = r.replace('B', 'F')
        r = r.replace('V', 'F')
        r = r.replace('M', 'N')

        # Step 16: Remove duplicate letters
        oldc = '#'
        newr = ''
        for c in r:
            if oldc != c:
                newr = newr + c
            oldc = c
        r = newr

        # Step 17: Remove ending T or X
        r = re.sub(r'(.*)[T|X]$', r'\1', r)

        return {r}

    except Exception:
        if LOG:
            LOG.debug("French phonetic encoding failed for %r", name)
        return {'0'}

We are converting to a Filter Rule based system for adding phonetic encoding systems. (Where the rule has adapting apply() for 5.2 to apply_to_one() for 6.0+)

The original approach did not make the encoding systems available to the rest of Gramps.

Sorry for the delay. I am out of AI tokens for a few more hours.)

Rule based Phonetic system version available for testing.

No problem. It was just a try (test).

Note, maybe a fallback around gramplet load failure has been added on recent branches (6.0, 6.1, 6.2, etc.)? Under 5.2 branch, we might still get:

Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/gramps/gui/widgets/grampletbar.py", line 478, in __page_added
    gramplet.set_orientation(self.orientation)
  File "/usr/lib/python3/dist-packages/gramps/gui/widgets/grampletbar.py", line 693, in set_orientation
    self.pui.set_orientation(orientation)
AttributeError: 'NoneType' object has no attribute 'set_orientation'

which could be fixed (I suppose) via a simple:

if self.pui: # or if gramplet
    self.pui.set_orientation(orientation) # gramplet.set_orientation(self.orientation)
else:
    return
    LOG.info(...)

Thanks! That confirmed (for Claude, anyway :face_with_spiral_eyes:) the issue. Since we don’t have access to patch the core, Claude is creating a workaround.

Does Claude also get problem on this section?
In fact, I just get recurent indirect error message around grampletbar layout when anything goes wrong on python coding (like during an experimental or incomplete python version downgrading or backport).

I am not sure that it is fixed on gramps 6.0
See Allow gramplets to have an orientation dependent layout by Nick-Hall · Pull Request #1370 · gramps-project/gramps · GitHub

I should be able to make a workaround on my local version too. An elegant patch with a better error message might be more useful for everyone. For reproducing it, just generate a code error (like a python syntax error) on one of your rules.

Yes. New 0.3.1 version will be posted within 15 30 minutes.

This is my insecurity about contributing code asking: do you think this Gramplet is (or will be) useful? Am I going in the right direction?