DEV Community

whchi
whchi

Posted on

Simple way to make i18n support with FastAPI

When dealing with i18n, the most common way is to add middleware to detect the specific locale in the input request. Fortunately, FastAPI supports middleware, making it pretty simple to do.

  • i18n_middleware.py
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
from starlette.requests import Request


class I18nMiddleware(BaseHTTPMiddleware):
    WHITE_LIST = ['en', 'ja', 'zh-TW']

    async def dispatch(  # type: ignore
            self, request: Request, call_next: RequestResponseEndpoint):
        # 1. headers 2. path 3. query string
        locale = request.headers.get('locale', None) or \
                 request.path_params.get('locale', None) or \
                 request.query_params.get('locale', None) or \
                 'zh-TW'

        if locale not in self.WHITE_LIST:
            locale = 'zh-TW'
        request.state.locale = locale

        return await call_next(request)
Enter fullscreen mode Exit fullscreen mode

You can add the middleware to your application in main.py:

  • main.py
app = FastAPI()
app.add_middleware(I18nMiddleware)
Enter fullscreen mode Exit fullscreen mode

Then you can use it in your router:

@router.get('/my-resource')
def get_my_resource(request: Request): 
    print(request.state.locale)
Enter fullscreen mode Exit fullscreen mode

However, there is no global helper like Laravel's __ or RoR's I18n.t, so we need to create our own.

The idea is pretty simple: put translation scripts in Python and write your own importer using importlib. Here's how you can do it:

  1. Write your translation scripts
locale = {
  'greeting': 'Hi, {user_name}',
  'title': 'hello world',
}
Enter fullscreen mode Exit fullscreen mode
  1. Put your translation scripts in the following structure
app/lang
├── en
│   └── messages.py
├── ja
│   └── messages.py
└── zh-TW
    └── messages.py
Enter fullscreen mode Exit fullscreen mode
  1. Write a class to handle translation
import importlib
from typing import Any, Dict


class Translator:
    _instances: Dict[str, 'Translator'] = {}

    def __new__(cls, lang: str) -> 'Translator':
        if lang not in cls._instances:
            cls._instances[lang] = super(Translator, cls).__new__(cls)
        return cls._instances[lang]

    def __init__(self, lang: str):
        self.lang = lang

    def t(self, key: str, **kwargs: Dict[str, Any]) -> str:
        file_key, *translation_keys = key.split('.')

        locale_module = importlib.import_module(f'app.lang.{self.lang}.{file_key}')

        translation = locale_module.locale
        for translation_key in translation_keys:
            translation = translation.get(translation_key, None)
            if translation is None:
                return f'Key {key} not found in {self.lang} locale'
        if kwargs.keys():
            translation = translation.format(**kwargs)
        return translation

Enter fullscreen mode Exit fullscreen mode

Then you can use it in your router

@router.get('/my-resource')
def get_my_resource(request: Request): 
    translator = Translator(request.state.locale)
    # 'hello world'
    print(translator.t('messages.title'))
    # 'Hi, Jon Doe'
    print(translator.t('messages.greeting'), user_name='Jon Doe')
Enter fullscreen mode Exit fullscreen mode

That is, pretty easy.

Top comments (0)