Guide to implementing multi-language support and managing site-wide text content in MintyFlaskThemes using YAML-based configuration.

Internationalisation (i18n)

Introduction

MintyFlaskThemes uses a YAML-based internationalisation system that separates site-wide text content from template logic. This approach provides a clean, maintainable way to manage translatable strings, site configuration, and multi-language support without requiring complex translation frameworks.

Why YAML-Based i18n?

Advantages:

  • Simple to maintain: Plain text YAML files that non-developers can edit
  • Version control friendly: Easy to track changes and manage translations
  • No compilation step: Changes take effect immediately
  • Framework agnostic: Works with any Flask setup
  • Hierarchical organisation: Natural structure for grouped content

When to use:

  • Site-wide labels and text
  • Navigation menu items
  • Form labels and button text
  • Error messages and notifications
  • SEO metadata (titles, descriptions)
  • Multi-language site content

The i18n System Architecture

How It Works

The i18n system follows a simple three-step process:

  1. YAML Configuration: Text content stored in YAML files
  2. Flask Integration: YAML loaded and passed to templates
  3. Template Access: Jinja2 templates access translations with fallbacks
# Flask loads YAML
i18n_data = load_yaml('config/i18n/en.yaml')

# Passed to template context
render_template('page.html', i18n_yaml=i18n_data)
{# Template accesses with fallback #}
{{ i18n_yaml.site_name or "Default Site Name" }}

File Structure

Organise i18n files by language and purpose:

project/
├── config/
│   └── i18n/
│       ├── en.yaml          # English (default)
│       ├── nl.yaml          # Dutch
│       ├── fr.yaml          # French
│       └── common/
│           ├── navigation.yaml
│           ├── forms.yaml
│           └── errors.yaml

YAML Configuration Files

Basic Structure

Create a primary language file with all translatable strings:

# config/i18n/en.yaml

# Site Identity
site_name: "Minty Fresh Flask"
tagline: "A minty fresh publishing platform"
description: "Discover fresh perspectives and insightful articles"
keywords: "blog, flask, python, web development, articles"

# Site Configuration
logo_url: "/static/images/logo.png"
language: "en"
locale: "en_GB"

# Navigation
nav:
  home: "Home"
  about: "About"
  blog: "Blog"
  contact: "Contact"

# Common Actions
actions:
  read_more: "Read more"
  learn_more: "Learn more"
  get_started: "Get started"
  subscribe: "Subscribe"
  search: "Search"

# Blog-Specific
blog:
  recent_posts: "Recent Posts"
  featured_post: "Featured Post"
  all_posts: "All Posts"
  categories: "Categories"
  tags: "Tags"
  archives: "Archives"
  no_posts: "No posts available"
  read_article: "Read article"

# Forms
forms:
  name_label: "Name"
  name_placeholder: "Enter your name"
  email_label: "Email"
  email_placeholder: "Enter your email"
  message_label: "Message"
  message_placeholder: "Your message"
  submit: "Submit"
  required: "required"

# Messages
messages:
  success: "Success! Your message has been sent."
  error: "An error occurred. Please try again."
  loading: "Loading..."

# Footer
footer:
  copyright:  2024 Minty Fresh Flask. All rights reserved."
  privacy: "Privacy Policy"
  terms: "Terms of Service"

Hierarchical Organisation

Group related strings under nested keys:

blog:
  titles:
    home: "Blog Home"
    category: "Category: {category}"
    tag: "Posts tagged with {tag}"
    archive: "Archive: {year}"

  empty_states:
    no_posts:
      title: "No posts available"
      message: "Check back soon for new content"
    no_category:
      title: "No articles in this category"
      message: "This category doesn't contain any published articles yet"

  metadata:
    reading_time: "{minutes} min read"
    published_on: "Published on {date}"
    by_author: "by {author}"

Multiple Language Files

Create corresponding files for each language:

# config/i18n/nl.yaml (Dutch)

site_name: "Minty Fresh Flask"
tagline: "Een minty fresh publicatieplatform"
description: "Ontdek frisse perspectieven en inzichtelijke artikelen"

nav:
  home: "Home"
  about: "Over ons"
  blog: "Blog"
  contact: "Contact"

actions:
  read_more: "Lees meer"
  learn_more: "Meer informatie"
  get_started: "Aan de slag"

blog:
  recent_posts: "Recente berichten"
  featured_post: "Uitgelicht bericht"
  all_posts: "Alle berichten"

Template Integration

Basic Usage Pattern

Access i18n strings in templates with fallback values:

{# Simple value access #}
<h1>{{ i18n_yaml.site_name or "Default Site Name" }}</h1>

{# Nested key access #}
<a href="{{ url_for('blog_bp.blog_index') }}">
  {{ i18n_yaml.blog.all_posts or "All Posts" }}
</a>

{# Multiple fallback levels #}
<p>{{ i18n_yaml.description or i18n_yaml.tagline or "Welcome" }}</p>

Navigation Menus

Build dynamic navigation from i18n configuration:

{# templates/components/navigation.html #}

<nav aria-label="Main navigation">
  <ul>
    <li>
      <a href="{{ url_for('home_bp.home') }}">
        {{ i18n_yaml.nav.home or "Home" }}
      </a>
    </li>
    <li>
      <a href="{{ url_for('blog_bp.blog_index') }}">
        {{ i18n_yaml.nav.blog or "Blog" }}
      </a>
    </li>
    <li>
      <a href="{{ url_for('main_bp.about') }}">
        {{ i18n_yaml.nav.about or "About" }}
      </a>
    </li>
    <li>
      <a href="{{ url_for('main_bp.contact') }}">
        {{ i18n_yaml.nav.contact or "Contact" }}
      </a>
    </li>
  </ul>
</nav>

Dynamic Content Substitution

Use Jinja2 filters for string interpolation:

{# Reading time with variable substitution #}
{% set reading_time_text = i18n_yaml.blog.metadata.reading_time or "{minutes} min read" %}
<span>{{ reading_time_text.replace('{minutes}', minutes|string) }}</span>

{# Or use format filter #}
<span>{{ (i18n_yaml.blog.metadata.reading_time or "{minutes} min read").format(minutes=minutes) }}</span>

{# Category title with substitution #}
<h1>{{ (i18n_yaml.blog.titles.category or "Category: {category}").format(category=category) }}</h1>

SEO Metadata Integration

Use i18n for comprehensive SEO support:

{# templates/pages/homepage.html #}

{% block page_meta %}
  <title>{{ i18n_yaml.site_name or "Minty Fresh Flask" }} - {{ i18n_yaml.tagline or "A Fresh Publishing Platform" }}</title>

  <meta name="description" content="{{ i18n_yaml.description or 'Discover fresh perspectives and insightful articles' }}">

  <meta name="keywords" content="{{ i18n_yaml.keywords or 'blog, flask, python' }}">

  {# Language and locale #}
  <html lang="{{ i18n_yaml.language or 'en' }}">
  <meta property="og:locale" content="{{ i18n_yaml.locale or 'en_GB' }}">

  {# Open Graph tags #}
  <meta property="og:site_name" content="{{ i18n_yaml.site_name }}">
  <meta property="og:title" content="{{ i18n_yaml.site_name }}">
  <meta property="og:description" content="{{ i18n_yaml.description }}">
{% endblock %}

Form Labels and Validation

Build accessible forms with i18n labels:

{# templates/components/contact-form.html #}

<form>
  <div class="form-group">
    <label for="name">
      {{ i18n_yaml.forms.name_label or "Name" }}
      {% if required %}
        <span aria-label="{{ i18n_yaml.forms.required or 'required' }}">*</span>
      {% endif %}
    </label>
    <input
      type="text"
      id="name"
      placeholder="{{ i18n_yaml.forms.name_placeholder or 'Enter your name' }}"
      required>
  </div>

  <div class="form-group">
    <label for="email">
      {{ i18n_yaml.forms.email_label or "Email" }}*
    </label>
    <input
      type="email"
      id="email"
      placeholder="{{ i18n_yaml.forms.email_placeholder or 'Enter your email' }}"
      required>
  </div>

  <button type="submit">
    {{ i18n_yaml.forms.submit or "Submit" }}
  </button>
</form>

Flask Integration

Loading YAML Files

Create a helper function to load i18n configuration:

# app/utils/i18n.py

import yaml
from pathlib import Path
from flask import current_app

def load_i18n(language='en'):
    """
    Load internationalisation strings from YAML file.

    Args:
        language: Language code (e.g., 'en', 'nl', 'fr')

    Returns:
        Dictionary of i18n strings
    """
    i18n_path = Path(current_app.config['I18N_DIR']) / f'{language}.yaml'

    try:
        with open(i18n_path, 'r', encoding='utf-8') as file:
            return yaml.safe_load(file) or {}
    except FileNotFoundError:
        current_app.logger.warning(f"i18n file not found: {i18n_path}")
        return {}
    except yaml.YAMLError as e:
        current_app.logger.error(f"Error parsing i18n file: {e}")
        return {}

Application Configuration

Configure i18n in your Flask application:

# config.py

class Config:
    # i18n Configuration
    I18N_DIR = 'config/i18n'
    DEFAULT_LANGUAGE = 'en'
    SUPPORTED_LANGUAGES = ['en', 'nl', 'fr']

Context Processor

Make i18n data available to all templates:

# app/__init__.py

from app.utils.i18n import load_i18n

def create_app():
    app = Flask(__name__)

    @app.context_processor
    def inject_i18n():
        """Inject i18n data into all templates."""
        language = get_current_language()  # Implement language detection
        return {
            'i18n_yaml': load_i18n(language),
            'current_language': language,
            'supported_languages': app.config['SUPPORTED_LANGUAGES']
        }

    return app

Language Detection

Implement language detection based on user preference:

# app/utils/i18n.py

from flask import request, session

def get_current_language():
    """
    Determine current language from session, cookie, or browser.

    Returns:
        Language code string
    """
    # 1. Check session
    if 'language' in session:
        return session['language']

    # 2. Check cookie
    language = request.cookies.get('language')
    if language:
        return language

    # 3. Check browser accept-language header
    return request.accept_languages.best_match(
        current_app.config['SUPPORTED_LANGUAGES'],
        default=current_app.config['DEFAULT_LANGUAGE']
    )

def set_language(language):
    """Set user's language preference."""
    session['language'] = language

Language Switcher Route

Create an endpoint for users to switch languages:

# app/routes/main.py

@main_bp.route('/set-language/<language>')
def set_language(language):
    """Handle language switching."""
    from app.utils.i18n import set_language as set_lang

    if language in current_app.config['SUPPORTED_LANGUAGES']:
        set_lang(language)

        # Create response and set cookie
        response = make_response(redirect(request.referrer or url_for('home_bp.home')))
        response.set_cookie('language', language, max_age=60*60*24*365)  # 1 year

        return response

    flash('Language not supported', 'error')
    return redirect(request.referrer or url_for('home_bp.home'))

Multi-Language Implementation

Language Switcher Component

Build a language switcher for users:

{# templates/components/language-switcher.html #}

<div class="language-switcher" role="navigation" aria-label="Language selection">
  <button class="language-button"
          aria-haspopup="true"
          aria-expanded="false">
    {{ current_language|upper }}
    <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
    </svg>
  </button>

  <ul class="language-menu" role="menu">
    {% for lang in supported_languages %}
      <li role="none">
        <a href="{{ url_for('main_bp.set_language', language=lang) }}"
           role="menuitem"
           class="language-option {% if lang == current_language %}active{% endif %}"
           hreflang="{{ lang }}">
          {{ lang|upper }}
        </a>
      </li>
    {% endfor %}
  </ul>
</div>

Language-Specific URLs

Implement language-specific URL patterns:

# URL structure: /en/about, /nl/about, /fr/about

@main_bp.route('/<language>/about')
def about(language):
    """About page with language parameter."""
    from app.utils.i18n import set_language

    if language not in current_app.config['SUPPORTED_LANGUAGES']:
        abort(404)

    set_language(language)
    return render_template('about.html')

Alternate Language Links (SEO)

Add alternate language links for SEO:

{# SEO: Indicate available translations #}
{% for lang in supported_languages %}
  <link rel="alternate"
        hreflang="{{ lang }}"
        href="{{ url_for(request.endpoint, language=lang, _external=True) }}">
{% endfor %}

{# Default language #}
<link rel="alternate"
      hreflang="x-default"
      href="{{ url_for(request.endpoint, language=config.DEFAULT_LANGUAGE, _external=True) }}">

Best Practices

Fallback Strategy

Always provide sensible fallbacks:

{# ✓ Good: Multiple fallback levels #}
{{ i18n_yaml.blog.titles.category or i18n_yaml.blog.category or "Category" }}

{# ✓ Good: Context-appropriate defaults #}
{{ i18n_yaml.actions.read_more or "Read more" }}

{# ✗ Bad: No fallback #}
{{ i18n_yaml.blog.some_key }}  {# May display empty string #}

{# ✗ Bad: Generic fallback #}
{{ i18n_yaml.blog.read_article or "Click here" }}  {# Too vague #}

Key Naming Conventions

Use clear, hierarchical naming:

# ✓ Good: Clear hierarchy and purpose
blog:
  actions:
    read_more: "Read more"
    view_all: "View all posts"
  titles:
    home: "Blog Home"
    category: "Category: {category}"

# ✗ Bad: Flat structure, unclear purpose
blog_read_more: "Read more"
blog_title_1: "Blog Home"
thing: "Category: {category}"

Maintainability Guidelines

Keep it organised:

# Group by feature/section
navigation:
  main: {}
  footer: {}

blog:
  titles: {}
  actions: {}
  messages: {}

Use comments for context:

# Blog Section
# These strings appear in blog templates and components
blog:
  # Post listing page
  recent_posts: "Recent Posts" # Homepage section heading
  all_posts: "All Posts" # Navigation link and page title

  # Empty states
  no_posts: "No posts available" # Shown when no posts exist

Consistent formatting:

# ✓ Good: Consistent sentence case
actions:
  read_more: "Read more"
  learn_more: "Learn more"
  get_started: "Get started"

# ✗ Bad: Inconsistent capitalisation
actions:
  read_more: "Read More"
  learn_more: "Learn more"
  get_started: "GET STARTED"

String Interpolation Guidelines

Design strings for easy substitution:

# ✓ Good: Clear placeholder names
blog:
  published_info: "Published on {date} by {author}"
  reading_time: "{minutes} min read"
  comment_count: "{count} comments"

# ✗ Bad: Unclear placeholders
blog:
  info: "Posted {1} by {2}"  # What is 1 and 2?
  time: "{x} min"            # What is x?

Template usage:

{# Clear and readable #}
{{ i18n_yaml.blog.published_info.format(date=post.date, author=post.author) }}
{{ i18n_yaml.blog.reading_time.format(minutes=5) }}

Common Patterns

Pattern: Pluralisation Handling

Handle singular and plural forms:

# Option 1: Separate keys
comments:
  single: "1 comment"
  plural: "{count} comments"

# Option 2: Full sentence variants
blog:
  post_count_single: "There is 1 post in this category"
  post_count_plural: "There are {count} posts in this category"

Template implementation:

{% if count == 1 %}
  {{ i18n_yaml.comments.single or "1 comment" }}
{% else %}
  {{ (i18n_yaml.comments.plural or "{count} comments").format(count=count) }}
{% endif %}

Pattern: Date and Time Formatting

Localise date formats:

formats:
  date_short: "%d/%m/%Y" # UK: 24/11/2024
  date_long: "%d %B %Y" # UK: 24 November 2024
  time_short: "%H:%M" # 24-hour format
  datetime: "%d/%m/%Y %H:%M"
{# Use format from i18n #}
{{ post.date.strftime(i18n_yaml.formats.date_long or "%d %B %Y") }}

Pattern: Conditional Content

Show/hide content based on language:

{# Language-specific content #}
{% if current_language == 'nl' %}
  <p>{{ i18n_yaml.special_notice_nl }}</p>
{% elif current_language == 'fr' %}
  <p>{{ i18n_yaml.special_notice_fr }}</p>
{% endif %}

{# Or use i18n key that exists only in some languages #}
{% if i18n_yaml.get('regional_promotion') %}
  <div class="promotion">{{ i18n_yaml.regional_promotion }}</div>
{% endif %}

Pattern: Rich Text Content

Store rich text in i18n files:

about:
  hero_title: "About Us"
  hero_subtitle: "Building better web experiences"
  intro: |
    We are a team of passionate developers creating 
    modern web applications with Flask and Python.

    Our mission is to make web development accessible 
    and enjoyable for everyone.

  features:
    - title: "Open Source"
      description: "All our projects are open source and community-driven"
    - title: "Modern Stack"
      description: "Using the latest Python and web technologies"

Template usage with preserved formatting:

<div class="about-content">
  {{ i18n_yaml.about.intro|safe }}

  <ul>
    {% for feature in i18n_yaml.about.features %}
      <li>
        <strong>{{ feature.title }}</strong>
        <p>{{ feature.description }}</p>
      </li>
    {% endfor %}
  </ul>
</div>

Troubleshooting

Problem: Keys Not Found

Symptoms:

  • Empty strings in templates
  • Missing translations

Solution:

{# Debug: Check if key exists #}
{% if i18n_yaml.get('blog', {}).get('title') %}
  {{ i18n_yaml.blog.title }}
{% else %}
  <span style="color: red;">MISSING: blog.title</span>
{% endif %}

{# Production: Always use fallbacks #}
{{ i18n_yaml.blog.title or "Default Title" }}

Problem: YAML Parsing Errors

Symptoms:

  • Empty i18n_yaml dictionary
  • Site using all fallback values

Diagnosis: Check Flask logs for YAML parsing errors.

Common YAML errors:

# ✗ Bad: Inconsistent indentation
nav:
  home: "Home"
   about: "About"  # Extra space

# ✓ Good: Consistent indentation (2 spaces)
nav:
  home: "Home"
  about: "About"

# ✗ Bad: Unescaped special characters
message: "It's a great day!"  # Apostrophe breaks parsing

# ✓ Good: Quoted strings with special characters
message: "It's a great day!"  # or 'It''s a great day!'

Problem: Language Not Switching

Symptoms:

  • Language switcher doesn't work
  • Site stays in default language

Diagnosis:

# Debug in Flask route
@main_bp.route('/debug-language')
def debug():
    return {
        'session_language': session.get('language'),
        'cookie_language': request.cookies.get('language'),
        'current_language': get_current_language(),
        'i18n_loaded': bool(load_i18n(get_current_language()))
    }

Solutions:

  1. Verify language code is supported
  2. Check session is configured correctly
  3. Ensure cookies are being set
  4. Confirm YAML files exist for all languages

Problem: String Interpolation Not Working

Symptoms:

  • Placeholders like {count} appearing literally
  • Formatted strings not showing variables

Solution:

{# ✗ Wrong: Missing .format() #}
{{ i18n_yaml.reading_time }}

{# ✓ Correct: Use .format() #}
{{ i18n_yaml.reading_time.format(minutes=5) }}

{# ✓ Alternative: Use replace() #}
{{ i18n_yaml.reading_time.replace('{minutes}', minutes|string) }}

Checklist for Implementation

Setup Checklist

  • Create i18n directory structure
  • Create default language file (en.yaml)
  • Configure Flask app with i18n settings
  • Implement YAML loading function
  • Create context processor for i18n data
  • Test i18n_yaml available in templates

Content Checklist

  • Extract hardcoded strings from templates
  • Organise strings in hierarchical YAML structure
  • Add descriptive comments to YAML files
  • Implement fallback values in all templates
  • Test all pages with empty i18n file

Multi-Language Checklist

  • Create language files for each supported language
  • Implement language detection logic
  • Build language switcher component
  • Add alternate language links for SEO
  • Test language switching functionality
  • Verify all translations are complete

Quality Checklist

  • All strings have sensible fallbacks
  • YAML files validated (no syntax errors)
  • Consistent naming conventions used
  • String interpolation works correctly
  • Pluralisation handled appropriately
  • Date/time formats localised

Additional Resources

Related Documentation

  • Theme Customisation: Learn how to customise themes with i18n
  • Template Development: Advanced Jinja2 patterns with i18n
  • Configuration Reference: Complete configuration options

Example Implementations

  • config/i18n/en.yaml: Default English translations
  • templates/components/language-switcher.html: Language switcher component
  • app/utils/i18n.py: i18n utility functions

External Resources

  • YAML Syntax: Learn YAML formatting rules
  • Flask-Babel: For more advanced i18n needs
  • Jinja2 Documentation: Template filters and functions

Conclusion

The YAML-based internationalisation system in MintyFlaskThemes provides a straightforward, maintainable approach to managing site-wide content and multi-language support.

Key takeaways:

  1. YAML files store all translatable content: Simple, editable, version-controllable
  2. Templates use fallback pattern: Robust against missing translations
  3. Flask integration is minimal: Context processor makes i18n data available everywhere
  4. Hierarchical organisation: Logical grouping makes content easy to find
  5. Scalable approach: Start simple, add languages as needed

This system works well for small to medium sites. For large-scale applications with hundreds of translations and complex workflows, consider dedicated i18n frameworks like Flask-Babel.