1. Django Tutorial(Airbnb) - Django Translation

ID์งฑ์žฌยท2021๋…„ 8์›” 30์ผ
0

Django

๋ชฉ๋ก ๋ณด๊ธฐ
29/43
post-thumbnail

๐ŸŒˆ Django Translation

๐Ÿ”ฅ Settings & TransTag

๐Ÿ”ฅ Translation File

๐Ÿ”ฅ get_current_language

๐Ÿ”ฅ Python code Translation



1. Settings & TransTag


๐Ÿค” LOCALE_PATHS & MIDDLEWARE

โœ”๏ธ Django Translation์€ ๊ตญ๊ฐ€ ์„ค์ •์„ ํ†ตํ•ด ํŽ˜์ด์ง€๋ฅผ ๋ฒˆ์—ญํ•ด์ฃผ๋Š” ๊ธฐ๋Šฅ์ž…๋‹ˆ๋‹ค. ์ด๋ฅผ ์œ„ํ•ด์„œ "locale" ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ํ”„๋กœ์ ํŠธ ๋””๋ ‰ํ† ๋ฆฌ ์•„๋ž˜ ๋งŒ๋“ค์–ด์ค๋‹ˆ๋‹ค.

โœ”๏ธ "locale" ๋””๋ ‰ํ† ๋ฆฌ์—๋Š” Translation ํŒŒ์ผ์ด ์ €์žฅ๋œ ์žฅ์†Œ๋กœ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. 'locale" ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ์ƒ์„ฑ ํ›„ settings.py์—์„œ Translation ํŒŒ์ผ์€ "locale"์— ์ €์žฅํ•˜๋ผ๊ณ  Django์—๊ฒŒ ๊ฒฝ๋กœ๋ฅผ ์•Œ๋ ค์ค๋‹ˆ๋‹ค.

  • ๐Ÿ”Ž LOCALE_PATHS = (os.path.join(BASE_DIR, "locale"),)
# settings.py
MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
    "django.middleware.locale.LocaleMiddleware", # ๐Ÿ‘ˆ ์ถ”๊ฐ€
]
...
...
...
# Locale
LOCALE_PATHS = (os.path.join(BASE_DIR, "locale"),)

๐Ÿค” trans ํ…œํ”Œ๋ฆฟ ์ฝ”๋“œ

โœ”๏ธ ์ด์ œ HTML์˜ ์–ด๋Š ๋ถ€๋ถ„์„ Django์—๊ฒŒ ๋ฒˆ์—ญ์‹œํ‚ฌ์ง€ ์˜์—ญ์„ ์ง€์ •ํ•ด ์ค๋‹ˆ๋‹ค. "footer"์™€ "search bar"์— "placeholder" ๋ถ€๋ถ„์„ Translation ์˜์—ญ์œผ๋กœ ์ง€์ •ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.
โœ”๏ธ ์ง€์ •์„ ์œ„ํ•ด์„œ๋Š” HTML ํŒŒ์ผ ์ตœ์ƒ๋‹จ์— i18n์„ loadํ•ด์•ผํ•˜๋Š”๋ฐ์š”, internationaliztion ์˜ ์ค„์ž„๋ง์ด i18n์ž…๋‹ˆ๋‹ค.

  • ๐Ÿ”Ž {% load static i18n %} ๋˜๋Š” {% load static internationaliztion %}

โœ”๏ธ "internationaliztion"์„ load ์‹œ์ผฐ๋‹ค๋ฉด, ๋ฒˆ์—ญ์ด ํ•„์š”ํ•œ ์˜์—ญ์— trans ํ…œํ”Œ๋ฆฟ ์ฝ”๋“œ๋ฅผ ์ ์šฉ์‹œํ‚ต๋‹ˆ๋‹ค.

  • ๐Ÿ”Ž {% trans "Please don't copy us." %}
  • ๐Ÿ”Ž placeholder="{% trans "Search By City" %}"
# footer.html
{% load i18n %} # ๐Ÿ‘ˆ load "i18n"
<footer class="container mx-auto text-center py-10 border-t font-medium text-gray-500">
    <span> {% trans "Please don't copy us." %} </span> # ๐Ÿ‘ˆ trnas ํ…œํ”Œ๋ฆฟ ์ฝ”๋“œ
    <span> &copy; 2021 Airbnb-Clone, {% trans "All rights reserved" %}. </span>
</footer>
# base.html
{% load static i18n %} # ๐Ÿ‘ˆ load "i18n"
<!DOCTYPE html>
<html lang="en">
...
...
<form method="get" action="{% url "rooms:search" %}" class="w-9/12">
    <input
        class="search-box border px-5 w-full font-medium text-gray-900 placeholder-gray-600 py-3 rounded-sm shadow-md hover:shadow-lg focus:outline-none"
        name="city"
        placeholder="{% trans "Search By City" %}" # ๐Ÿ‘ˆ trnas ํ…œํ”Œ๋ฆฟ ์ฝ”๋“œ
    />
</form>
</html>

๐Ÿค” blocktrans ํ…œํ”Œ๋ฆฟ ์ฝ”๋“œ

โœ”๏ธ ๋‹จ์–ด๋ฅผ ํ•˜๋‚˜ํ•˜๋‚˜ ๋ณ€๊ฒฝํ•  ๋•Œ๋Š” trans ํ…œํ”Œ๋ฆฟ ํ…Œ๊ทธ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, ์˜์—ญ์„ ์ง€์ •ํ•ด์„œ ๋ณ€๊ฒฝํ•  ๋•Œ๋Š” blocktrans ํƒœ๊ทธ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ๋งฅ๋ฝ์ด ์ค‘์š”ํ•  ๋•Œ๋Š” ์ด ๋ฐฉ๋ฒ•์ด ๋” ์œ ์šฉํ•˜๊ฒŒ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. blockrans๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ๋„ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ i18n์„ loadํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

  • ๐Ÿ”Ž {% load i18n %}

โœ”๏ธ blocktrans๋Š” ํ…œํ”Œ๋ฆฟ ๋ณ€์ˆ˜๋ฅผ ์ž๋™์œผ๋กœ ์ธ์‹ํ• ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์ด์— ํ…œํ”Œ๋ฆฟ ๋ณ€์ˆ˜๋ฅผ ์ธ์‹์‹œํ‚ค๊ธฐ ์œ„ํ•ด ํ•„์š”ํ•œ ๋ณ€์ˆ˜๋ฅผ blocktrans ๋‚ด์—์„œ with ๊ตฌ๋ฌธ ์‚ฌ์šฉํ•˜์—ฌ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค.

  • ๐Ÿ”Ž {% blocktrans with current_page=page_obj.number total_pages=page_obj.paginator.num_pages %}
{% extends "base.html" %}
{% load i18n %} # ๐Ÿ‘ˆ "i18n" load
    {% block page_title %}
        Home
    {% endblock page_title %}
    {% block content %}
    ...
    ...
<span class="mx-3 font-medium text-lg">
    {% blocktrans with   # ๐Ÿ‘ˆ ๋ณ€์ˆ˜๋ฅผ ์ง€์ •ํ•œ ๋’ค, ์‚ฌ์šฉํ•ด์•ผ ์ธ์‹ํ•ฉ๋‹ˆ๋‹ค.
        current_page=page_obj.number 
        total_pages=page_obj.paginator.num_pages 
    %}Page {{current_page}} of {{total_pages}}
    {% endblocktrans %}
</span>
...
...


2. Translation File

๐Ÿค” makemessages & comilemessages

โœ”๏ธ Translation์˜ ๊ฒฝ๋กœ๋ฅผ settings.py์—์„œ ์„ค์ •ํ•˜๊ณ , HTML์—์„œ ๋ฒˆ์—ญํ•  ์˜์—ญ์„ ์ง€์ •ํ•˜์˜€์Šต๋‹ˆ๋‹ค. ์ด์ œ Django์—๊ฒŒ ๋ฒˆ์—ญํ•  ์–ธ์–ด์™€ ํ•จ๊ป˜makemessage ๋ช…๋ น์„ ๋‚ด๋ ค์ฃผ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

โœ”๏ธ ๋งŒ์ผ ๋ช…๋ น์—์„œ ์˜ค๋ฅ˜๊ฐ€ ๋‚œ๋‹ค๋ฉด, gettext๊ฐ€ ์„ค์น˜๋˜์ง€ ์•Š์•˜์„ ์ˆ˜ ์žˆ์–ด์š”. ์•„๋ž˜์˜ ๋ช…๋ น์„ ์‹คํ–‰ํ•ด๋ณด์„ธ์š”:)

  • ๐Ÿ”Ž ์„ค์น˜ ๋ชฉ๋ก ํ™•์ธ : brew list
  • ๐Ÿ”Ž gettext ์„ค์น˜ : brew install gettext
  • ๐Ÿ”Ž gettext ์—ฐ๊ฒฐ : brew link gettext --force

โœ”๏ธ gettext๊ฐ€ ์„ค์น˜์™€ ์—ฐ๊ฒฐ์ด ์ž˜ ๋˜์–ด์žˆ๊ณ , makemessages ๋ช…๋ น์— ๋ฌธ์ œ๊ฐ€ ์—†๋‹ค๋ฉด, locale ํด๋”์— "django.po"๋ž€ ํŒŒ์ผ์ด ์ƒ์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ์šฐ๋ฆฌ๊ฐ€ "locale" ๋””๋ ‰ํ† ๋ฆฌ์— ํŒŒ์ผ ๊ฒฝ๋กœ๋ฅผ ์ง€์ •ํ•œ ์ด์œ ๋Š” ๋ฐ”๋กœ ์ด๋ฅผ ์œ„ํ•จ์ž…๋‹ˆ๋‹ค.

#: templates/base.html:24
msgid "Search By City"
msgstr ""
#: templates/partials/footer.html:3
msgid "Please don't copy us."
msgstr ""
#: templates/partials/footer.html:4
msgid "All rights reserved"
msgstr ""

โœ”๏ธ "makemessages" ๋ช…๋ น์„ ํ†ตํ•ด Django๊ฐ€ taggingํ•œ ์˜์—ญ์„ ๋ชจ๋‘ ์ฐพ์•„ Translationํ•  ์ˆ˜ ์žˆ๊ฒŒ ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด ์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค. ์ด์— "msgstr" ๋ถ€๋ถ„์— ๋ฒˆ์—ญํ•  ๋‚ด์šฉ์„ ์ ์–ด์ค๋‹ˆ๋‹ค.

...
...
#: templates/base.html:24
msgid "Search By City"
msgstr "๋„์‹œ๋ณ„ ๊ฒ€์ƒ‰"
#: templates/partials/footer.html:3
msgid "Please don't copy us."
msgstr "์ œ๋ฐœ ์šฐ๋ฆฌ๋ฅผ ๋„์šฉํ•˜์ง€ ๋งˆ์„ธ์š”."
#: templates/partials/footer.html:4
msgid "All rights reserved"
msgstr "ํŒ๊ถŒ ์†Œ์œ "
#: templates/rooms/room_list.html:24
#, python-format
msgid "Page %(current_page)s of %(total_pages)s"
msgstr "์ด %(total_pages)s ์ค‘ %(current_page)s ์ชฝ" # ๐Ÿ‘ˆ s๊นŒ์ง€ ์‚ฌ์šฉํ•ด์•ผํ•ด์š”.

โœ”๏ธ "django.po"์—์„œ ๋ฒˆ์—ญํ•  ๋‚ด์šฉ์„ ์ž‘์„ฑํ•˜์˜€๋‹ค๋ฉด, compliemessages ๋ช…๋ น์„ ๋‚ด๋ ค์ค๋‹ˆ๋‹ค. ์ด ๋ช…๋ น์„ ์ƒ์„ฑํ•˜๋ฉด locale ๋””๋ ‰ํ† ๋ฆฌ์— "django.mo" ํŒŒ์ผ์ด ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.

  • ๐Ÿ”Ž django-admin compilemessages


3. get_current_language

๐Ÿค” get_current_language

โœ”๏ธ ์ด์ œ ์‚ฌ์šฉ์ž๊ฐ€ HTML์—์„œ ์–ธ์–ด ์„ ํƒ์„ ํ•  ์ˆ˜ ์žˆ๋„๋ก ์„ ํƒ ๋ชฉ๋ก์„ ๋งŒ๋“ค์–ด ์ฃผ๊ณ , ์ด ์„ ํƒ ์‚ฌํ•ญ์— ๋”ฐ๋ผ ํŽ˜์ด์ง€์˜ ์–ธ์–ด๊ฐ€ ๋ณ€๊ฒฝ๋  ์ˆ˜ ์žˆ๋„๋ก ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.
โœ”๏ธ ์–ธ์–ด ์„ ํƒ์„ ์œ„ํ•ด select ํƒœ๊ทธ๋ฅผ ์ด์šฉํ•˜์—ฌ ์„ ํƒ๋ชฉ๋ก์„ ์ถ”๊ฐ€ํ–ˆ๊ณ , ko๋กœ ์–ธ์–ด๋ฅผ ๋ฐ”๊ฟง๋‹ค๊ฐ€, ๋‹ค์‹œ en๋กœ ๋Œ์•„์˜ค๊ธฐ ์œ„ํ•ด์„œ "get_current_language"๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

  • ๐Ÿ”Ž {% get_current_language as LANGUAGE_CODE %}
# footer.html
{% load i18n %}
<footer class="container mx-auto text-center py-10 border-t font-medium text-gray-500">
    <div class="flex flex-col">
        <span>
            {% trans "Please don't copy us." %}
        </span<>
        <span>&copy; 2021 AirBnb-Clone, {% trans "All rights reserved" %}.</span>
    </div>
    <div class="mt-10 flex">
    	{% get_current_language as LANGUAGE_CODE %}
        <select class="w-1/5 h-8" id="js-lang"> # ๐Ÿ‘ˆ js์—์„œ ํ•ด๋‹น ํƒœ๊ทธ์˜ ๋ณ€ํ™”๋ฅผ ๊ฐ์ง€ํ•  ์ˆ˜ ์žˆ๊ฒŒ id๊ฐ’์„ ๋„ฃ์–ด ์ค๋‹ˆ๋‹ค.
            <option value="en"
                {% if LANGUAGE_CODE == 'en' %}
                selected
                {% endif %}>English
            </option>
            <option value="ko"
                {% if LANGUAGE_CODE == 'ko' %}
                    selected
                {% endif %}>Korean
            </option>
        </select>
    </div>
</footer>

๐Ÿค” addEventListener

โœ”๏ธ select ํƒœ๊ทธ์˜ ๋ณ€ํ™”๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด, addEventListener๋ฅผ ํ†ตํ•ด ์ด๋ฅผ ๊ฐ์ง€ํ•˜๊ณ  ํŽ˜์ด์ง€๋ฅผ ์ƒˆ๊ณ ๋กœ์นจ(reload)ํ•  ์ˆ˜ ์žˆ๋„๋ก javascript๋ฅผ ํ™œ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.
โœ”๏ธ ๋˜ํ•œ fetch๋ฅผ ํ†ตํ•ด ๊ฒฝ๋กœ๋ฅผ ์„ค์ •ํ•ด ์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.

  • ๐Ÿ”Ž fetch({% url 'users:switch-language' %}?lang=${selected})
    <script>
        const langSelect = document.getElementById("js-lang");
        const handleLangChange = () => {
            const selected = langSelect.value; // ๐Ÿ‘ˆ value๊ฐ’ ์ถ”์ถœ
            fetch(`{% url 'users:switch-language' %}?lang=${selected}`).then(() => window.location.reload());
        } // ๐Ÿ‘ˆ ๊ฒฝ๋กœ๋กœ request๋ฅผ ๋ณด๋‚ด๊ณ , ๊ทธ๋Ÿฌ๊ณ ๋‚˜์„œ ์ƒˆ๋กœ๊ณ ์นจ
        langSelect.addEventListener("change", handleLangChange); // ๐Ÿ‘ˆ "js-lang"์— ๋ณ€ํ™”๋ฅผ ๊ฐ์ง€ํ•˜๋ฉด, "handleLangChange" ํ•จ์ˆ˜ ์‹คํ–‰
    </script>

๐Ÿค” views.py & urls.py

โœ”๏ธ view์™€ url์„ ๋งคํ•‘ํ•ด ์ž‘๋™๋˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
โœ”๏ธ tranlation์„ import ํ›„, url์—์„œ ๊ฐ€์ ธ์˜จ ์–ธ์–ด์˜ ๊ฐ’์ด("langSelect.value") ์กด์žฌํ•œ๋‹ค๋ฉด,, session์— ์–ธ์–ด ๊ฐ’์„ ์ถ”๊ฐ€ํ•ด์ค๋‹ˆ๋‹ค.

  • ๐Ÿ”Ž request.session[translation.LANGUAGE_SESSION_KEY] = lang
#user/views.py
from django.http import HttpResponse # ๐Ÿ‘ˆ "HttpResponse" ์ถ”๊ฐ€
from django.utils import translation # ๐Ÿ‘ˆ "translation" ์ถ”๊ฐ€
...
...
def switch_language(request):
    lang = request.GET.get("lang", None)
    if lang is not None:
        request.session[translation.LANGUAGE_SESSION_KEY] = lang  # ๐Ÿ‘ˆ session์— ์–ธ์–ด๊ฐ’ ์ถ”๊ฐ€
    return HttpResponse(status=200)
#user/urls.py
from django.urls import path
from . import views
app_name = "users"
urlpatterns = [
    ...
    ...
    path("switch-language/", views.switch_language, name="switch-language"),
]



4. Python code Translation

๐Ÿค” get_textlazy

โœ”๏ธ Template์—์„œ Translationํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„ ๋ณด์•˜์œผ๋‹ˆ, python code๋ฅผ Translationํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณผ๊ป˜์š”:)
โœ”๏ธ ์ด๋ฅผ ์œ„ํ•ด gettext_lazy๋ฅผ importํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” reverselazy์ฒ˜๋Ÿผ ์ฆ‰๊ฐ์ ์œผ๋กœ ์‹คํ–‰๋˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ, ์ฝ”๋“œ๊ฐ€ ํ˜ธ์ถœ๋˜์—ˆ์„ ๋•Œ๋งŒ ๋ฐ˜์‘ํ•ด์š”.

  • ๐Ÿ”Ž from django.utils.translation import gettext_lazy

โœ”๏ธ gettextlazy๋ฅผ `๋กœ ์‚ฌ์šฉํ•˜๊ธฐ๋กœ ํ•˜์˜€์œผ๋‹ˆ, _`์— ์ธ์ž๋กœ ์ „๋‹ฌํ•˜์—ฌ makemessages๋ฅผ ๋ช…๋ น์‹œํ‚ต๋‹ˆ๋‹ค.

  • ๐Ÿ”Ž django-admin makemessages locale=ko
# users/models.py
from django.utils.translation import gettext_lazy as _ # ๐Ÿ‘ˆ gettext_lazy ํ˜ธ์ถœ
...
# Create your models here.
class User(AbstractUser):
    GENDER_MALE = "male"
    GENDER_FEMALE = "female"
    GENDER_OTHER = "other"
    GENDER_CHOICES = (
        (GENDER_MALE, _("Male")), # ๐Ÿ‘ˆ _()๋กœ gettext_lazy๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.
        (GENDER_FEMALE, _("Female")), # ๐Ÿ‘ˆ _()๋กœ gettext_lazy๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.
        (GENDER_OTHER, _("Other")), # ๐Ÿ‘ˆ _()๋กœ gettext_lazy๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.
    )
    LANGUAGE_ENGLISH = "en"
    LANGUAGE_KOREAN = "kr"
    LANGUAGE_CHOICES = (
        (LANGUAGE_ENGLISH, _("English")), # ๐Ÿ‘ˆ _()๋กœ gettext_lazy๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.
        (LANGUAGE_KOREAN, _("Korean")), # ๐Ÿ‘ˆ _()๋กœ gettext_lazy๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.
    )
   ...
   ...
    avatar = models.ImageField(upload_to="avatars", blank=True)
    gender = models.CharField(
        _("gender"), choices=GENDER_CHOICES, max_length=10, blank=True
    ) # ๐Ÿ‘ˆ _()๋กœ gettext_lazy๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.
    bio = models.TextField(blank=True)
    birthdate = models.DateField(null=True, blank=True)
    language = models.CharField(
        _("language"), # ๐Ÿ‘ˆ _()๋กœ gettext_lazy๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.
        choices=LANGUAGE_CHOICES,
        max_length=2,
        blank=True,
        default=LANGUAGE_KOREAN,
    )
...
...

โœ”๏ธ msgstr์„ ๋‹ค ์ž‘์„ฑํ•œ ํ›„์— compile๋„ ์ง„ํ–‰ํ•ด์ฃผ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

  • ๐Ÿ”Ž django-admin compilemessages
#: users/models.py:20
msgid "Male"
msgstr "๋‚จ์„ฑ"
#: users/models.py:21
msgid "Female"
msgstr "์—ฌ์„ฑ"
#: users/models.py:22
msgid "Other"
msgstr "๊ธฐํƒ€"
#: users/models.py:29
msgid "English"
msgstr "์˜์–ด"
#: users/models.py:30
msgid "Korean"
msgstr "ํ•œ๊ตญ์–ด"
#: users/models.py:53
msgid "gender"
msgstr "์„ฑ๋ณ„"
#: users/models.py:58
msgid "language"
msgstr "์–ธ์–ด"
#: users/models.py:85
msgid "Verify Airbnb Account"
msgstr "Airbnb ๊ณ„์ • ์ธ์ฆ"
profile
Keep Going, Keep Coding!

0๊ฐœ์˜ ๋Œ“๊ธ€