feat(i18n): add browser-language localization with Jinja2 _() and locale middleware
Some checks failed
ci/woodpecker/pr/pipeline Pipeline failed
Some checks failed
ci/woodpecker/pr/pipeline Pipeline failed
Add i18n support to the blog web UI with 4 languages (en/ru/fr/de),
80 translation keys, automatic Accept-Language detection, persistent
locale cookie, and a language switcher dropdown in the header.
- Infrastructure: TranslationService, translation dicts, convenience _()
- Presentation: locale middleware, /web/lang/{locale} switcher route
- Templates: all 9 templates use {{ _(key, current_locale) }}
- Tests: 26 tests across TranslationService, locale detection helpers
- Docs: TEST_MODEL.md and FEATURE_INFRASTRUCTURE.md updated with TC-UNIT-811-821
This commit is contained in:
@@ -1,29 +1,29 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Blog - Home{% endblock %}
|
||||
{% block meta_description %}Discover stories, thinking, and expertise from writers on any topic. A modern blog built with FastAPI.{% endblock %}
|
||||
{% block meta_keywords %}blog, articles, posts, writing, fastapi, python{% endblock %}
|
||||
{% block title %}{{ _('home.title', current_locale) }}{% endblock %}
|
||||
{% block meta_description %}{{ _('home.meta_description', current_locale) }}{% endblock %}
|
||||
{% block meta_keywords %}{{ _('home.meta_keywords', current_locale) }}{% endblock %}
|
||||
|
||||
{% block og_type %}website{% endblock %}
|
||||
{% block og_title %}Blog - Home{% endblock %}
|
||||
{% block og_description %}Discover stories, thinking, and expertise from writers on any topic.{% endblock %}
|
||||
{% block og_title %}{{ _('home.title', current_locale) }}{% endblock %}
|
||||
{% block og_description %}{{ _('home.meta_description', current_locale) }}{% endblock %}
|
||||
|
||||
{% block twitter_title %}Blog - Home{% endblock %}
|
||||
{% block twitter_description %}Discover stories, thinking, and expertise from writers on any topic.{% endblock %}
|
||||
{% block twitter_title %}{{ _('home.title', current_locale) }}{% endblock %}
|
||||
{% block twitter_description %}{{ _('home.meta_description', current_locale) }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="page-header" data-testid="page-header-home">
|
||||
<div class="page-header-flex">
|
||||
<div data-testid="page-header-content">
|
||||
<h1 class="page-title" data-testid="page-title-home">Latest Posts</h1>
|
||||
<p class="page-subtitle" data-testid="page-subtitle-home">Discover stories, thinking, and expertise from writers on any topic.</p>
|
||||
<h1 class="page-title" data-testid="page-title-home">{{ _('home.page_title', current_locale) }}</h1>
|
||||
<p class="page-subtitle" data-testid="page-subtitle-home">{{ _('home.page_subtitle', current_locale) }}</p>
|
||||
</div>
|
||||
{% if can_create %}
|
||||
<a href="/web/posts/new" class="btn btn-primary" data-testid="btn-create-post-header">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" style="margin-right: 0.5rem;">
|
||||
<path d="M8 2v12M2 8h12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
Write a Post
|
||||
{{ _('home.write_post', current_locale) }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -38,9 +38,9 @@
|
||||
<a href="/web/posts/{{ post.slug }}" data-testid="post-title-link-{{ post.id }}">{{ post.title }}</a>
|
||||
</h2>
|
||||
{% if post.published %}
|
||||
<span class="badge badge-success" data-testid="post-status-{{ post.id }}">Published</span>
|
||||
<span class="badge badge-success" data-testid="post-status-{{ post.id }}">{{ _('home.status_published', current_locale) }}</span>
|
||||
{% else %}
|
||||
<span class="badge" data-testid="post-status-{{ post.id }}">Draft</span>
|
||||
<span class="badge" data-testid="post-status-{{ post.id }}">{{ _('home.status_draft', current_locale) }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
<a href="/web/posts/{{ post.slug }}" class="btn btn-sm" data-testid="btn-read-more-{{ post.id }}">
|
||||
Read more
|
||||
{{ _('home.read_more', current_locale) }}
|
||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" style="margin-left: 0.25rem;">
|
||||
<path d="M6 12L10 8L6 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
@@ -77,26 +77,26 @@
|
||||
|
||||
<nav class="pagination" data-testid="pagination" aria-label="Pagination">
|
||||
{% if has_prev %}
|
||||
<a href="{{ request.url.path }}?page={{ current_page - 1 }}" class="pagination-item" data-testid="pagination-prev">Previous</a>
|
||||
<a href="{{ request.url.path }}?page={{ current_page - 1 }}" class="pagination-item" data-testid="pagination-prev">{{ _('home.pagination_previous', current_locale) }}</a>
|
||||
{% else %}
|
||||
<span class="pagination-item disabled" data-testid="pagination-prev">Previous</span>
|
||||
<span class="pagination-item disabled" data-testid="pagination-prev">{{ _('home.pagination_previous', current_locale) }}</span>
|
||||
{% endif %}
|
||||
|
||||
<span class="pagination-item active" data-testid="pagination-current">{{ current_page }}</span>
|
||||
|
||||
{% if has_next %}
|
||||
<a href="{{ request.url.path }}?page={{ current_page + 1 }}" class="pagination-item" data-testid="pagination-next">Next</a>
|
||||
<a href="{{ request.url.path }}?page={{ current_page + 1 }}" class="pagination-item" data-testid="pagination-next">{{ _('home.pagination_next', current_locale) }}</a>
|
||||
{% else %}
|
||||
<span class="pagination-item disabled" data-testid="pagination-next">Next</span>
|
||||
<span class="pagination-item disabled" data-testid="pagination-next">{{ _('home.pagination_next', current_locale) }}</span>
|
||||
{% endif %}
|
||||
</nav>
|
||||
|
||||
{% else %}
|
||||
<div class="empty-state" data-testid="empty-state">
|
||||
<div class="empty-state-icon" data-testid="empty-state-icon">📝</div>
|
||||
<h3 data-testid="empty-state-title">No posts yet</h3>
|
||||
<p data-testid="empty-state-description">Be the first to write a post!</p>
|
||||
<a href="/web/posts/new" class="btn btn-primary" data-testid="btn-create-first-post">Create your first post</a>
|
||||
<h3 data-testid="empty-state-title">{{ _('home.empty_title', current_locale) }}</h3>
|
||||
<p data-testid="empty-state-description">{{ _('home.empty_description', current_locale) }}</p>
|
||||
<a href="/web/posts/new" class="btn btn-primary" data-testid="btn-create-first-post">{{ _('home.empty_action', current_locale) }}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user