Internationalization

Chapter 8: Making your app available in multiple languages.

sparQ apps can support multiple languages with simple JSON translation files. Users see the app in their preferred language automatically. Let's make your app multilingual.

Why JSON-Based Translations?

sparQ uses a lightweight, custom translation system instead of heavier solutions. The benefits:

How Translations Work

Each module has its own lang/ folder with JSON files for each language:

myapp/ └── lang/ ├── en.json # English (default) ├── es.json # Spanish ├── fr.json # French └── de.json # German

When sparQ starts, it preloads all translations into memory. At runtime, it looks up the current user's language preference and returns the appropriate translation instantly.

Creating Translation Files

Translation files are simple key-value JSON. The key is what you write in code, the value is what users see:

// lang/en.json
{
    "Tasks": "Tasks",
    "Add Task": "Add Task",
    "Edit": "Edit",
    "Delete": "Delete",
    "Save": "Save",
    "Cancel": "Cancel",
    "Task created successfully": "Task created successfully",
    "Are you sure?": "Are you sure you want to delete this?"
}
// lang/es.json
{
    "Tasks": "Tareas",
    "Add Task": "Agregar Tarea",
    "Edit": "Editar",
    "Delete": "Eliminar",
    "Save": "Guardar",
    "Cancel": "Cancelar",
    "Task created successfully": "Tarea creada exitosamente",
    "Are you sure?": "¿Estás seguro de que quieres eliminar esto?"
}

Use English keys. Even if your native language isn't English, use English keys. It makes your code readable and serves as documentation.

en.json is optional. If there's no English translation file, the key itself is used as the default text. Anything in en.json simply overwrites the default—making it easy to customize any label without touching template code.

Using Translations in Templates

Use the _() function in your Jinja2 templates:

<h1>{{ _("Tasks") }}</h1>

<button class="btn btn-primary">
    {{ _("Add Task") }}
</button>

<div class="btn-group">
    <button class="btn btn-sm">{{ _("Edit") }}</button>
    <button class="btn btn-sm btn-danger">{{ _("Delete") }}</button>
</div>

When a Spanish-speaking user views this page, they'll see "Tareas", "Agregar Tarea", etc.

Using Translations in Python

Import the translation function from sparQ's i18n module:

from system.i18n import _

@bp.route('/new', methods=['POST'])
def create():
    task = Task.create(title=request.form.get('title'))
    flash(_("Task created successfully"), 'success')
    return redirect(url_for('tasks.main.index'))

Flash messages, error messages, and any user-facing strings should be translated.

Translations with Variables

Sometimes you need to include dynamic values in translations. Use Python's format string syntax:

// lang/en.json
{
    "Hello {name}": "Hello {name}",
    "{count} tasks remaining": "{count} tasks remaining"
}
// lang/es.json
{
    "Hello {name}": "Hola {name}",
    "{count} tasks remaining": "{count} tareas pendientes"
}

Pass the variables as keyword arguments:

<!-- In templates -->
<h2>{{ _("Hello {name}", name=user.first_name) }}</h2>
<p>{{ _("{count} tasks remaining", count=pending_count) }}</p>
# In Python
from system.i18n import _

message = _("Hello {name}", name=user.first_name)
flash(_("{count} tasks remaining", count=5), 'info')

The translation function handles the formatting automatically when you pass keyword arguments.

How Language Selection Works

sparQ determines the user's language in this order:

  1. User preference - Stored in the user's profile settings
  2. Browser language - From the Accept-Language header
  3. Default language - Falls back to English

Users can change their language preference in their profile settings, and the change takes effect immediately—no restart required.

Language Fallbacks

If a translation is missing, sparQ falls back gracefully:

  1. Look for the key in the user's language file
  2. If not found, look in the English file
  3. If still not found, display the key itself

This means your app won't break if translations are incomplete—users just see English (or the key) for missing strings. This is especially useful during development when you're adding new strings.

No compilation step. Unlike traditional gettext-based systems, you don't need to compile your translations. Just edit the JSON file and refresh the page.

Module-Scoped Translations

Each module manages its own translations independently. This means:

Performance

The translation system is designed for speed:

Even with thousands of translations across multiple languages, the runtime impact is minimal.

Best Practices

Customizing Default Labels

One of the most powerful features of this system: you can customize any label without touching template code. Since the key is used as the fallback, you only need to add entries to en.json for strings you want to change:

// lang/en.json - Only override what you need
{
    "Save": "Save Changes",
    "Delete": "Remove",
    "Are you sure?": "This action cannot be undone. Continue?"
}

Everything else uses the key as-is. This makes it incredibly easy to:

Adding a New Language

To add support for a new language:

  1. Create a new JSON file (e.g., lang/pt.json for Portuguese)
  2. Copy the structure from en.json
  3. Translate each value
  4. Restart sparQ to load the new translations
// lang/pt.json
{
    "Tasks": "Tarefas",
    "Add Task": "Adicionar Tarefa",
    "Edit": "Editar",
    "Delete": "Excluir",
    "Save": "Salvar",
    "Cancel": "Cancelar"
}

Key Takeaways