Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?

like image 671
Raymond Hettinger Avatar asked Dec 08 '25 13:12

Raymond Hettinger


1 Answers

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.

like image 168
Raymond Hettinger Avatar answered Dec 10 '25 03:12

Raymond Hettinger



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!