Skip to content

Localizing Responses

Response localization allows your assistant to reply in the user's language. The key idea: store a translation key with format arguments, so the translated template is resolved first, then the arguments are injected into it.

# Without localization — hardcoded language:
Response(text=f"Hello, {name}!")
# gives "Hello, Mark!"

# With localization — deferred:
Response(text=LocalizableString("greeting", "fr", name=str(name)))
# Localizer resolves "greeting" for "fr" → "Bonjour, {name}!"
# Then formats → "Bonjour, Mark!"

This matters because argument positions and surrounding text differ between languages — you can't just translate a pre-formatted string.

LocalizableString

LocalizableString stores a key, a language code, and format arguments. At response time, Localizer.localize() looks up the key in localizable.strings for the given language, then calls .format(**arguments) on the resolved template.

from stark.general.localisation import LocalizableString

LocalizableString("greeting", "ru", name="Mark")
# .string = "greeting"         — the key
# .language_code = "ru"        — which translation to use
# .arguments = {"name": "Mark"} — injected after translation

If the key is not found, localize() emits a RuntimeWarning and falls back to the raw key string.

Using in Commands

Response.text and Response.voice accept both plain str and LocalizableString:

@manager.new({
    "base": "hello $name:Word",
    "ru": "привет $name:Word",
})
async def greet(name: Word) -> Response:
    lang = name.value.language_code  # from the parsed input
    return Response(
        text=LocalizableString("greeting_response", lang, name=str(name)),
        voice=LocalizableString("greeting_response", lang, name=str(name)),
    )

Resolving at Response Time

The core framework stores LocalizableString as-is in the Response object — it does not resolve it automatically. Resolution happens at the delegate level (e.g., VoiceAssistant provided by stark), where the Localizer is available:

# In your custom delegate / response handler:
if isinstance(response.text, LocalizableString):
    text = localizer.localize(response.text)
else:
    text = response.text

This keeps the core framework decoupled from any specific output target — the same Response can be rendered differently by a voice assistant (TTS), a chat UI, or a logging system.

Fallback Behavior

If the key is not found in localizable.strings for the requested language, localize() falls back to:

  1. The base language strings
  2. The raw key string itself (with a RuntimeWarning)

String Bundles

Response strings use the same .strings bundle format and directory structure as pattern localization. The localizable.strings files are the output counterpart to recognizable.strings:

strings/
  en/
    localizable.strings    ← response strings
    recognizable.strings   ← pattern strings
  ru/
    localizable.strings
    recognizable.strings

See Localizing Parsing for the full bundle format reference.

Formatting Complex Values with PyICU

For formatting locale-sensitive values like numbers, dates, units, and currencies in responses, PyICU is a great companion library. It wraps the ICU C++ library (the same engine behind iOS/Swift's Foundation formatting) and provides ready-made locale-aware formatting for:

  • Numbers — decimal, percent, currency, and spelled-out (e.g., "five", "пять")
  • Dates/Times — locale-specific patterns, relative dates ("yesterday", "in 2 days")
  • Units"5 kilometers", "3 lbs", "2 hours" with localized names
  • Messages — pluralization and gender rules ("{num, plural, one {# item} other {# items}}")

PyICU is not a dependency of S.T.A.R.K — use it alongside when you need locale-aware value formatting in your responses. See Command Response for more examples of response building with formatted values.