jumpserver/jumpserver · error · ValueError

invalid i18n key

Error message

invalid i18n key

What it means

In apps/common/utils/yml.py, a Jinja2 environment registers a 'trans' filter whose safe_trans wrapper only accepts string keys; passing a non-str (int, dict, list, None) raises ValueError('invalid i18n key') before translate() is called. StrictUndefined is also set on the env, so template data must be complete.

Source

Thrown at apps/common/utils/yml.py:42

    lang = settings.LANGUAGE_CODE if lang is None else lang
    lang = lang.lower().replace('_', '-')
    lang_data = i18n.get(key, {})
    return lang_data.get(lang, lang_data.get(lang.split('-')[0], key))


def yaml_load_with_i18n(stream, lang=None):
    ori_text = stream.read()
    data = yaml.safe_load(ori_text)
    i18n = data.get("i18n", {})

    env = SandboxedEnvironment(
        undefined=StrictUndefined,
        autoescape=False,
    )

    def safe_trans(key):
        if not isinstance(key, str):
            raise ValueError("invalid i18n key")
        return translate(key, i18n, lang)

    env.filters.clear()
    env.globals.clear()
    env.filters["trans"] = safe_trans

    template = env.from_string(ori_text)
    try:
        rendered = template.render()
    except Exception as e:
        rendered = ori_text

    result = yaml.safe_load(rendered)
    result.pop("i18n", None)
    return result


def wrap_ansible_unsafe(value):

View on GitHub (pinned to 6ec464fabd)

Solutions

  1. Coerce to str at the call site: {{ x|string|trans }} or fix the data producer to emit message keys
  2. Guard in the template: {% if x is string %}{{ x|trans }}{% else %}{{ x }}{% endif %}
  3. Fix the context data so 'trans' only ever receives translation message keys (strings)
  4. If non-string passthrough should be allowed, relax safe_trans to return str(key) or the value unchanged instead of raising

Example fix

# before
{{ item.label | trans }}   {# item.label is sometimes 42 #}
# after
{{ item.label | string | trans }}
Defensive patterns

Strategy: type-guard

Validate before calling

key = context.get('label')
if not isinstance(key, str):
    context['label'] = key = str(key) if key is not None else ''
env.render(context)

Type guard

def is_trans_keyable(value) -> bool:
    return isinstance(value, str)

Try / catch

try:
    out = env.render(ctx)
except ValueError as e:
    if 'invalid i18n key' in str(e):
        ctx = {k: (v if not k.endswith('_trans') else str(v)) for k, v in ctx.items()}
        out = env.render(ctx)
    else:
        raise

Prevention

When it happens

Trigger: Rendering a Jinja2 template with {{ x | trans }} where x is a number, boolean, or nested object; programmatic calls env.filters['trans'](123); templates whose variable resolves to a translated value that is itself a dict/list.

Common situations: Templates iterating heterogeneous data where some items are numeric IDs passed through the filter; refactors changing a context variable from str to an enum/int; YAML/template data where the key field is optional and sometimes null.

Related errors


AI-assisted analysis of jumpserver/jumpserver@6ec464fabd (2026-08-28). Data as JSON: /api/errors/9fff9212d18b9eec. Report an issue: GitHub.