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:
- No compilation required - Edit JSON files and see changes immediately
- Module-scoped - Each app manages its own translations independently
- Fast lookups - Translations are cached in memory at startup
- Minimal dependencies - Just JSON files, no external tools needed
- Easy to maintain - Simple key-value pairs anyone can edit
How Translations Work
Each module has its own lang/ folder with JSON files for each language:
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:
- User preference - Stored in the user's profile settings
- Browser language - From the
Accept-Languageheader - 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:
- Look for the key in the user's language file
- If not found, look in the English file
- 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:
- Your app's translations don't conflict with other apps
- You can use the same key ("Save", "Cancel") in different apps with different translations
- Base modules and apps each maintain their own
lang/folder - Translations are portable—just copy the
lang/folder when sharing your app
Performance
The translation system is designed for speed:
- Preloaded at startup - All JSON files load into memory when sparQ starts
- Cached lookups - No file I/O during request handling
- Negligible overhead - Translation lookups are simple dictionary access
Even with thousands of translations across multiple languages, the runtime impact is minimal.
Best Practices
- Start with English - Create
en.jsonfirst, then translate - Keep keys readable - Use the English text as the key: "Save" not "btn_save"
- Include context when needed - "Delete task" vs just "Delete" if the meaning differs
- Test with longer languages - German and French words are often longer than English
- Always use
_()- Even if you only support English now, wrap all user-facing text - Keep translations in sync - When adding a new key to
en.json, add it to other language files too
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:
- Customize button labels for your brand voice
- Make messages more specific to your app's context
- A/B test different copy without code changes
- Let non-developers customize text through JSON files
Adding a New Language
To add support for a new language:
- Create a new JSON file (e.g.,
lang/pt.jsonfor Portuguese) - Copy the structure from
en.json - Translate each value
- 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
- Translation files are simple JSON in the
lang/folder - Use
_()in templates and Python for all user-facing text - Pass variables as keyword arguments:
_("Hello {name}", name=user.name) - No compilation required—edit JSON and refresh
- Translations are cached in memory for fast lookups
- Each module manages its own translations independently