bacommon.langstr package

Language-agnostic complex strings – the LangStrSpec authoring model.

“LangStr” is the name for this whole generation of string handling (formerly working-titled Lstr2); the legacy client translation class keeps the Lstr name, which stays reserved for it to avoid ambiguity. Within that generation the type split is semantic (see strings-asset-migration.md D28 in ballistica-internal):

  • LangStrSpec (here) is the authoring spec form – a claim about a string carrying no guarantee that its asset-package is locally present (or even still exists). This is the currency for authoring surfaces and wire/model dataclasses; consuming ends verify/resolve before display.

  • The native client babase.LangStr (a later optimized C++ port of this proven model) represents a verified-local string – holding one implies it is displayable there. Its .spec property projects back to this form (always valid); there is deliberately no public unverified->verified conversion.

The model lets us pass around minimal, language-independent representations of a complex string (substitutions, plurals, nesting) and resolve to a flat string in a particular language only at display – so one representation serves clients of any language.

type bacommon.langstr.EncodedLangStr = list[str | int | EncodedLangStr]
class bacommon.langstr.LangStrDir(apverid: str, tree: WrapperTree, prefix: str = '')[source]

Bases: object

Runtime root/subdir accessor for a generated wrapper package.

exception bacommon.langstr.LangStrError[source]

Bases: Exception

A malformed language-string or encode-context operation.

class bacommon.langstr.LangStrSpec[source]

Bases: IOMultiType[LangStrSpecTypeID]

A deferred, language-agnostic complex string.

The base of a small multitype: a language-string is a LangStrSpecResource (an asset-package string addressed by apverid + logical name – the common authored form), a LangStrSpecValue (a raw literal that needs no package), or a LangStrSpecResourceIndexed (the compact integer-addressed projection of a resource, for contexts that carry a package-index map). All forms take keyword substitutions whose values may themselves be language-strings, so a LangStrSpec is a recursive tree; it holds tokens, not text, and only decodes to a flat string in some particular locale at display time.

Wire notes: the indexed form is the multitype default, so it alone serializes without a type tag (it is the space-sensitive form). Clients older than LANGSTR_EXT_MIN_BUILD understand only tag-free resource values with flat subs; producers that know the client build must gate everything beyond that (nested subs and the value/indexed forms) on it.

classmethod get_default_type_id() LangStrSpecTypeID | None[source]

Return a type-id to be assumed when none is present.

By default, dataclassio errors when deserializing multitype data that contains no type-id value. Overriding this to return a type-id changes that behavior: data with no type-id present will be deserialized as the returned type, and instances of that type will be serialized without a type-id value. This both saves a bit of space and allows ‘upgrading’ an existing regular dataclass to a multitype - simply designate the original dataclass type as the default and old serialized data will remain loadable (and data for the default type will remain loadable by old code).

Be aware of the following, however:

  • Once serialized data exists anywhere without type-id values, the default type-id must never be changed or removed; doing so would cause that existing data to be silently reinterpreted as some other type (or to error).

  • A missing type-id normally acts as a sanity check when deserializing; defining a default effectively disables that check, meaning malformed data may deserialize successfully as the default type instead of erroring.

classmethod get_type(type_id: LangStrSpecTypeID) type[LangStrSpec][source]

Return the subclass for each of our type-ids.

classmethod get_type_id() LangStrSpecTypeID[source]

Return the type-id for this subclass.

classmethod get_type_id_storage_name() str[source]

Return the key used to store type id in serialized data.

The default is a short obscure value so that it is unlikely to conflict with members of individual type attrs, but in some cases one might prefer to serialize it to something simpler like ‘type’ by overriding this call. One just needs to make sure that no encompassed types serialize anything to that same name themself (dataclassio will error if they do).

class bacommon.langstr.LangStrSpecResource(apverid: str, name: str, subs: dict[str, str | int | ~bacommon.langstr._core.LangStrSpec]=<factory>)[source]

Bases: LangStrSpec

An asset-package string: the common authored LangStrSpec form.

subs maps each substitution keyword to its value – a flat str / int or a nested LangStrSpec. A no-arg string has empty subs. The value carries its own exact apverid (so an encode context can discover the package union from the values themselves) and the string’s logical name (mapped to its integer index at encode time).

apverid: str
classmethod get_type_id() LangStrSpecTypeID[source]

Return the type-id for this subclass.

name: str
subs: dict[str, str | int | LangStrSpec]
class bacommon.langstr.LangStrSpecResourceIndexed(pkg: int, index: int, subs: list[str | int | LangStrSpec] = <factory>)[source]

Bases: LangStrSpec

The compact integer-addressed projection of a resource string.

Usable only against a context carrying the {pkg_int: apverid} package-index map (and package structures for positional-sub ordering); see LanguageStringDecodeContext. Substitutions are positional here (canonical param order), matching the EncodedLangStr chunk model. This form is the multitype default, so it serializes without a type tag.

classmethod get_type_id() LangStrSpecTypeID[source]

Return the type-id for this subclass.

index: int
pkg: int
subs: list[str | int | LangStrSpec]
class bacommon.langstr.LangStrSpecTypeID(*values)[source]

Bases: Enum

Type IDs for the LangStrSpec multitype’s forms.

RESOURCE = 'r'
RESOURCE_INDEXED = 'i'
VALUE = 'v'
class bacommon.langstr.LangStrSpecValue(value: str, subs: dict[str, str | int | ~bacommon.langstr._core.LangStrSpec]=<factory>)[source]

Bases: LangStrSpec

A raw literal string value needing no asset package.

For server-generated dynamic text (player names, pre-formatted numbers, etc.) that rides a LangStrSpec-shaped slot without a package entry. The value is locale-independent; subs are substituted into {name} tokens exactly like a plain resource value (nested language-strings allowed).

classmethod get_type_id() LangStrSpecTypeID[source]

Return the type-id for this subclass.

subs: dict[str, str | int | LangStrSpec]
value: str
class bacommon.langstr.LanguageStringDecodeContext(package_index_map: dict[int, str], structures: dict[str, PackageStructure], language: dict[str, dict[str, str | StringSelector]], locale: Locale)[source]

Bases: object

Decodes chunks into flat strings for one target locale.

Holds the {pkg_int: apverid} map (from the encoder), the package structures, and the per-apverid string values for a single locale. decode() resolves a chunk (recursively rendering nested values) via bacommon.loctext.evaluate().

decode(encoded: EncodedLangStr) str[source]

Resolve a chunk to a flat string in this context’s locale.

Fail-visible: any structural problem yields an LANGSTR_ERROR:… sentinel (and a logged warning) rather than crashing the caller.

decode_value(lstr: LangStrSpec) str[source]

Resolve any language-string form to flat text in this locale.

The tolerant all-forms counterpart of decode(): handles LangStrSpecResourceIndexed (via this context’s package-index map + structures), LangStrSpecValue (self-contained), and plain LangStrSpecResource (by name, for legacy/mixed payloads). Fail-visible like everything else on the decode side.

to_resource(lstr: LangStrSpec, _depth: int = 0) LangStrSpec[source]

Convert integer-indexed nodes back to the resource form.

The inverse of LanguageStringEncodeContext.to_indexed, for consumers that ingest indexed wire values but hold some of them in the self-describing name form (e.g. deferred client effects that outlive their containing payload’s package-index map). Returns new objects (resource/value nodes are rebuilt with converted subs); raises LangStrError for indices unknown to this context.

class bacommon.langstr.LanguageStringEncodeContext(lstrs: list[LangStrSpec], structures: dict[str, PackageStructure])[source]

Bases: object

Encodes LangStrSpec values into minimal language-free chunks.

Built from the batch of values to send: it computes the union of apverids they reference (recursively – nested values know their own apverid) and assigns each a stable integer index. encode() then emits [pkg_int, str_int, …subs]; package_index_map is the only mapping the consumer needs (string indices resolve from the content-pinned apverid itself).

encode(lstr: LangStrSpec) EncodedLangStr[source]

Encode one value (recursively) into a minimal chunk.

property package_index_map: dict[int, str]

The {pkg_int: apverid} map a decoder needs.

to_indexed(lstr: LangStrSpec) LangStrSpec[source]

Convert a resource/value tree to its integer-indexed form.

Resource nodes become LangStrSpecResourceIndexed (with positional subs in canonical param order); literal LangStrSpecValue nodes pass through (with their nested subs converted). New objects are returned; the input tree is never mutated. Raises LangStrError loudly for packages/strings unknown to this context (authoring-side errors) or already-indexed input.

class bacommon.langstr.LanguageStringNameDecodeContext(language: dict[str, dict[str, str | StringSelector]], locale: Locale)[source]

Bases: object

Decodes LangStrSpec values directly, by name, for one locale.

The name-based counterpart to LanguageStringDecodeContext: it resolves an in-memory LangStrSpec (carrying its apverid, string name, and keyword subs) straight against per-apverid per-locale values – no integer indices, package-index-map, or PackageStructure needed, since the subs are self-describing keyword->value pairs. This is the client’s primary path: resolve the referenced packages, gather their per-locale values, then decode each LangStrSpec in the client’s locale.

Fail-visible like LanguageStringDecodeContext – any structural problem yields an LANGSTR_ERROR:… sentinel (and a logged warning) rather than crashing the caller.

decode(lstr: LangStrSpec) str[source]

Resolve a LangStrSpec to a flat string in this locale.

Fail-visible: any structural problem yields an LANGSTR_ERROR:… sentinel (and a logged warning) rather than crashing the caller.

class bacommon.langstr.PackageDef(apverid: str, strings: tuple[StringDef, ...])[source]

Bases: object

Language-free definition of one asset-package-version’s strings.

The shared source the encode/decode PackageStructure and the type-safe wrapper codegen both derive from (in the real system, from an apverid’s resolved listing; in tests, hand-built).

apverid: str
strings: tuple[StringDef, ...]
class bacommon.langstr.PackageStructure(apverid: str, strings: dict[str, tuple[str, ...]])[source]

Bases: object

Language-free structure of one asset-package-version.

Maps string names <-> integer indices (assigned in canonical sorted-name order so both ends agree without shipping the mapping) and holds each string’s ordered substitution-keyword list. Carries no translations – encoding needs only this.

apverid

strings maps each logical name to its substitution keywords (() for a no-arg string). Order of the passed keywords is ignored: positional-substitution order is canonically alphabetical, enforced here so producer- and consumer-derived structures can’t disagree on it.

classmethod from_def(pkgdef: PackageDef) PackageStructure[source]

Build the encode/decode structure from a package definition.

classmethod from_language_values(apverid: str, values: dict[str, str | StringSelector]) PackageStructure[source]

Derive the structure from one locale’s complete value set.

The consumer-side counterpart of from_def(): string indices come from the canonical sorted-name order (the key set is identical across locales by construction – see complete_locale_values) and each string’s substitution keywords from its own value via bacommon.loctext.substitution_names(). Both ends canonicalize param order alphabetically, so a structure derived here agrees with the producer’s brief-derived one; producer tests lock that agreement.

index_of(name: str) int[source]

Return the integer index for a string name.

name_of(index: int) str[source]

Return the string name for an integer index.

params_of(name: str) tuple[str, ...][source]

Return the ordered substitution keywords for a string name.

class bacommon.langstr.StringDef(path: str, params: tuple[tuple[str, str], ...] = (), docs: str = '', english: str = '')[source]

Bases: object

One string’s language-free definition.

params is the ordered list of (keyword, kind) where kind is 'text' (a text sub -> str | LangStrSpec) or 'count' (the plural pivot -> int); () for a no-arg string. The canonical ordering (sorted keyword) is what fixes the positional substitution order.

docs (author usage docs) and english (an English preview of the rendered text) are optional docstring material for wrapper codegen only – neither participates in the encode/decode structure.

docs: str = ''
english: str = ''
params: tuple[tuple[str, str], ...] = ()
path: str
class bacommon.langstr.WrapParams(min_lines: int = 1, max_lines: int | None = None, max_chars_per_line: int | None = None)[source]

Bases: object

Constraints for splitting a text value into lines client-side.

Mirrors the engine’s simple equal-width line splitter (babase.split_text_into_lines()): text is broken only at valid line-break opportunities, using the fewest lines that keep every line within max_chars_per_line (when provided) while staying between min_lines and max_lines (None means unlimited), with line lengths balanced within that count. So max_chars_per_line alone gives basic wrapping and min_lines alone gives an exact line count. Constraints are best-effort.

Default to pinning an exact line count: set min_lines to the layout’s designed count and leave max_chars_per_line unset. A max_chars_per_line-driven wrap yields a per-locale varying line count (translation lengths differ), which reads as broken in layouts designed around a specific count — and every legacy-converted string is such a layout, since the legacy pipeline hand-baked newlines at fixed counts (see D21 in the strings-asset-migration initiative). Reserve max_chars_per_line for surfaces explicitly designed to tolerate a variable number of lines.

Per decision D-t these are definition-time presentation hints: a string definition carries them optionally, they ride each locale blob, and evaluation applies them automatically. They are locale-invariant, and width-driven layout consumers may ignore them (they are a fallback presentation default).

max_chars_per_line: int | None = None
max_lines: int | None = None
min_lines: int = 1
type bacommon.langstr.WrapperTree = dict[str, tuple[str, ...] | WrapperTree]
bacommon.langstr.collect_apverids(lstr: LangStrSpec, acc: set[str]) None[source]

Gather every asset-package-version a language-string tree references into acc.

Indexed nodes resolve against an out-of-band context so they contribute no apverids themselves, but their substitution values are still walked (a resource-form node can appear anywhere in a mixed tree).

Note to implementers: keep this a module-level function; a self-recursive closure would create a reference cycle (function -> closure cell -> function) at every call site, adding cyclic-gc pressure the engine works hard to avoid.

bacommon.langstr.contains_resource_form(lstr: LangStrSpec) bool[source]

Return whether a language-string tree contains any full resource-form (non-indexed) node.

Used by consumers verifying that a wire payload claiming the integer-indexed form really is fully indexed (a resource-form leak means some producer path skipped indexing).

bacommon.langstr.package_structure(apverid: str, tree: WrapperTree) PackageStructure[source]

Build a PackageStructure from a wrapper’s runtime _TREE.

Flattens the nested tree into the {logical-path: param-keywords} map the encode/decode contexts need – so a consumer of a vendored package just passes module.APVERID, module._TREE (both module-level).

bacommon.langstr.parse_language_blob(text: str) dict[str, str | StringSelector][source]

Parse a canonical language blob into a {name: value} map.

The exact inverse of serialize_language_blob(): reads the top-level strings object, turning each value back into a str (plain) or a StringSelector (a dict). A blob with no strings key (e.g. a legacy-only package) yields an empty map; malformed values are skipped (fail-soft on the consumer side).

bacommon.langstr.serialize_language_blob(values: dict[str, str | StringSelector], wraps: dict[str, WrapParams] | None = None) str[source]

Serialize a per-locale value map to the canonical language blob.

values maps each string’s logical name to its value – a plain str or a StringSelector. wraps optionally maps names to their definition-time WrapParams (decision D-t); a wrapped entry is emitted as a {'v': value, 'w': wrap} carrier dict (which pre-wrap clients skip fail-soft). Output is deterministic (sorted keys, fixed formatting) for cache stability and diffability.