Skip to content
Haiku OSDeep Dive Published Updated 7 min readViews unavailable

Haiku BStringFormat: Localized Message Patterns and Integer Arguments

Format localized Haiku messages with BStringFormat and ICU patterns, checking initialization, locale choice, placeholder contracts, and output appending.

BStringFormat adapts ICU message formatting to Haiku’s Locale Kit. It is distinct from BNumberFormat, BDateFormat, and BTimeFormat: those classes format a typed value as a number or date/time, while BStringFormat inserts its integer argument into a complete message pattern. That makes it useful for count-sensitive text such as “1 file” versus “12 files,” where translators may need to change word order or choose plural forms.

The current public API is intentionally narrower than ICU’s entire formatting model. Its Format() method takes a BString output buffer and a single int64 argument. The constructor can take a BLanguage plus a pattern or just a pattern using the default BFormat locale path. Always check InitCheck() before formatting and check the returned status. The wrapper does not expose a generic variadic argument list for arbitrary C++ values.

Store whole messages, not English fragments

Message formatting is valuable because a translator can reorder the sentence around the number. Avoid constructing text as count + " files"; that assumes English word order and pluralization. Instead, keep one semantic message with a placeholder that the pattern language can select based on the integer value.

BStringFormat fileCountPattern(
    BString("{0, plural, one {# file} other {# files}}"));
if (fileCountPattern.InitCheck() != B_OK)
    return B_ERROR;

BString message;
status_t status = fileCountPattern.Format(message, fileCount);
if (status != B_OK)
    return status;

This uses ICU MessageFormat plural syntax and the pattern is illustrative; confirm the exact ICU version and supported pattern features shipped by the target Haiku build. The public Haiku wrapper supplies one integer argument, so {0} is the corresponding argument index. Do not copy a pattern that expects a second argument, a date object, or a floating-point value into this API and assume the wrapper can provide it.

When patterns come from translation catalogs, keep the placeholder contract discoverable. Add translator comments that identify the value’s meaning and type. Test the translated pattern with the actual argument range. A translation that omits the placeholder, references a nonexistent argument, or changes the plural selection can be a functional defect. Validate catalog updates during release review rather than waiting for users to report raw braces in the UI.

Choose the language intentionally

The constructor accepting BLanguage lets the caller select the language used to interpret the pattern. Use the language associated with the catalog or content being rendered, not necessarily the numeric formatting conventions chosen for currency or dates. Haiku’s locale model separates preferred languages from formatting preferences; a user’s language and regional number/date conventions can differ.

The one-argument constructor uses the default locale initialization path inherited from BFormat. That is convenient for ordinary UI output, but the choice should be explicit in a component that formats content for a language different from the current application locale. Avoid caching one formatter globally if the language can change while the application remains open; create a formatter per language or rebuild it when locale state changes.

Do not use a human-language pattern as a machine protocol or persistent file format. Message formatting is presentation. Store numeric values and semantic message keys, then format at the UI boundary. A locale change must not alter the bytes of a database key, config identifier, or network field.

Understand output-buffer behavior

The current implementation constructs a BStringByteSink over the supplied output BString and writes the formatted UTF-8 text into that sink. In practice, the output is appended to the buffer rather than replacing its previous contents. Start with a fresh BString for a single message, or deliberately clear the destination before formatting. Reusing a non-empty buffer without clearing it can produce a valid message prefixed by stale text.

BString output("Status: ");
status_t status = formatter.Format(output, itemCount);
if (status == B_OK) {
    // The formatted text follows the existing prefix.
    ShowStatus(output.String());
}

Treat the output as UTF-8 text and keep the BString alive while reading its String() pointer. Do not cache that pointer across later modifications to the buffer. If formatting fails, preserve or discard the previous output according to your application’s error policy; do not display a partially constructed message as though it succeeded.

Check initialization and errors at boundaries

BStringFormat may fail to initialize its ICU formatter if the pattern is invalid or memory allocation fails. The implementation exposes InitCheck() and returns a status from Format(). Validate both when a pattern is first loaded, then retain a clear fallback message if construction fails. Do not compile an invalid pattern on every repaint; prepare and validate it during localization loading or UI model setup.

Pattern syntax errors are not all equivalent to missing translations. A malformed ICU pattern should fail the formatter, while a missing catalog entry may fall back to source text through the Locale Kit’s catalog path. Test those cases separately. Keep the English fallback as a complete sentence and never display the raw untranslated template with braces if formatting failed.

Guard count conversion. Format() accepts int64; if a domain count is narrower, convert it safely, and if it is unsigned or can exceed INT64_MAX, define a saturation or error policy. Avoid converting a negative sentinel to a very large unsigned quantity and then formatting it as a legitimate count.

Do not confuse plural selection with localized number display

The # token in ICU plural patterns renders the numeric value in the plural message context. If a message needs multiple different numeric values, the current BStringFormat wrapper’s one-argument signature is not sufficient. Do not work around that by concatenating a second value into the pattern string; that bypasses proper formatting and can corrupt translation.

For a value that is a date, decimal measurement, currency amount, or percentage, use the corresponding typed Locale Kit formatter and compose the result only through a translator-safe message mechanism. Do not assume BStringFormat knows the unit or semantic type of a plain int64. If it represents bytes, frames, or elapsed time, the surrounding message should make that unit explicit and use the right specialized formatter where available.

Plural rules vary across languages. Do not reduce an internationalized message to a Boolean English singular/plural test. Use ICU plural categories in the source pattern and allow translators to provide locale-appropriate branches. Include zero explicitly only when the product’s wording differs from the locale’s normal plural rule, and test values at category boundaries in each supported language.

Make formatter lifecycle predictable

BStringFormat owns an internal ICU MessageFormat object. Keep the formatter alive for the period in which it is used, do not copy it through raw memory, and do not share one mutable output buffer between threads. The formatter’s public interface is const for Format(), but thread-safety of every wrapped ICU object should be verified for the target build before sharing one instance across concurrent callers. A per-thread or per-operation formatter is simpler when formatting volume is modest.

If a catalog or language setting changes, rebuild the formatter from the new source pattern and language. A currently visible string will not necessarily update itself; refresh the relevant view model and layout. Longer translations may require a wider or taller control, so couple localization testing with the layout constraints rather than validating only the output bytes.

Acceptance tests for message patterns

Test zero, one, two, negative input if the domain permits it, the largest supported count, and values that trigger different plural categories in target languages. Also test malformed syntax, a missing translation, a pattern with an invalid argument index, appending into an empty buffer, appending into a non-empty buffer, and locale changes.

For every user-visible pattern, verify that the output is grammatical and that punctuation, spacing, and translated order come from the complete message. If a BStringFormat pattern changes, run the formatter tests and the corresponding UI layout checks. Log the message key and language on formatting failure, but avoid logging private user data inserted into the pattern.

BStringFormat is a focused bridge between ICU message patterns and Haiku’s locale-aware application code. Use whole semantic messages, select the language deliberately, provide only the single integer argument the API accepts, check initialization and formatting status, and clear or intentionally append to the destination buffer.

Related:

Sources:

Comments