feat(i18n): add browser-language localization with Jinja2 _() and locale middleware
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:
2026-05-10 16:22:06 +03:00
parent 4e6505c598
commit d32ad29abc
18 changed files with 1077 additions and 100 deletions

View File

@@ -1,6 +1,6 @@
{% extends "base.html" %}
{% block title %}{% if is_edit %}Edit Post{% else %}New Post{% endif %} - Blog{% endblock %}
{% block title %}{% if is_edit %}{{ _('post_form.title_edit', current_locale) }}{% else %}{{ _('post_form.title_new', current_locale) }}{% endif %} - {{ _('base.default_title', current_locale) }}{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="/static/css/easymde.min.css" data-testid="easymde-stylesheet">
@@ -9,7 +9,7 @@
{% block content %}
<section class="page-header" data-testid="page-header-form">
<h1 class="page-title" data-testid="page-title-form">
{% if is_edit %}Edit Post{% else %}Create New Post{% endif %}
{% if is_edit %}{{ _('post_form.page_title_edit', current_locale) }}{% else %}{{ _('post_form.page_title_new', current_locale) }}{% endif %}
</h1>
</section>
@@ -22,7 +22,7 @@
<div class="card-body" data-testid="form-post-body">
<div class="form-group" data-testid="form-group-title">
<label for="title" class="form-label form-label-required" data-testid="label-title">
Title
{{ _('post_form.label_title', current_locale) }}
</label>
<input
type="text"
@@ -30,31 +30,31 @@
name="title"
class="input input-lg"
value="{% if post %}{{ post.title }}{% endif %}"
placeholder="Enter post title"
placeholder="{{ _('post_form.placeholder_title', current_locale) }}"
required
data-testid="input-title"
>
<span class="form-hint" data-testid="hint-title">A catchy title for your post</span>
<span class="form-hint" data-testid="hint-title">{{ _('post_form.hint_title', current_locale) }}</span>
</div>
<div class="form-group" data-testid="form-group-content">
<label for="content" class="form-label form-label-required" data-testid="label-content">
Content
{{ _('post_form.label_content', current_locale) }}
</label>
<textarea
id="content"
name="content"
rows="12"
placeholder="Write your post content here..."
placeholder="{{ _('post_form.placeholder_content', current_locale) }}"
required
data-testid="textarea-content"
>{% if post %}{{ post.content }}{% endif %}</textarea>
<span class="form-hint" data-testid="hint-content">The main content of your post. Markdown is supported.</span>
<span class="form-hint" data-testid="hint-content">{{ _('post_form.hint_content', current_locale) }}</span>
</div>
<div class="form-group" data-testid="form-group-tags">
<label for="tags" class="form-label" data-testid="label-tags">
Tags
{{ _('post_form.label_tags', current_locale) }}
</label>
<input
type="text"
@@ -62,10 +62,10 @@
name="tags"
class="input"
value="{% if post %}{{ post.tags|join(', ') }}{% endif %}"
placeholder="python, fastapi, tutorial"
placeholder="{{ _('post_form.placeholder_tags', current_locale) }}"
data-testid="input-tags"
>
<span class="form-hint" data-testid="hint-tags">Comma-separated list of tags</span>
<span class="form-hint" data-testid="hint-tags">{{ _('post_form.hint_tags', current_locale) }}</span>
</div>
</div>
@@ -73,15 +73,15 @@
<div class="card-footer" data-testid="form-post-footer">
<div class="flex justify-between items-center" data-testid="form-actions">
<a href="{% if is_edit %}/web/posts/{{ post.slug }}{% else %}/web/{% endif %}" class="btn" data-testid="btn-cancel">
Cancel
{{ _('post_form.cancel', current_locale) }}
</a>
<div class="flex gap-2" data-testid="form-submit-actions">
<button type="submit" name="action" value="draft" class="btn" data-testid="btn-save-draft">
Save as Draft
{{ _('post_form.save_draft', current_locale) }}
</button>
<button type="submit" name="action" value="publish" class="btn btn-primary" data-testid="btn-publish-post">
{% if is_edit %}Update Post{% else %}Publish Post{% endif %}
{% if is_edit %}{{ _('post_form.update_post', current_locale) }}{% else %}{{ _('post_form.publish_post', current_locale) }}{% endif %}
</button>
</div>
</div>
@@ -99,7 +99,7 @@
spellChecker: false,
status: false,
minHeight: '300px',
placeholder: 'Write your post content here...',
placeholder: '{{ _('post_form.placeholder_content', current_locale) }}',
toolbar: [
'bold', 'italic', 'heading', '|',
'code', 'quote', 'unordered-list', 'ordered-list', '|',