Python flask web application with multilanguages support by host and prefix
Asked Answered
P

4

3

I have one server with flask application instance and have several domain which mapped to this server by DNS.

My site must support several languages by host and prefix:

mysite.com - english
mysite.com/fr - franch
mysite.ru - russian
mysite.ru/by - belarusian
localhost or other unknown host without language prefix - default language (english)

I implemented it with double route registration /endpoint and /<lang>/endpoint and reloaded url_for function and it work, but now I must implement custom error pages for abort function:

mysite.com/wrong-url-there - mysite.com/404.html (english)
mysite.com/fr/wrong-url-there - mysite.com/fr/404.html (franch)
mysite.ru/wrong-url-there - mysite.ru/404.html (russian)
mysite.ru/by/wrong-url-there - mysite.ru/by/404.html (belorusian)

And I don't see solution for this. I think my implementation bad and I must improve it. I think I must create one instance of application for each site language root with predefined language for it or use blueprint, but I don't find solution for me yet.

Is anybody can give me advice how resolve this url multilanguages support with flask or wsgi or nginx?

Prearrange answered 4/2, 2013 at 11:45 Comment(0)
P
0

My own solution:

from flask import Flask, g, render_template, redirect, request


app = Flask(__name__)

default_language = 'en'
language_urls = {
    'en': 'mysite.com',
    'fr': 'mysite.com/fr',
    'ru': 'mysite.ru',
    'by': 'mysite.ru/by',
}
languages = ','.join(language_urls.keys())


def get_language_by_request(request_host, request_path):
    '''
    Looking bad, but work.
    I cab't use request.view_args there,
    because this can't detect language for 404 pages
    like mysite.com/fr/unknown-page
    '''
    request_host_path = request_host + request_path
    request_paths = request_host_path.split('/', 2)
    if (len(request_paths) > 1 and request_paths[1] in language_urls.keys()):
        request_language_prefix = request_paths[1]
        return request_language_prefix
    for language, url in language_urls.items():
        host_prefix = url.split('/')
        if len(host_prefix) == 1:
            host, = host_prefix
            if request_host == host:
                return language
    return default_language


def get_language_url_parameter_value(language, request_host):
    host_prefix = language_urls[language]
    if host_prefix == request_host:
        return None
    return language


def get_redirection_url_by_request(request_host, request_path, request_url):
    '''
    Looking bad, but work.
    I cab't use request.view_args there,
    because this can't detect language for 404 pages
    like mysite.com/fr/unknown-page
    '''
    request_host_path = request_host + request_path
    request_paths = request_host_path.split('/', 2)
    request_language_prefix = None
    if (len(request_paths) > 1 and request_paths[1] in language_urls.keys()):
        request_language_prefix = request_paths[1]
    hosts = []
    for language, url in language_urls.items():
        host_prefix = url.split('/')
        if len(host_prefix) == 1:
            host, = host_prefix
            language_prefix = None
        else:
            host, language_prefix = host_prefix
        if request_host == host and request_language_prefix == language_prefix:
            return None
        hosts.append(host)
    if request_host not in hosts:
        return None
    if request_language_prefix:
        request_host_prefix = request_host + '/' + request_language_prefix
        host_prefix = language_urls[request_language_prefix]
        return request_url.replace(request_host_prefix, host_prefix)
    return None


@app.url_defaults
def set_language_in_url(endpoint, values):
    if '_lang' not in values and hasattr(g, 'language_url_value'):
        values['_lang'] = g.language_url_value


@app.url_value_preprocessor
def get_language_from_url(endpoint, values):
    g.language = get_language_by_request(request.host, request.path)
    g.language_url_value = get_language_url_parameter_value(g.language, request.host)
    if values and '_lang' in values:
        del values['_lang']


@app.before_request
def check_language_redirection():
    redirection_url = get_redirection_url_by_request(request.host, request.path, request.url)
    return redirect(redirection_url) if redirection_url else None


@app.route('/')
@app.route('/<any(%s):_lang>/' % languages)
def home():
    return render_template('home.html')


@app.route('/other/')
@app.route('/<any(%s):_lang>/other/' % languages)
def other():
    return render_template('other.html')

I don't use blueprints there because I also use flask-login and I can't set several login pages with different languages for each blueprint. For example if page required login, flask redirect me to login page and I must update language for this page. Also login pages can't be as mysite.com/login, mysite.com/fr/login and etc without several redirections.

UPD: I can't use request.view_args for detect language or redirection, because on this case I can't detect language for error pages as mysite.com/fr/wrong-page-there (can't detect endpoint and view_args). To avoid this problem I can use hask: add url rule as /<lang_code>/<path:path> and raise 404 error there.

Prearrange answered 19/2, 2013 at 13:36 Comment(0)
C
4

It's in the official doc: http://flask.pocoo.org/docs/patterns/urlprocessors/ (This is basically the same answer as Matthew Scragg's).

Codie answered 12/4, 2013 at 10:27 Comment(1)
Thasks for useful link, now I have solution very similar for this, but see bottom of my own answer why I don't use blueprints and request.view_args for language detection: https://mcmap.net/q/1778137/-python-flask-web-application-with-multilanguages-support-by-host-and-prefix.Prearrange
H
3

I worked on something similar few months back. I modified it a bit and pushed to github. You can do what codegeek suggested if you are unable to make your templates language neutral. With this method you can cut down on the template files needed.

https://github.com/scragg0x/Flask-Localisation-Example

mysite.py

from flask import Flask, Blueprint, g, redirect, request

app = Flask(__name__)

mod = Blueprint('mysite', __name__, url_prefix='/<lang_code>')

sites = {
    'mysite.com': 'en',
    'myothersite.com': 'fr'
}

@app.url_defaults
def add_language_code(endpoint, values):
    values.setdefault('lang_code', g.lang_code)

@app.url_value_preprocessor
def pull_lang_code(endpoint, values):
    url = request.url.split('/', 3)
    g.lang_code = sites[url[2]]

@mod.url_defaults
def add_language_code(endpoint, values):
    values.setdefault('lang_code', g.lang_code)

@mod.url_value_preprocessor
def pull_lang_code(endpoint, values):
    g.lang_code = values.pop('lang_code')

@app.route('/')
@mod.route('/')
def index():
    # Use g.lang_code to pull localized data for template
    return 'lang = %s' % g.lang_code

app.register_blueprint(mod)

tests.py

import os
import unittest
import re
import requests
import urllib2
import json
from mysite import app

class MySiteTestCase(unittest.TestCase):

    def setUp(self):
        app.config['TESTING'] = True
        app.config['SERVER_NAME'] = 'mysite.com'
        self.domain = 'http://mysite.com/'
        self.app = app.test_client()

    def tearDown(self):
        pass

    def test_en_index(self):
        rv = self.app.get('/en/', self.domain)
        self.assertEqual(rv.data, 'lang = en')
        print self.domain, rv.data

    def test_fr_index(self):
        rv = self.app.get('/fr/', self.domain)
        self.assertEqual(rv.data, 'lang = fr')
        print self.domain, rv.data

    def test_default(self):
        rv = self.app.get('/', self.domain)
        self.assertEqual(rv.data, 'lang = en')
        print self.domain, rv.data

class MyOtherSiteTestCase(unittest.TestCase):

    def setUp(self):
        app.config['TESTING'] = True
        app.config['SERVER_NAME'] = 'myothersite.com'
        self.domain = 'http://myothersite.com/'
        self.app = app.test_client()

    def tearDown(self):
        pass

    def test_en_index(self):
        rv = self.app.get('/en/', self.domain)
        self.assertEqual(rv.data, 'lang = en')
        print self.domain, rv.data

    def test_fr_index(self):
        rv = self.app.get('/fr/', self.domain)
        self.assertEqual(rv.data, 'lang = fr')
        print self.domain, rv.data

    def test_default(self):
        rv = self.app.get('/', self.domain)
        self.assertEqual(rv.data, 'lang = fr')
        print self.domain, rv.data

if __name__ == '__main__':
    unittest.main()
Hancock answered 7/2, 2013 at 4:57 Comment(1)
Thansk, blueprints good idea but it has some troubles with flask-login: I can't set several login pages for several brueprints. However my own solution also use url_value_preprocessor and url_default. Instead blueprints I use double routing as: / and /<any(en, fr, ru, be):_lang>.Prearrange
T
2

Disclaimer: This code is not tested. I am just giving you a ballpark idea of how to approach this.

I suggest you use blueprints in combination with an extension like Flask-Babel. For example, you can do something like:

views.py

mysitebp = Blueprint('mysitebp',__name__)

Then in your application package (usually __init__.py) , you can do:

__init__.py

from mysite.views import mysitebp
app = Flask(__name__)
app.register_blueprint(mysitebp,url_prefix='/en/',template_folder='en')
app.register_blueprint(mysitebp,url_prefix='/fr',template_folder='fr')

..and so on

Your directory structure could look like:

mysite/
__init__.py
views.py
templates/
    base.html
    404.html
    en/
        en.html
    fr/
        french.html

Flask-Babel would help you translate the 404.html etc.

Tradesman answered 4/2, 2013 at 14:49 Comment(3)
Blueprints good idea but it has some troubles with flask-login: I can't set several login pages for several brueprints.Prearrange
You don't need to set several login pages for several blueprints. I suggest you use 1 blueprint to manage users,auth etc. where you use flask-login. Then, you can just use the functions in any other blueprint.Tradesman
How Flask will resolve login_required for blueprints? I mean it rederect to one login page /login instead /login, /fr/login, /by/login for each language.Prearrange
P
0

My own solution:

from flask import Flask, g, render_template, redirect, request


app = Flask(__name__)

default_language = 'en'
language_urls = {
    'en': 'mysite.com',
    'fr': 'mysite.com/fr',
    'ru': 'mysite.ru',
    'by': 'mysite.ru/by',
}
languages = ','.join(language_urls.keys())


def get_language_by_request(request_host, request_path):
    '''
    Looking bad, but work.
    I cab't use request.view_args there,
    because this can't detect language for 404 pages
    like mysite.com/fr/unknown-page
    '''
    request_host_path = request_host + request_path
    request_paths = request_host_path.split('/', 2)
    if (len(request_paths) > 1 and request_paths[1] in language_urls.keys()):
        request_language_prefix = request_paths[1]
        return request_language_prefix
    for language, url in language_urls.items():
        host_prefix = url.split('/')
        if len(host_prefix) == 1:
            host, = host_prefix
            if request_host == host:
                return language
    return default_language


def get_language_url_parameter_value(language, request_host):
    host_prefix = language_urls[language]
    if host_prefix == request_host:
        return None
    return language


def get_redirection_url_by_request(request_host, request_path, request_url):
    '''
    Looking bad, but work.
    I cab't use request.view_args there,
    because this can't detect language for 404 pages
    like mysite.com/fr/unknown-page
    '''
    request_host_path = request_host + request_path
    request_paths = request_host_path.split('/', 2)
    request_language_prefix = None
    if (len(request_paths) > 1 and request_paths[1] in language_urls.keys()):
        request_language_prefix = request_paths[1]
    hosts = []
    for language, url in language_urls.items():
        host_prefix = url.split('/')
        if len(host_prefix) == 1:
            host, = host_prefix
            language_prefix = None
        else:
            host, language_prefix = host_prefix
        if request_host == host and request_language_prefix == language_prefix:
            return None
        hosts.append(host)
    if request_host not in hosts:
        return None
    if request_language_prefix:
        request_host_prefix = request_host + '/' + request_language_prefix
        host_prefix = language_urls[request_language_prefix]
        return request_url.replace(request_host_prefix, host_prefix)
    return None


@app.url_defaults
def set_language_in_url(endpoint, values):
    if '_lang' not in values and hasattr(g, 'language_url_value'):
        values['_lang'] = g.language_url_value


@app.url_value_preprocessor
def get_language_from_url(endpoint, values):
    g.language = get_language_by_request(request.host, request.path)
    g.language_url_value = get_language_url_parameter_value(g.language, request.host)
    if values and '_lang' in values:
        del values['_lang']


@app.before_request
def check_language_redirection():
    redirection_url = get_redirection_url_by_request(request.host, request.path, request.url)
    return redirect(redirection_url) if redirection_url else None


@app.route('/')
@app.route('/<any(%s):_lang>/' % languages)
def home():
    return render_template('home.html')


@app.route('/other/')
@app.route('/<any(%s):_lang>/other/' % languages)
def other():
    return render_template('other.html')

I don't use blueprints there because I also use flask-login and I can't set several login pages with different languages for each blueprint. For example if page required login, flask redirect me to login page and I must update language for this page. Also login pages can't be as mysite.com/login, mysite.com/fr/login and etc without several redirections.

UPD: I can't use request.view_args for detect language or redirection, because on this case I can't detect language for error pages as mysite.com/fr/wrong-page-there (can't detect endpoint and view_args). To avoid this problem I can use hask: add url rule as /<lang_code>/<path:path> and raise 404 error there.

Prearrange answered 19/2, 2013 at 13:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.