Developer Guide

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>
Image: Animation showing HTMX form adding a task without page reload

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:

myapp/ └── views/ ├── templates/ │ └── myapp/ # Same name as your app │ ├── desktop/ │ │ ├── index.html │ │ ├── show.html │ │ └── partials/ │ │ └── task_row.html │ └── mobile/ │ └── index.html └── assets/ └── css/ └── myapp.css

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.html for the full layout
  • Use {{ }} for variables, {% %} for logic
  • current_user, _(), and url_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);
}
Image: Apps with different theme colors in the sidebar

Adding Custom CSS

Create a CSS file in your app's views folder:

myapp/ └── views/ └── assets/ └── css/ └── myapp.css

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:

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:

  • 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.json first, 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:

  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

  • 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