os_helper.i18n_utils module

Localization Utilities

A package that shows text to a person in more than one language needs one place where that text lives, and one rule for which version comes out. Spread the strings through the source and you get the two failures this module exists to prevent: a sentence that exists in French and not in English, found by a user rather than by a test, and a sentence edited in one language whose twin silently keeps the old wording.

Locales loads a locales/i18n.yaml file — the suite’s canonical shape, described in CODING.md section 21.3 — and answers two questions: what is the text for this key in this language, and is this file complete.

The file is organised in sections of semantic keys, each key carrying one string per locale:

meta:
  default_locale: fr
  supported_locales: [fr, en]
gui:
  submit_button:
    fr: Envoyer
    en: Send

Sections are named by the caller and carry no meaning here: this module parses, resolves and validates, and never knows what any one section is for. That is deliberate — it is what lets a single loader serve every member of the suite whatever each one keeps in its file.

Keys are semantic, not the English text (section 21.3.1): wording changes more often than meaning, so submit_button keeps the code untouched when the copy is rewritten.

Resolution is deterministic (section 21.3.4). A key missing in the requested locale falls back to meta.default_locale; a key missing there too raises, because a raw key reaching a user is worse than a loud failure in testing.

Usage example

>>> import os_helper as osh
>>> loc = osh.Locales.from_path("locales/i18n.yaml")
>>> loc.text("gui", "submit_button", "en")
'Send'
>>> loc.render("gui", "greeting", "fr", name="Alice")
'Bonjour Alice'

Author

Warith HARCHAOUI, https://linkedin.com/in/warith-harchaoui

class os_helper.i18n_utils.Locales(document, path)[source]

Bases: object

A parsed locales/i18n.yaml: sections of keys, each with one text per locale.

Build one with from_path() rather than calling the constructor, so the file is validated as it is read.

Parameters:
document

The whole parsed file, sections included.

Type:

dict

path

Where it was read from. Used in error messages, so a failure names the file to fix.

Type:

pathlib.Path

property default_locale: str

The locale that answers when the asked-for one has nothing.

document: dict[str, Any]
classmethod from_path(path)[source]

Parse a locale file, failing loudly if it cannot serve its purpose.

Parameters:

path (str or pathlib.Path) – Path to the YAML file, conventionally locales/i18n.yaml at the package root.

Returns:

The parsed file.

Return type:

Locales

Raises:
  • FileNotFoundError – If the file is absent.

  • RuntimeError – If it does not parse as a YAML mapping, or lacks a usable meta section. A malformed locale file must fail at load rather than surface later as a puzzling KeyError deep in a caller.

path: Path
problems(*, require_parity=True, sections=None)[source]

Return everything wrong with this file, as sentences, worst first.

Built for a test to assert on (section 21.3.7). It reports all the problems rather than raising on the first, because a translator fixing a file wants the whole list in one pass.

Parameters:
  • require_parity (bool, optional) – When true (the default), a key whose translations do not all take the same {placeholder} fields is reported. Parity matters because a template missing a field renders a sentence with a hole in it, in that language only.

  • sections (list of str, optional) – Which sections to check. Defaults to every content section except those listed in unlocalized_sections, since a section that is deliberately single-language has no translations to miss.

Returns:

One sentence per problem. Empty when the file is sound.

Return type:

list of str

Examples

>>> import os_helper as osh
>>> osh.Locales.from_path("locales/i18n.yaml").problems()
[]
raw(section, key)[source]

Return one entry exactly as written, with no locale resolution.

The accessor for an unlocalized section, whose sub-keys are not locales and so cannot be resolved by text().

Parameters:
  • section (str) – Section name.

  • key (str) – Key within that section.

Returns:

Whatever the file holds there — usually a string or a mapping.

Return type:

Any

Raises:

KeyError – If the section or the key is absent.

render(section, key, locale=None, **values)[source]

Return a key’s text with its {placeholder} fields filled in.

Templates are data, never code (section 21.3.5): this formats, it never evaluates, and it checks the placeholder names first so a caller learns what it forgot instead of shipping a half-filled sentence.

Parameters:
  • section (str) – Section name.

  • key (str) – Semantic key within that section.

  • locale (str, optional) – Wanted locale. Defaults to default_locale.

  • **values – One argument per placeholder in the template.

Returns:

The filled text.

Return type:

str

Raises:

KeyError – If the template needs a placeholder that values does not carry.

section(name)[source]

Return one whole section: semantic key -> {locale: text}.

Parameters:

name (str) – Section name, as spelled in the file.

Returns:

The section’s entries.

Return type:

dict

Raises:

KeyError – If the section is absent.

sections()[source]

Section names, meta excluded — the content sections.

Return type:

list[str]

property supported_locales: list[str]

Every locale this file declares, in declaration order.

text(section, key, locale=None)[source]

Return the text for one key, falling back to the default locale.

Parameters:
  • section (str) – Section name.

  • key (str) – Semantic key within that section.

  • locale (str, optional) – Wanted locale. Defaults to default_locale.

Returns:

The text in locale, or in the default locale when locale has no entry for this key.

Return type:

str

Raises:

KeyError – If the key is absent, or carries neither the wanted locale nor the default one. Returning the raw key instead would push a developer’s identifier in front of a user (section 21.3.4).

property unlocalized_sections: list[str]

Sections whose keys are deliberately not indexed by locale.

A product may keep one section in a single language on purpose, and then its sub-keys mean something else entirely — a role, a variant, a field name. Declaring them here is what stops problems() from reading those sub-keys as locale codes and reporting a wall of translations that were never meant to exist.

Declared in the file, so the loader stays ignorant of what any section is for:

meta:
  unlocalized_sections: [templates]
os_helper.i18n_utils.META_SECTION = 'meta'

it declares which locales exist and which one answers when the asked-for locale has nothing.

Type:

The section every file must carry