Developer Guide 3: Interface & Design
Views & Templates
Chapter 6: Building user interfaces with Jinja2, HTMX, and Bootstrap.
Views are what users actually see—the HTML pages that display your data. sparQ uses Jinja2 templates combined with HTMX for interactivity and Bootstrap for styling.
Template Basics
Templates live in views/templates/myapp/desktop/ within your app folder. They extend the base layout:
{% extends "core/desktop/base.html" %}
{% block content %}
<div class="container py-4">
<h1>My Tasks</h1>
<p>You have {{ tasks|length }} tasks.</p>
</div>
{% endblock %}
The {% extends "core/desktop/base.html" %} gives you the full sparQ layout with header, sidebar, and footer.
Displaying Data
Use {{ }} to output variables passed from your controller:
<!-- Simple variable -->
<h1>{{ task.title }}</h1>
<!-- With filter -->
<p>Created {{ task.created_at|timeago }}</p>
<!-- Conditional -->
{% if task.done %}
<span class="badge bg-success">Done</span>
{% else %}
<span class="badge bg-warning">Pending</span>
{% endif %}
<!-- Loop -->
{% for task in tasks %}
<div>{{ task.title }}</div>
{% endfor %}
Global Template Variables
These variables are always available in templates:
| Variable | Description |
|---|---|
current_user |
The logged-in user (or anonymous) |
g.current_module |
Current module's manifest |
g.lang |
Current language code ("en", "es", etc.) |
<!-- Show current user -->
{% if current_user.is_authenticated %}
Welcome, {{ current_user.name }}!
{% else %}
<a href="{{ url_for('core.auth.login') }}">Log in</a>
{% endif %}
Template Functions
Translation with _()
<h1>{{ _("Welcome") }}</h1>
<button>{{ _("Save") }}</button>
URLs with url_for()
<a href="{{ url_for('tasks.main.index') }}">All Tasks</a>
<a href="{{ url_for('tasks.main.show', id=task.id) }}">View</a>
Check if Module Exists
{% if module_enabled('billing') %}
<a href="{{ url_for('billing.invoices.index') }}">Invoices</a>
{% endif %}
HTMX for Interactivity
HTMX lets you build dynamic interfaces without writing JavaScript. Add attributes to HTML elements to make them send AJAX requests:
Click to Load
<button hx-get="/tasks/more"
hx-target="#task-list"
hx-swap="beforeend">
Load More
</button>
<div id="task-list">
<!-- Tasks appear here -->
</div>
Search with Debounce
<input type="search"
name="q"
placeholder="Search tasks..."
hx-get="/tasks/search"
hx-trigger="keyup changed delay:300ms"
hx-target="#results">
<div id="results"></div>
Inline Edit
<!-- Display mode -->
<span hx-get="/tasks/{{ task.id }}/edit-form"
hx-trigger="click"
hx-swap="outerHTML"
class="editable">
{{ task.title }}
</span>
<!-- Edit mode (returned by /edit-form) -->
<form hx-post="/tasks/{{ task.id }}"
hx-swap="outerHTML">
<input name="title" value="{{ task.title }}">
<button>Save</button>
</form>
Form Submission
<form hx-post="/tasks/create"
hx-target="#task-list"
hx-swap="afterbegin"
hx-on::after-request="this.reset()">
<input name="title" placeholder="New task..." required>
<button>Add</button>
</form>
Bootstrap Components
sparQ includes Bootstrap 5. Use its classes for consistent styling:
Cards
<div class="card">
<div class="card-header">Task Details</div>
<div class="card-body">
<h5 class="card-title">{{ task.title }}</h5>
<p class="card-text">{{ task.description }}</p>
</div>
</div>
Tables
<table class="table table-hover">
<thead>
<tr>
<th>Task</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for task in tasks %}
<tr>
<td>{{ task.title }}</td>
<td>
<span class="badge bg-{{ 'success' if task.done else 'secondary' }}">
{{ "Done" if task.done else "Pending" }}
</span>
</td>
<td>
<a href="{{ url_for('tasks.main.edit', id=task.id) }}"
class="btn btn-sm btn-outline-primary">Edit</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
Forms
<form method="POST">
<div class="mb-3">
<label class="form-label">Task Title</label>
<input type="text" name="title" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">Description</label>
<textarea name="description" class="form-control" rows="3"></textarea>
</div>
<button type="submit" class="btn btn-primary">Save Task</button>
</form>
Alpine.js for Local State
Use Alpine.js for small UI interactions like toggles and dropdowns:
<div x-data="{ open: false }">
<button @click="open = !open">
Toggle Details
</button>
<div x-show="open" x-transition>
These are the hidden details.
</div>
</div>
Template File Structure
Templates are organized in a specific folder structure:
The nested templates/myapp/desktop/ structure ensures template names don't collide across apps. When rendering, reference templates as myapp/desktop/index.html.
Device-Aware Templates
Desktop templates must be placed in a desktop/ subfolder. Mobile templates are optional and go in a mobile/ subfolder with the same file name.
Use render_device_template() in your controllers instead of Flask's render_template. It automatically serves the mobile version on mobile devices and falls back to desktop if no mobile template exists:
from system.device.template import render_device_template
# Always pass the desktop path — mobile is resolved automatically
return render_device_template('myapp/desktop/index.html', tasks=tasks)
To test mobile rendering during development, append ?device=mobile to any URL.
Partials for Reusability
Extract repeated HTML into partial templates:
<!-- views/templates/tasks/desktop/partials/task_row.html -->
<tr>
<td>{{ task.title }}</td>
<td>{{ task.status }}</td>
</tr>
<!-- Use it in your main template -->
{% for task in tasks %}
{% include "tasks/desktop/partials/task_row.html" %}
{% endfor %}
Key Takeaways
- Templates extend
core/desktop/base.htmlfor the full layout - Use
{{ }}for variables,{% %}for logic current_user,_(), andurl_for()are always available- HTMX adds interactivity without JavaScript
- Use Bootstrap classes for consistent styling
- Extract repeated HTML into partials
- Desktop templates must be in a
desktop/subfolder; mobile is optional
Styling
Chapter 7: Making your app look great with CSS and Bootstrap.
sparQ uses Bootstrap 5 for its UI framework, plus custom CSS variables for theming. Let's learn how to style your app consistently.
The Color System
sparQ uses CSS variables for consistent colors across the platform:
/* Primary colors */
var(--color-primary) /* Main purple #7c3aed */
var(--color-primary-dark) /* Hover state */
var(--color-primary-light) /* Light backgrounds */
/* Grays */
var(--color-gray-900) /* Dark text */
var(--color-gray-700) /* Secondary text */
var(--color-gray-500) /* Placeholders */
var(--color-gray-300) /* Borders */
var(--color-gray-100) /* Light backgrounds */
/* Semantic colors */
var(--color-success) /* Green #10b981 */
var(--color-danger) /* Red #ef4444 */
var(--color-warning) /* Amber #f59e0b */
var(--color-info) /* Blue #3b82f6 */
/* Your app's theme color */
var(--module-color) /* Set in manifest */
Your App's Theme Color
Set your app's color in the manifest:
# __manifest__.py
{
'name': 'Task Manager',
'color': '#3B82F6', # Blue theme
# ...
}
This color appears in your app icon and can be used in your CSS as var(--module-color):
.task-header {
border-left: 4px solid var(--module-color);
}
Adding Custom CSS
Create a CSS file in your app's views folder:
Start your CSS file by setting the module color:
/* myapp.css */
.myapp-app { --module-color: var(--color-primary); }
/* Your custom styles */
.task-card {
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.task-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
Using Bootstrap Classes
Bootstrap 5 is included globally. Use its utility classes for quick styling:
Spacing
<!-- Margin: m-{size}, mt, mb, ms, me, mx, my -->
<div class="mt-4 mb-2">Top margin 1.5rem, bottom 0.5rem</div>
<!-- Padding: p-{size}, pt, pb, ps, pe, px, py -->
<div class="p-3">Padding 1rem all sides</div>
Flexbox
<div class="d-flex justify-content-between align-items-center">
<span>Left</span>
<span>Right</span>
</div>
<div class="d-flex gap-2">
<button>One</button>
<button>Two</button>
</div>
Text
<p class="text-muted">Secondary text</p>
<p class="text-center">Centered text</p>
<p class="fw-bold">Bold text</p>
<p class="fs-5">Larger text</p>
Button Styles
sparQ uses Bootstrap buttons with some customizations:
<!-- Primary action -->
<button class="btn btn-primary">Save</button>
<!-- Secondary action -->
<button class="btn btn-outline-secondary">Cancel</button>
<!-- Danger action -->
<button class="btn btn-outline-danger">Delete</button>
<!-- Small button -->
<button class="btn btn-sm btn-outline-primary">Edit</button>
Badges
Use Bootstrap badges for status indicators:
<span class="badge bg-success">Complete</span>
<span class="badge bg-warning text-dark">Pending</span>
<span class="badge bg-danger">Overdue</span>
<span class="badge bg-secondary">Draft</span>
Cards
Cards are great for grouping related content:
<div class="card">
<div class="card-header d-flex justify-content-between">
<span>Tasks</span>
<button class="btn btn-sm btn-primary">Add</button>
</div>
<div class="card-body">
<!-- Content -->
</div>
</div>
Responsive Design
Bootstrap's breakpoints:
/* Small devices (phones) */
@media (max-width: 576px) { }
/* Medium devices (tablets) */
@media (max-width: 768px) { }
/* Large devices (desktops) */
@media (max-width: 992px) { }
/* Extra large devices */
@media (max-width: 1200px) { }
Use responsive classes to show/hide elements:
<!-- Hidden on mobile, visible on desktop -->
<div class="d-none d-md-block">Desktop only</div>
<!-- Visible on mobile, hidden on desktop -->
<div class="d-md-none">Mobile only</div>
Key Takeaways
- Set your app's theme color in the manifest
- Use CSS variables for consistent colors
- Bootstrap utility classes handle most styling needs
- Create custom CSS in
views/assets/css/ - Use responsive classes for mobile-friendly layouts
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