LiveSvelteGettext.Plurals (LiveSvelteGettext v0.2.0)

View Source

Reads translated plural forms straight from a backend's .po files.

Why this exists

Gettext's runtime API cannot give us an untranslated-but-localized plural form. Gettext.Backend.lngettext/7 unconditionally overwrites the :count binding with the numeric n it was called with:

var!(bindings) = Map.put(bindings, :count, n)

So there is no way to ask it for the Spanish plural of "%{count} items" with the %{count} placeholder still intact - and the placeholder is exactly what has to reach the browser, because interpolation happens client-side with the real count.

Before this module existed, all_translations/1 worked around that by returning the raw English msgid/msgid_plural, which meant plurals were never translated at all.

Reading the .po file gives us the exact msgstr for every plural form, placeholders untouched.

Plural form selection

The browser has to pick a form from a list. Gettext identifies forms by a numeric index whose meaning is language-specific, while Intl.PluralRules in the browser speaks CLDR category names ("one", "few", "many", ...).

Rather than shipping a rule evaluator, we send a small probe table: a handful of representative counts and the gettext form index each one selects. The client asks Intl.PluralRules for the category of its count, finds a probe with the same category, and uses that probe's index. Both systems partition counts along the same linguistic lines, so a matching category implies a matching form.

Failure behaviour

Every function here degrades to :error or a safe default rather than raising. A missing, unreadable or malformed .po file must never take down a page render - it just means the untranslated strings are used.

Summary

Types

Map of {msgid, msgid_plural} to its translated forms

Translated plural forms, ordered by gettext form index

Functions

Loads the plural messages of a backend's .po file for locale.

Returns the number of plural forms locale has, defaulting to 2.

Returns the probe table for locale as a list of [count, form_index] pairs.

Types

catalog()

@type catalog() :: %{optional({String.t(), String.t()}) => forms()}

Map of {msgid, msgid_plural} to its translated forms

forms()

@type forms() :: [String.t()]

Translated plural forms, ordered by gettext form index

Functions

load(backend, locale)

@spec load(module(), String.t()) :: catalog()

Loads the plural messages of a backend's .po file for locale.

Returns a map keyed by {msgid, msgid_plural}. Messages with any empty plural form are skipped, matching Gettext's own behaviour of falling back to the original string when a translation is incomplete.

Returns an empty map when the locale has no .po file, when it cannot be read, or when the backend does not expose the Gettext introspection API.

Results are cached in :persistent_term, keyed by the file's size and modification time, so an edited .po file is picked up without a restart.

nplurals(backend, locale)

@spec nplurals(module(), String.t()) :: pos_integer()

Returns the number of plural forms locale has, defaulting to 2.

probes(backend, locale)

@spec probes(module(), String.t()) :: [[non_neg_integer()]]

Returns the probe table for locale as a list of [count, form_index] pairs.

The client uses this to map a count to a gettext plural form index. See the module documentation for how the mapping works.

Falls back to the English rule (0 for one, 1 for anything else) when the locale's plural rules cannot be determined.