How can I apply gettext translations to string literals in case statements?
Asked Answered
H

1

18

I need to add gettext translation to all the string literals in our code, but it doesn't work with literals in case statements.

This failed attempt gives SyntaxError: Expected ':':

from gettext import gettext as _

direction = input(_('Enter a direction: '))   # <-- This works
match direction:
    case _('north'):                          # <-- This fails
        adj = 1, 0
    case _('south'):
        adj = -1, 0
    case _('east'):
        adj = 0, 1
    case _('west'):
        adj = 0, -1
    case _:
        raise ValueError(_('Unknown direction'))

What does the error mean and how can the directions be marked for translation?

Hollyanne answered 13/5, 2022 at 6:17 Comment(0)
H
18

What does the error mean?

The grammar for the match/case statement treats the _ as a wildcard pattern. The only acceptable token that can follow is a colon. Since your code uses an open parenthesis, a SyntaxError is raised.

How to fix it

Switch from a literal pattern such as case "north": ... to a value pattern such as case Directions.north: ... which uses dot-operator.

The translation can then be performed upstream, outside of the case statement:

from gettext import gettext as _

class Directions:
    north = _('north')
    south = _('south')
    east = _('east')
    west = _('west')

direction = input(_('Enter a direction: '))
match direction:
    case Directions.north:
        adj = 1, 0
    case Directions.south:
        adj = -1, 0
    case Directions.east:
        adj = 0, 1
    case Directions.west:
        adj = 0, -1
    case _:
        raise ValueError(_('Unknown direction'))

Not only do the string literals get translated, the case statements are more readable as well.

More advanced and dynamic solution

The above solution only works if the choice of language is constant. If the language can change (perhaps in an online application serving users from difference countries), dynamic lookups are needed.

First we need a descriptor to dynamically forward value pattern attribute lookups to function calls:

class FuncCall:
    "Descriptor to convert fc.name to func(name)."

    def __init__(self, func):
        self.func = func

    def __set_name__(self, owner, name):
        self.name = name

    def __get__(self, obj, objtype=None):
        return self.func(self.name)

We use it like this:

class Directions:
    north = FuncCall(_)  # calls _('north') for every lookup
    south = FuncCall(_)
    east = FuncCall(_)
    west = FuncCall(_)

def convert(direction):
    match direction:
        case Directions.north:
            return 1, 0
        case Directions.south:
            return -1, 0
        case Directions.east:
            return 0, 1
        case Directions.west:
            return 0, -1
        case _:
            raise ValueError(_('Unknown direction'))
    print('Adjustment:', adj)

Here is a sample session:

>>> set_language('es')   # Spanish
>>> convert('sur')
(-1, 0)
>>> set_language('fr')   # French
>>> convert('nord')
(1, 0)

Namespaces for the Value Pattern

Any namespace with dotted lookup can be used in the value pattern: SimpleNamespace, Enum, modules, classes, instances, etc.

Here a class was chosen because it is simple and will work with the descriptor needed for the more advanced solution.

Enum wasn't considered because it is much more complex and because its metaclass logic interferes with the descriptors. Also, Enum is intended for giving symbolic names to predefined constants rather than for dynamically computed values like we're using here.

Hollyanne answered 13/5, 2022 at 6:17 Comment(3)
Incidentally, any chance of appropriate parts of the standard library being retrofitted to use types.SimpleNamespace?Imitate
No, I think Raymond only used SimpleNamespace here (rather than e.g. Enum) to take up less space on that part and allow more focus to be put on the match block. Though it may take up a couple more lines of code, using Enum is safer, better code than using SimpleNamespace here, and friendlier to static analysis (e.g. linters can catch typos more easily).Hilaire
A small remark: using gettext instead of _ in case expressions (e.g. case gettext('north')) will not resolve the issue either, and instead raises another error: TypeError: called match pattern must be a type. This is because the foo() is used for type matching. Anecdotally, some languages don't have cardinal directions, but I suppose that's for another question :)Nitrite

© 2022 - 2024 — McMap. All rights reserved.