实操多语言博客

一、今日学习内容概述

二、模型设计

# models.py
from django.db import models
from django.conf import settings
from django.urls import reverse
from django.utils.translation import gettext_lazy as _

class Category(models.Model):
    """文章分类"""
    name = models.CharField(_('名称'), max_length=100)
    slug = models.SlugField(_('URL标识'), unique=True)
    
    class Meta:
        verbose_name = _('分类')
        verbose_name_plural = _('分类')
        
    def __str__(self):
        return self.name

class Post(models.Model):
    """博客文章"""
    category = models.ForeignKey(
        Category,
        on_delete=models.CASCADE,
        verbose_name=_('分类')
    )
    created_at = models.DateTimeField(_('创建时间'), auto_now_add=True)
    updated_at = models.DateTimeField(_('更新时间'), auto_now=True)
    is_active = models.BooleanField(_('是否激活'), default=True)
    
    class Meta:
        verbose_name = _('文章')
        verbose_name_plural = _('文章')
        
    def get_absolute_url(self):
        return reverse('post_detail', args=[str(self.id)])

class PostTranslation(models.Model):
    """文章翻译"""
    post = models.ForeignKey(
        Post,
        on_delete=models.CASCADE,
        related_name='translations'
    )
    language = models.CharField(
        _('语言'),
        max_length=10,
        choices=settings.LANGUAGES
    )
    title = models.CharField(_('标题'), max_length=200)
    content = models.TextField(_('内容'))
    slug = models.SlugField(_('URL标识'), max_length=200)
    
    class Meta:
        unique_together = ('post', 'language')
        verbose_name = _('文章翻译')
        verbose_name_plural = _('文章翻译')

三、视图实现

# views.py
from django.shortcuts import render, get_object_or_404
from django.utils.translation import get_language
from django.views.generic import ListView, DetailView
from .models import Post, PostTranslation

class PostListView(ListView):
    template_name = 'blog/post_list.html'
    context_object_name = 'posts'
    
    def get_queryset(self):
        language = get_language()
        return Post.objects.filter(
            translations__language=language,
            is_active=True
        ).select_related(
            'category'
        ).prefetch_related(
            'translations'
        )

class PostDetailView(DetailView):
    model = Post
    template_name = 'blog/post_detail.html'
    
    def get_object(self, queryset=None):
        post = super().get_object(queryset)
        language = get_language()
        translation = get_object_or_404(
            PostTranslation,
            post=post,
            language=language
        )
        post.translation = translation
        return post
    
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['available_languages'] = PostTranslation.objects.filter(
            post=self.object
        ).values_list('language', flat=True)
        return context

四、URL配置

# urls.py
from django.conf.urls.i18n import i18n_patterns
from django.urls import path, include
from . import views

urlpatterns = [
    path('i18n/', include('django.conf.urls.i18n')),
]

urlpatterns += i18n_patterns(
    path('', views.PostListView.as_view(), name='post_list'),
    path('<int:pk>/<slug:slug>/', 
         views.PostDetailView.as_view(), 
         name='post_detail'),
    prefix_default_language=False
)

五、模板实现

<!-- templates/blog/base.html -->
{% load i18n %}
<!DOCTYPE html>
<html lang="{{ LANGUAGE_CODE }}">
<head>
    <meta charset="UTF-8">
    <title>{% block title %}{% trans "多语言博客" %}{% endblock %}</title>
</head>
<body>
    <header>
        <nav>
            <form action="{% url 'set_language' %}" method="post" class="language-form">
                {% csrf_token %}
                <input name="next" type="hidden" value="{{ request.path }}">
                <select name="language" onchange="this.form.submit()">
                    {% get_current_language as LANGUAGE_CODE %}
                    {% get_available_languages as LANGUAGES %}
                    {% for lang_code, lang_name in LANGUAGES %}
                        <option value="{{ lang_code }}"
                                {% if lang_code == LANGUAGE_CODE %}selected{% endif %}>
                            {{ lang_name }}
                        </option>
                    {% endfor %}
                </select>
            </form>
        </nav>
    </header>

    <main>
        {% block content %}{% endblock %}
    </main>
</body>
</html>

<!-- templates/blog/post_list.html -->
{% extends "blog/base.html" %}
{% load i18n %}

{% block content %}
<div class="container">
    <h1>{% trans "文章列表" %}</h1>
    
    {% for post in posts %}
        <article class="post-preview">
            <h2>{{ post.translations.title }}</h2>
            <p class="meta">
                {% blocktrans with category=post.category.name date=post.created_at %}
                    分类:{{ category }} | 发布于:{{ date }}
                {% endblocktrans %}
            </p>
            <div class="excerpt">
                {{ post.translations.content|truncatewords:50 }}
            </div>
            <a href="{{ post.get_absolute_url }}" class="read-more">
                {% trans "阅读更多" %}
            </a>
        </article>
    {% endfor %}
</div>
{% endblock %}

六、流程图

每天40分玩转Django:实操多语言博客-LMLPHP

七、管理接口

# admin.py
from django.contrib import admin
from django.utils.translation import gettext_lazy as _
from .models import Category, Post, PostTranslation

class PostTranslationInline(admin.StackedInline):
    model = PostTranslation
    extra = 1
    
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ('get_title', 'category', 'created_at', 'is_active')
    list_filter = ('is_active', 'category', 'created_at')
    search_fields = ('translations__title', 'translations__content')
    inlines = [PostTranslationInline]
    
    def get_title(self, obj):
        default_trans = obj.translations.filter(
            language=settings.LANGUAGE_CODE
        ).first()
        return default_trans.title if default_trans else _('无标题')
    get_title.short_description = _('标题')

@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
    list_display = ('name', 'slug')
    prepopulated_fields = {'slug': ('name',)}

八、表单处理

# forms.py
from django import forms
from django.utils.translation import gettext_lazy as _
from .models import Post, PostTranslation

class PostTranslationForm(forms.ModelForm):
    class Meta:
        model = PostTranslation
        fields = ['title', 'content', 'slug']
        
    def clean_slug(self):
        slug = self.cleaned_data['slug']
        language = self.cleaned_data.get('language')
        
        if PostTranslation.objects.filter(
            slug=slug,
            language=language
        ).exists():
            raise forms.ValidationError(_('此URL标识已被使用'))
        return slug

class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ['category', 'is_active']

九、中间件

# middleware.py
from django.utils import translation
from django.conf import settings
from django.urls import resolve

class LanguageMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response
        
    def __call__(self, request):
        # 检查URL中的语言代码
        resolved = resolve(request.path_info)
        language = resolved.kwargs.get('language')
        
        if language:
            # 如果URL中包含语言代码,则设置语言
            translation.activate(language)
            request.LANGUAGE_CODE = language
        else:
            # 否则使用默认语言或用户偏好
            language = request.COOKIES.get(settings.LANGUAGE_COOKIE_NAME)
            if language:
                translation.activate(language)
                request.LANGUAGE_CODE = language
                
        response = self.get_response(request)
        return response

十、工具函数

# utils.py
from django.utils import translation
from django.conf import settings

def get_translated_field(obj, field_name, language=None):
    """获取翻译字段的值"""
    if language is None:
        language = translation.get_language()
        
    try:
        trans = obj.translations.get(language=language)
        return getattr(trans, field_name)
    except obj.translations.model.DoesNotExist:
        # 如果找不到翻译,返回默认语言的值
        try:
            trans = obj.translations.get(
                language=settings.LANGUAGE_CODE
            )
            return getattr(trans, field_name)
        except obj.translations.model.DoesNotExist:
            return None

def copy_translation(obj, from_lang, to_lang):
    """复制翻译"""
    try:
        source = obj.translations.get(language=from_lang)
        target, created = obj.translations.get_or_create(
            language=to_lang
        )
        
        if not created:
            return False
            
        for field in ['title', 'content', 'slug']:
            setattr(target, field, getattr(source, field))
        target.save()
        
        return True
    except obj.translations.model.DoesNotExist:
        return False

通过本章学习,你应该能够:

  1. 实现多语言博客系统
  2. 管理翻译内容
  3. 处理URL国际化
  4. 优化多语言用户体验

怎么样今天的内容还满意吗?再次感谢朋友们的观看,关注GZH:凡人的AI工具箱,回复666,送您价值199的AI大礼包。最后,祝您早日实现财务自由,还请给个赞,谢谢!

12-26 14:48