How Music Assistant Translation and Internationalization Works: Build-Time Compilation and Runtime Resolution

Music Assistant implements a hierarchical translation system that compiles distributed strings.json files into a flat English source catalog at build time, then uses a lazy-loading TranslationController at runtime to resolve localized strings with fallback chains and parameter interpolation.

The Music Assistant translation and internationalization system manages all user-facing text across the open-source server codebase. Unlike monolithic translation files, this architecture distributes source strings alongside their respective providers and controllers, then aggregates them into optimized runtime bundles. Understanding this design reveals how the server supports multiple languages while maintaining type safety and performance through deterministic key resolution.

Build-Time Translation Catalog Generation

The foundation of the system lies in authoring strings.json files scattered throughout the codebase. These files reside at the package root (music_assistant/strings.json), within each provider (music_assistant/providers/*/strings.json), and within each controller (music_assistant/controllers/*/strings.json).

Source File Discovery and Aggregation

The script scripts/build_translations.py orchestrates the build process. The _collect_source_files() function walks the providers/ and controllers/ directories (lines 51-70), generating (prefix, path) pairs that map file locations to key namespaces like provider.spotify. or controller.metadata..

Flattening Nested Structures

Once discovered, the _flatten_into() method recursively processes nested dictionaries into dotted keys (lines 84-92). For example, a nested structure becomes provider.spotify.config_entries.api_key.label. This flattened catalog is written deterministically (sorted alphabetically) to music_assistant/translations/en.json (lines 94-100), which serves as the source language (SOURCE_LANGUAGE = "en"). This file is committed to the repository and uploaded to Lokalise for community translation.

Runtime Translation Management

At server startup, the TranslationController (a subclass of CoreController located in controllers/translations/__init__.py, lines 40-45) initializes the runtime translation environment.

The TranslationController Architecture

The controller immediately loads the English source file into self._source during initialization (lines 73-76). This eager loading ensures that the fallback language is always available in memory, even if localized bundles are not yet parsed.

Lazy Locale Loading Strategy

Localized translations follow a lazy loading pattern. The _discover_locale_files() method scans the translations/ directory once at startup to catalog available <lang>.json files (such as nl.json or de.json), storing them in self._locale_files (lines 93-100). However, actual parsing occurs only when first requested via ensure_locale_loaded() (lines 12-30), which uses per-locale asyncio.Lock to prevent duplicate loads and race conditions.

Hierarchical Key Resolution Algorithm

The get_translation() method implements a sophisticated fallback chain (lines 82-100):

  1. Owner Prefix Construction: When an owner parameter (e.g., provider.spotify) is provided, _owner_prefix() constructs the appropriate key prefix (lines 13-18).

  2. Candidate Key Generation: The _candidate_keys() function produces an ordered list of keys to attempt, handling fully-qualified keys to common rewrites (provider.* → common.*), relative keys with owner-specific variations, and .name suffix stripping (lines 20-30).

  3. Locale Normalization: The _locale_candidates() method normalizes hyphens and underscores while adding base language fallbacks, converting de_DE into ["de_DE", "de"] (lines 62-66).

  4. Lookup and Formatting: The _lookup() method checks loaded locale bundles first, then the English source (lines 71-79). Upon finding a match, _format() safely interpolates positional placeholders like {0} and {1} using supplied parameters, leaving the template unchanged if formatting fails (lines 69-77).

Integration Points and Usage

The translation system integrates deeply into Music Assistant's data models through the TRANSLATION_RESOLVER global context variable from music_assistant_models.translations.

Configuration Entries and Provider Manifests

When serializing configuration data, ConfigEntry.to_dict() uses the active resolver to inject translated label and category_label values according to the test implementations in tests/core/test_translations.py (lines 47-64). Similarly, ProviderManifest.to_dict() automatically resolves translation keys like provider.<domain>.manifest.name and provider.<domain>.manifest.description into localized strings (lines 15-30).

Error Messages and Background Tasks

ErrorResultMessage stores raw translation_key values and arguments. When serialized via to_dict(), it returns localized details strings while stripping the translation machinery from the output (lines 83-104). Background task names utilize translation keys such as common.background_task.sync_tracks, with provider names inserted via parameter interpolation (lines 56-66).

Practical Implementation Examples


# Initialize the controller (normally handled by server startup)

from music_assistant.controllers.translations import TranslationController
ctrl = TranslationController(mass)  # mass is the running Music Assistant instance

await ctrl.setup(None)  # Discovers locales and loads en.json

# Resolve a localized string with parameters

title = ctrl.get_translation(
    "provider.spotify.media.liked",  # Fully-qualified key

    locale="nl",                     # Dutch locale

    params=["My Playlist"]           # Fills {0} placeholder

)

# Returns: "Nummers die je leuk vindt My Playlist"

# Pre-load a locale for a connecting client

await ctrl.ensure_locale_loaded("de")  # Loads de.json if present

# Use the resolver in a request context

from music_assistant_models.translations import TRANSLATION_RESOLVER
from functools import partial

with partial(ctrl.get_translation, locale="nl") as resolver:
    token = TRANSLATION_RESOLVER.set(resolver)
    # ConfigEntry.to_dict() calls within this block return Dutch labels

    TRANSLATION_RESOLVER.reset(token)

# Reverse lookup for search fallback

canonical = await ctrl.reverse_lookup_media_names("Klassiek")

# Returns: {"Classical"}

Summary

  • Distributed authoring: Source strings live in strings.json files co-located with providers and controllers, then compile into music_assistant/translations/en.json via scripts/build_translations.py.
  • Lazy runtime loading: The TranslationController eagerly loads English but defers other locales until ensure_locale_loaded() is called, using asyncio.Lock for thread safety.
  • Hierarchical resolution: The get_translation() method implements multi-layer fallback through owner prefixes, common rewrites, and base language normalization.
  • Context-aware integration: The TRANSLATION_RESOLVER global enables transparent localization of configuration entries, provider manifests, and error messages throughout the request lifecycle.

Frequently Asked Questions

How does Music Assistant handle missing translations for specific locales?

When a key is missing in the requested locale, the system falls back through a chain of candidates: first the specific locale (e.g., de_DE), then the base language (de), then the English source (en). The _lookup() method in controllers/translations/__init__.py checks each candidate in sequence until finding a match, ensuring that users always see English text rather than raw keys when translations are incomplete.

What is the purpose of the reverse_lookup_media_names method?

The reverse_lookup_media_names() function enables search functionality across localized content. When a user searches using a translated term (e.g., "Klassiek" for Classical), this method maps the localized string back to its canonical English name. This allows the media library search to function correctly regardless of the user's interface language, as internal media metadata remains in English.

How are translation keys structured in Music Assistant?

Keys follow a dotted namespace convention where the prefix indicates ownership. Providers use provider.<domain>.<key>, controllers use controller.<name>.<key>, and common strings use common.<key>. During build time, these hierarchical structures flatten into dot-notation keys like provider.spotify.config_entries.api_key.label, while the _candidate_keys() runtime function handles lookups for both fully-qualified and relative key formats.

Can providers define their own translation strings without modifying core files?

Yes. Providers maintain their own strings.json files within their respective directories (music_assistant/providers/<provider_name>/strings.json). The build script automatically discovers these files via _collect_source_files() and prefixes their keys appropriately (e.g., provider.spotify.*). This modular approach allows third-party providers to ship complete localization support without touching the core Music Assistant codebase.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →