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(apvernum: ApverNum, 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.LangStrFlatIndexContext(packages: list[ApverNum], counts: CountSource)[source]

Bases: object

Folds/unfolds indexed language-strings for one manifest.

Built from the manifest’s package list (in index order) and a way to obtain per-package string counts. The offset table is built once on first use.

describe_domain() → str[source]

Per-package string counts, for diagnosing a digest mismatch.

domain_digest() → str[source]

Short digest of the exact domain this context addresses.

The string counterpart to domain_digest(), and for the same reason: a folded index that lands in the wrong package still decodes to a string, so nothing downstream notices. A payload carries the producer’s digest and the consumer refuses to unfold when its own does not match.

Counts alone, since counts are all the layout depends on – the names stay native at both ends.

domain_size() → int[source]

Total number of addressable strings across the manifest.

from_flat(flat: int) → tuple[int, int][source]

Unfold one integer back into (package index, string index).

to_flat(pkg: int, index: int) → int[source]

Fold a (package index, string index) pair into one integer.

exception bacommon.langstr.LangStrIndexError[source]

Bases: Exception

A language-string index could not be folded or unfolded.

An authoring or wiring fault rather than a runtime condition: a package outside the manifest, or a flat index outside the domain.

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 apvernum + 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. One further form, LangStrSpecTimeTarget, is not a string at all but a substitution value (a moment in time) for duration params; it lives in this multitype because a substitution slot holds exactly one dataclass type.

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(apvernum: ApverNum, 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 apvernum (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).

apvernum: ApverNum
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: apvernum} 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.LangStrSpecTimeTarget(millis: int)[source]

Bases: LangStrSpec

A moment in time, as the value of a duration param.

Where a plain int duration sub is a fixed signed length (the target - now milliseconds computed when the string was built), this holds the absolute moment itself, so each evaluation measures it against that evaluation’s now. A string carrying one is therefore time-varying: a countdown (“Expires in 4m 12s”) or a since-then readout (“Joined 3m ago”) that stays correct for as long as it is displayed, with clients re-rendering it each time its visible text changes.

Only meaningful as the substitution value of a duration-formatted param ({t|duration(...)}, in a resource’s brief or a literal’s template); that param’s spec decides how it renders and in which direction, exactly as for a fixed length. Anywhere else – a plain-text param, or evaluated on its own – it is a decode error.

Wrapper accessors produce one for a datetime.datetime passed to a duration param without now; construct directly via at().

classmethod at(moment: datetime.datetime) → LangStrSpecTimeTarget[source]

Return a target for a timezone-aware datetime.

classmethod get_type_id() → LangStrSpecTypeID[source]

Return the type-id for this subclass.

millis: int

The moment, as UTC epoch milliseconds.

class bacommon.langstr.LangStrSpecTypeID(*values)[source]

Bases: Enum

Type IDs for the LangStrSpec multitype’s forms.

RESOURCE = 'r'
RESOURCE_INDEXED = 'i'
TIME_TARGET = 'tt'
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).

Note that value is a template, not raw display text: a {name} token with no matching sub is a fail-visible error at display time. Text not authored against this contract (user input, error messages, anything that could contain a brace) must go through literal() instead.

classmethod get_type_id() → LangStrSpecTypeID[source]

Return the type-id for this subclass.

classmethod join(items: Sequence[LangStrSpec], separator: str = '') → LangStrSpecValue[source]

Return a spec showing items one after another.

separator is literal text placed between items (a newline for a list, say); its braces display literally, and it is the same in every locale. Each item keeps its own translation and arguments, so a variable-length list of authored entries (one line per player, …) stays fully localized. Items must be language-strings – wrap plain text with literal().

The spec counterpart of the native babase.LangStr.join(), building exactly what it does: a generated template ({i0}<separator>{i1}...) with each item a nested substitution. Raises ValueError for more than MAX_JOIN_ITEMS (256) items or a result nested deeper than MAX_NESTING_DEPTH.

classmethod literal(text: str) → LangStrSpecValue[source]

Return a spec displaying text verbatim in every locale.

Escapes brace characters ({ -> {{, } -> }}) so the substitution engine renders them literally rather than treating {...} runs as substitution tokens. Use this for any dynamic text not authored as a template – otherwise text that happens to contain braces displays as a LANGSTR_ERROR:... sentinel.

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

Bases: object

Decodes chunks into flat strings for one target locale.

Holds the {pkg_int: apvernum} map (from the encoder), the package structures, and the per-apvernum 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[ApverNum, PackageStructure], also: set[ApverNum] | None = None)[source]

Bases: object

Encodes LangStrSpec values into minimal language-free chunks.

Built from the batch of values to send: it computes the union of apvernums they reference (recursively – nested values know their own apvernum) 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 apvernum itself).

encode(lstr: LangStrSpec) → EncodedLangStr[source]

Encode one value (recursively) into a minimal chunk.

property package_index_map: dict[int, ApverNum]

The {pkg_int: apvernum} 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[ApverNum, dict[str, str | StringSelector]], locale: Locale, *, param_kinds: dict[ApverNum, dict[str, dict[str, str]]] | None = None, components: dict[ApverNum, dict[str, str | StringSelector]] | None = None, now: datetime.datetime | None = None)[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 apvernum, string name, and keyword subs) straight against per-package 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(apvernum: ApverNum, 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 apvernum’s resolved listing; in tests, hand-built).

apvernum: ApverNum
strings: tuple[StringDef, ...]
class bacommon.langstr.PackageStructure(apvernum: ApverNum, 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.

apvernum

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(apvernum: ApverNum, 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.

string_count() → int[source]

How many strings this package holds.

The size of the canonical sorted name list – all a producer needs to fold indices into one flat domain across a manifest.

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 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. Characters are counted in Latin-width units: East Asian wide characters (CJK, kana, Hangul, full-width forms) count as two, and the engine further scales the budget per locale by how wide its script renders (Cyrillic, Greek and Tamil run wider, Arabic and Hindi narrower), so a max_chars_per_line means about the same width in every language.

Sizing a max_chars_per_line to a width: allow about 9.3 text units of width per character at text scale 1.0 (measured across every locale, 2026-10-01). For example, a doc-ui row footnote (scale 0.7) in a small-layout column (534 units of text width) fills well at 534 / (9.3 * 0.7), about 82. Aim a little wide rather than narrow: text scales down to fit its max width, so a slightly long line costs less than a visibly short one.

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_apvernums(lstr: LangStrSpec, acc: set[ApverNum]) → 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 apvernums 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.

Conversion is driven purely by each value’s type, so no per-param kind knowledge is needed – the typed stubs are what hold authors to passing time types only for duration params:

  • A datetime.timedelta is a fixed signed length (integer milliseconds; see time_sub_millis()).

  • A datetime.datetime is a moment. With no now it stays one – a LangStrSpecTimeTarget, measured against the current time at each display, so the text stays correct for as long as it is shown. Passing now instead freezes it into the fixed length moment - now (one shared now keeps a batch of static renders, such as a table column, from drifting against each other).

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.

values is the components package’s per-locale value map. The ladder and the adaptive decimals (one place below ten units, none above) mirror efro.util.data_size_str(), as does compact for width-constrained slots: the only rung it changes is bytes, which renders through the abbreviated bytes_compact entry (“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 KeyError if the components package is missing an entry – the decode path turns that into the usual fail-visible LANGSTR_ERROR sentinel 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”; maxparts caps the composed units largest-first, decimals applies fractional places to the last one), rendering each part through the components package’s per-locale duration/ unit templates and joining with the curated duration_separator.

millis follows the signed target - now convention (D12), in integer milliseconds – the sub wire type must stay int, and ms precision is what keeps decimals meaningful. direction ('past' / 'future') renders the magnitude of that sign and floors the other to 0s – countdown-safe by construction; clamp floors negatives for an undirected length; with neither, negatives render as magnitude plus a leading - (D-neg, same caveats).

The smallest unit shown rounds down, except for future (a countdown), which rounds up like timedelta_str(round_up=True): “1s” until the moment actually arrives rather than “0s” for the whole final second.

Raises KeyError if the components package is missing an entry – the decode path turns that into the usual fail-visible LANGSTR_ERROR sentinel 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.literal_template_kinds(template: str) → tuple[str, dict[str, str]][source]

Split a literal template’s spec’d tokens from its text.

A LangStrSpecValue template may mark a substitution with a rendering spec exactly as a brief does ('Ends in {t|duration}'), which is what lets a not-yet-translated literal display a formatted duration or size the way its eventual authored string will. Returns the template with each such token reduced to a plain {name} plus the {name: display-kind} map those specs declare (the same expressions a language blob carries for authored strings). Raises LangStrError for a malformed or unknown spec.

bacommon.langstr.package_structure(apvernum: ApverNum, 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.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() (plain str or StringSelector), read from the sibling components key. 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_kinds in serialize_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, matching parse_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.

kindexpr is the display-kind expression a blob’s k carrier (or a wrapper’s baked kinds map) holds – the bare kind or kind plus spec args ('bytes(compact=true)'); see display_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.

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.

param_kinds optionally 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.timedelta is already a signed length; a datetime.datetime is an absolute time, converted to signed target - now per D12 so the string’s dir handles past/future. now defaults to efro.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).

bacommon.langstr.time_target_offset_millis(target: LangStrSpecTimeTarget, now: datetime.datetime | None = None) → int[source]

The signed target - now length a time target measures.

The fixed-length snapshot of a live moment, for consumers with no slot for one. now defaults to efro.util.utc_now().