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.specproperty 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:
objectRuntime root/subdir accessor for a generated wrapper package.
- exception bacommon.langstr.LangStrError[source]¶
Bases:
ExceptionA 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), aLangStrSpecValue(a raw literal that needs no package), or aLangStrSpecResourceIndexed(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 aLangStrSpecis 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_BUILDunderstand 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:
LangStrSpecAn asset-package string: the common authored
LangStrSpecform.subsmaps each substitution keyword to its value – a flatstr/intor a nestedLangStrSpec. A no-arg string has emptysubs. The value carries its own exactapverid(so an encode context can discover the package union from the values themselves) and the string’s logicalname(mapped to its integer index at encode time).- classmethod get_type_id() LangStrSpecTypeID[source]¶
Return the type-id for this subclass.
- class bacommon.langstr.LangStrSpecResourceIndexed(pkg: int, index: int, subs: list[str | int | LangStrSpec] = <factory>)[source]¶
Bases:
LangStrSpecThe 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); seeLanguageStringDecodeContext. Substitutions are positional here (canonical param order), matching theEncodedLangStrchunk 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.
- subs: list[str | int | LangStrSpec]¶
- class bacommon.langstr.LangStrSpecTypeID(*values)[source]¶
Bases:
EnumType IDs for the
LangStrSpecmultitype’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:
LangStrSpecA 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;subsare 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.
- 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:
objectDecodes 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) viabacommon.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(): handlesLangStrSpecResourceIndexed(via this context’s package-index map + structures),LangStrSpecValue(self-contained), and plainLangStrSpecResource(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); raisesLangStrErrorfor indices unknown to this context.
- class bacommon.langstr.LanguageStringEncodeContext(lstrs: list[LangStrSpec], structures: dict[str, PackageStructure])[source]¶
Bases:
objectEncodes
LangStrSpecvalues 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_mapis 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.
- 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); literalLangStrSpecValuenodes pass through (with their nested subs converted). New objects are returned; the input tree is never mutated. RaisesLangStrErrorloudly 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, *, param_kinds: dict[str, dict[str, dict[str, str]]] | None = None, components: dict[str, dict[str, str | StringSelector]] | None = None)[source]¶
Bases:
objectDecodes
LangStrSpecvalues directly, by name, for one locale.The name-based counterpart to
LanguageStringDecodeContext: it resolves an in-memoryLangStrSpec(carrying itsapverid, stringname, and keywordsubs) straight against per-apverid per-locale values – no integer indices, package-index-map, orPackageStructureneeded, 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 eachLangStrSpecin the client’s locale.Fail-visible like
LanguageStringDecodeContext– any structural problem yields anLANGSTR_ERROR:…sentinel (and a logged warning) rather than crashing the caller.- decode(lstr: LangStrSpec) str[source]¶
Resolve a
LangStrSpecto 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:
objectLanguage-free definition of one asset-package-version’s strings.
The shared source the encode/decode
PackageStructureand the type-safe wrapper codegen both derive from (in the real system, from an apverid’s resolved listing; in tests, hand-built).
- class bacommon.langstr.PackageStructure(apverid: str, strings: dict[str, tuple[str, ...]])[source]¶
Bases:
objectLanguage-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¶
stringsmaps 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 – seecomplete_locale_values) and each string’s substitution keywords from its own value viabacommon.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.
- class bacommon.langstr.StringDef(path: str, params: tuple[tuple[str, str], ...] = (), docs: str = '', english: str = '')[source]¶
Bases:
objectOne string’s language-free definition.
paramsis 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) andenglish(an English preview of the rendered text) are optional docstring material for wrapper codegen only – neither participates in the encode/decode structure.
- class bacommon.langstr.WrapParams(min_lines: int = 1, max_lines: int | None = None, max_chars_per_line: int | None = None)[source]¶
Bases:
objectConstraints 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 withinmax_chars_per_line(when provided) while staying betweenmin_linesandmax_lines(Nonemeans unlimited), with line lengths balanced within that count. Somax_chars_per_linealone gives basic wrapping andmin_linesalone gives an exact line count. Constraints are best-effort.Default to pinning an exact line count: set
min_linesto the layout’s designed count and leavemax_chars_per_lineunset. Amax_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). Reservemax_chars_per_linefor 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).
- 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.convert_time_subs(subs: dict[str, 'str | int | LangStrSpec | datetime.datetime | datetime.timedelta'], now: datetime.datetime | None = None) dict[str, 'str | int | LangStrSpec'][source]¶
Convert any time-typed sub values to their wire form.
The integer-milliseconds wire value is an implementation detail of the duration machinery; conversion is driven purely by each value’s type (see
time_sub_millis()), so no per-param kind knowledge is needed – the typed stubs are what hold authors to passing time types only for duration params.nowis resolved at most once per call.
- bacommon.langstr.data_size_str(bytecount: int, locale: Locale, values: dict[str, str | StringSelector], *, compact: bool = False) str[source]¶
Render a byte count as human-readable size in
locale.valuesis the components package’s per-locale value map. The ladder and the adaptive decimals (one place below ten units, none above) mirrorefro.util.data_size_str(), as doescompactfor width-constrained slots: the only rung it changes is bytes, which renders through the abbreviatedbytes_compactentry (“37 B”) instead of pluralizing (“37 bytes”) – larger rungs are already abbreviated. Abbreviations don’t inflect, so that entry is a plain{amount}template like the larger rungs, not a plural.Raises
KeyErrorif the components package is missing an entry – the decode path turns that into the usual fail-visibleLANGSTR_ERRORsentinel rather than letting it escape.
- bacommon.langstr.duration_str(millis: int, locale: Locale, values: dict[str, str | StringSelector], *, maxparts: int = 2, decimals: int = 0, direction: str | None = None, clamp: bool = False) str[source]¶
Render a length of time as composed abbreviated units.
Mirrors
efro.util.timedelta_str(“1h 23m”;maxpartscaps the composed units largest-first,decimalsapplies fractional places to the last one), rendering each part through the components package’s per-localeduration/unit templates and joining with the curatedduration_separator.millisfollows the signedtarget - nowconvention (D12), in integer milliseconds – the sub wire type must stayint, and ms precision is what keepsdecimalsmeaningful.direction('past'/'future') renders the magnitude of that sign and floors the other to0s– countdown-safe by construction;clampfloors negatives for an undirected length; with neither, negatives render as magnitude plus a leading-(D-neg, same caveats).Raises
KeyErrorif the components package is missing an entry – the decode path turns that into the usual fail-visibleLANGSTR_ERRORsentinel rather than letting it escape.
- bacommon.langstr.format_number(value: float, decimals: int, locale: Locale) str[source]¶
Render a number with fixed decimals and the locale’s mark.
Rounding happens first and the mark is swapped last, on a known ASCII rendering – never format-then-reparse, which would have to guess which of
./,it was looking at.
- bacommon.langstr.package_structure(apverid: str, tree: WrapperTree) PackageStructure[source]¶
Build a
PackageStructurefrom 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 passesmodule.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-levelstringsobject, turning each value back into astr(plain) or aStringSelector(a dict). A blob with nostringskey (e.g. a legacy-only package) yields an empty map; malformed values are skipped (fail-soft on the consumer side).
- bacommon.langstr.parse_language_components(text: str) dict[str, str | StringSelector][source]¶
Read the build-embedded formatter components out of a blob.
Same value shapes as
parse_language_blob()(plainstrorStringSelector), read from the siblingcomponentskey. Absent on any package with no spec’d params, and on every blob written before components existed – both yield an empty map rather than an error.
- bacommon.langstr.parse_language_param_kinds(text: str) dict[str, dict[str, str]][source]¶
Read the
{name: {param: kind}}map out of a language blob.The display-side counterpart of
param_kindsinserialize_language_blob(). Only strings carrying a non-text param appear; everything else is absent and reads as plain text substitution, which is what a blob written before this existed yields for every entry. Fail-soft throughout, matchingparse_language_blob()– a malformed entry is skipped rather than failing the whole blob.
- bacommon.langstr.render_display_param(kindexpr: str, value: str | int | float, locale: Locale, components: dict[str, str | StringSelector]) str[source]¶
Render one spec’d param value for a locale – the shared dispatch.
kindexpris the display-kind expression a blob’skcarrier (or a wrapper’s baked kinds map) holds – the bare kind or kind plus spec args ('bytes(compact=true)'); seedisplay_kind. The single render-dispatch both the server-side decode context and the client wrapper runtime route through, so the two sides can’t drift.Raises on a malformed expression, an unknown kind, or a missing component entry; callers apply their own fail-visible or fail-soft policy.
- bacommon.langstr.serialize_language_blob(values: dict[str, str | StringSelector], wraps: dict[str, WrapParams] | None = None, param_kinds: dict[str, dict[str, str]] | None = None, components: dict[str, str | StringSelector] | None = None) str[source]¶
Serialize a per-locale value map to the canonical language blob.
valuesmaps each string’s logical name to its value – a plainstror aStringSelector.wrapsoptionally maps names to their definition-timeWrapParams(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.param_kindsoptionally maps a name to its{param: kind}for params whose kind the display side must know – a byte count rendered as “1.2 GB” cannot be recovered from the translated text, which carries only a{name}token. It rides the same carrier under'k'.Pass only the params that actually need it (anything but
'text'). A string with none is emitted exactly as before, so adding this leaves every existing blob byte-identical – which matters because blob content is a cache key.
- bacommon.langstr.time_sub_millis(value: datetime | timedelta, now: datetime | None = None) int[source]¶
One time-typed sub value’s wire form (signed ms int).
The single shared arithmetic for wrapper accessors’ useful duration types: a
datetime.timedeltais already a signed length; adatetime.datetimeis an absolute time, converted to signedtarget - nowper D12 so the string’sdirhandles past/future.nowdefaults toefro.util.utc_now(); batch callers pass one shared value so a page of renders can’t drift against itself. Datetimes must be timezone-aware (naive ones raise from the subtraction, per stdlib rules).