Kansainvälistäminen ja lokalisointi
Web-sovelluksen käyttäjät puhuvat eri kieliä, asuvat eri aikavyöhykkeillä ja ovat tottuneet erilaisiin päivämäärien, numeroiden ja nimien esitystapoihin. Tällä luennolla käydään läpi, miten Flask-sovellus rakennetaan niin, että se toimii oikein kaikille. Painopiste on aikojen käsittelyssä, koska siinä virheitä syntyy eniten. Luennon asioita voi kokeilla esimerkkisovelluksessa.
Käsitteet
Kansainvälistäminen (engl. internationalization, lyhennettynä i18n: i, 18 kirjainta, n) tarkoittaa sovelluksen rakentamista niin, että sen voi sovittaa eri kielille ja alueille muuttamatta ohjelmakoodia. Lokalisointi eli kotoistus (localization, l10n) on itse sovittaminen: käännökset, päivämäärien ja numeroiden muotoilut sekä kieli- ja aluekohtainen sisältö. Ensin sovellus kansainvälistetään, minkä jälkeen se voidaan lokalisoida mille tahansa kielelle ilman ohjelmointityötä.
Kieli ilmaistaan BCP 47
-kielitunnisteella. W3C:n ohjeen kultainen sääntö on pitää tunniste
mahdollisimman lyhyenä: useimmiten riittää pelkkä kielikoodi, kuten fi,
sv tai en. Alue tai muu tarkenne lisätään vain, kun se
erottaa jotakin olennaista. Esimerkiksi brittienglannissa (en-GB)
päivämäärä kirjoitetaan 22/09/2026, amerikanenglannissa (en-US) 9/22/26.
Jos sovellus ei tee tällaista eroa, en riittää.
Pythonin kirjastot käyttävät kielen ja alueen välissä usein alaviivaa
(en_GB), HTML ja HTTP yhdysmerkkiä (en-GB).
Ajat ja aikavyöhykkeet
Aikaleima, paikallinen aika ja päivämäärä
Sana "aika" tarkoittaa ohjelmoinnissa kolmea eri asiaa, ja ne on pidettävä erillään:
- Aikaleima (engl. timestamp) on yksiselitteinen kohta aikajanalla, esimerkiksi suunnistajan rastileimauksen aika. Se on sama kaikkialla maailmassa, vaikka kello näyttää eri paikoissa eri aikaa.
- Paikallinen aika on kellonaika tietyssä paikassa, esimerkiksi "kilpailu alkaa klo 9.00". Ilman tietoa paikasta se ei ole yksiselitteinen.
- Päivämäärä ilman kellonaikaa, esimerkiksi syntymäpäivä, ei ole aikaleima lainkaan, eikä sitä pidä muuntaa aikavyöhykkeestä toiseen.
Pythonissa aikaleimaa vastaa aikavyöhykkeellinen datetime,
paikallista aikaa aikavyöhykkeetön datetime ja päivämäärää
date. Pythonin englanninkielisessä dokumentaatiossa niistä käytetään
nimityksiä aware ja naive. Perussääntö on:
Tallenna aikaleimat tietokantaan UTC-aikana aikavyöhyketiedon kanssa ja muunna ne käyttäjän tai tapahtuman aikavyöhykkeeseen vasta näytettäessä. Näin vertailut ja laskutoimitukset ovat aina oikein, eikä kesäaika sotke niitä.
UTC (koordinoitu yleisaika, engl. Coordinated Universal Time) on atomikelloihin perustuva maailmanlaajuinen aikastandardi. Siinä ei ole kesäaikaa, ja kaikki aikavyöhykkeet ilmaistaan poikkeamana siitä: Suomen aika on talvella UTC+2 ja kesällä UTC+3.
GMT (Greenwichin aika, Greenwich Mean Time) on UTC:n edeltäjä: Lontoon Greenwichin observatorion kautta kulkevan pituuspiirin aurinkoon perustuva aika. Käytännössä se on sama kuin UTC, ja sitä käytetään edelleen yleisesti UTC:n synonyymina — esimerkiksi HTTP-otsakkeissa, joissa merkintä GMT tarkoittaa UTC-aikaa. Nykyään GMT on myös Britannian talviajan nimi.
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
SUOMI = ZoneInfo("Europe/Helsinki")
aikaleima = datetime.now(timezone.utc) # tallennetaan tietokantaan
print(aikaleima.astimezone(SUOMI).isoformat(timespec="seconds"))
# 2026-09-22T11:05:31+03:00
Aikavyöhykkeettömään aikaan liittyy ansoja. Esimerkiksi Pythonin
Firestore-kirjasto tulkitsee aikavyöhykkeettömän ajan UTC-ajaksi ja palauttaa sen
UTC-merkittynä. Suomen aikana tallennettu "klo 9.00" siirtyy kahdella tai kolmella
tunnilla heti, kun sitä yrittää muuntaa Suomen aikaan. Toinen ansa on palvelimen
kello: datetime.now() ilman aikavyöhykettä palauttaa palvelimen
paikallisen ajan, ja Cloud Runin palvelimet toimivat UTC-ajassa.
Kenen aikavyöhyke?
Suunnistuskilpailun alkuaika kuuluu kilpailupaikan aikavyöhykkeeseen, ei
käyttäjän. Jos kilpailu pidetään Norjassa, se alkaa Norjan aikaa, katsoi sitä
kuka tahansa ja mistä tahansa. Tallenna siksi tapahtumaan sen aikavyöhyke nimenä,
esimerkiksi Europe/Helsinki, eikä pelkkää poikkeamaa
+03:00: poikkeama vaihtuu kesäajan mukana, nimi ei.
Aikavyöhykkeiden säännöt tulevat IANA:n
aikavyöhyketietokannasta, ja niitä muutetaan useita kertoja vuodessa jossain
päin maailmaa. zoneinfo käyttää käyttöjärjestelmän tietokantaa tai
tzdata-pakettia. Lisää tzdata sovelluksen riippuvuuksiin,
jotta säännöt löytyvät myös kontista ja pysyvät ajan tasalla.
Kesäaika
Kesäajan vaihtuminen tekee paikallisesta ajasta hankalan. Suomessa kellot siirretään keväällä klo 3.00 tunnilla eteenpäin ja syksyllä klo 4.00 tunnilla taaksepäin. Vuonna 2026 tästä seuraa:
| Paikallinen aika | Mitä tapahtuu |
|---|---|
| 29.3.2026 klo 3.30 | Aikaa ei ole olemassa: kello hyppää 3.00:sta 4.00:aan. Python hyväksyy sen silti varoittamatta. |
| 25.10.2026 klo 3.30 | Aika esiintyy kahdesti, ensin kesäaikana (+03:00) ja sitten talviaikana
(+02:00). Pythonissa kerrat erotetaan fold-attribuutilla. |
| 24.10.2026 klo 12 + 1 vuorokausi | timedelta(days=1) tuottaa 25.10. klo 12, mutta todellisuudessa
väliin jää 25 tuntia. |
Kun käyttäjä syöttää paikallisen ajan, sen kelvollisuuden voi tarkistaa muuntamalla ajan UTC:ksi ja takaisin:
from datetime import datetime, timezone
def tarkista_paikallinen_aika(teksti, vyohyke):
paikallinen = datetime.fromisoformat(teksti) # aikavyöhykkeetön
aika = paikallinen.replace(tzinfo=vyohyke)
# Olematon aika ei säily muunnoksessa UTC:ksi ja takaisin.
if aika.astimezone(timezone.utc).astimezone(vyohyke).replace(tzinfo=None) != paikallinen:
return "ei ole olemassa"
# Kahdesti esiintyvällä ajalla on kaksi eri UTC-poikkeamaa.
if aika.utcoffset() != aika.replace(fold=1).utcoffset():
return "esiintyy kahdesti"
return "kelvollinen"
tarkista_paikallinen_aika("2026-03-29T03:30", SUOMI) # 'ei ole olemassa'
tarkista_paikallinen_aika("2026-10-25T03:30", SUOMI) # 'esiintyy kahdesti'
ISO 8601 ja ajat HTML:ssä
Koneiden välillä ajat välitetään ISO 8601
-muodossa tai sen tiukemmassa RFC 3339 -profiilissa:
2026-09-22T11:05:31+03:00. Muoto on yksiselitteinen, lajittuu
merkkijonona oikeaan järjestykseen ja on jokaisen ohjelmointikielen
jäsennettävissä.
HTML:ssä time-elementti yhdistää ihmiselle näytettävän ja koneelle
tarkoitetun muodon. Selain, hakukone ja ruudunlukija lukevat
datetime-attribuutin, ja käyttäjä näkee sisällön:
<p>Päivitetty <time datetime="2026-09-22T11:05:31+03:00">22.9.2026 klo 11.05</time></p>
HTTP-otsakkeissa, kuten Last-Modified ja Expires, ajat
ovat aina GMT-aikaa omassa muodossaan: Tue, 22 Sep 2026 08:05:31 GMT.
Werkzeugin http_date tuottaa oikean muodon.
Ajat lomakkeilla
Kenttätyypit date ja datetime-local näyttävät ajan
käyttäjän selaimen asetusten mukaan, mutta lähettävät arvon aina ISO-muodossa
(2026-09-22 tai 2026-09-22T09:00). Palvelimen ei siis
tarvitse tulkita aluekohtaisia muotoja. Sekunnit saa mukaan valitsimella
step="1".
datetime-local-kentässä ei ole aikavyöhykettä. Palvelimen on
päätettävä, minkä aikavyöhykkeen ajasta on kyse — yleensä tapahtuman:
paikallinen = datetime.fromisoformat(request.form["alkuaika"])
alkuaika = paikallinen.replace(tzinfo=ZoneInfo(kilpailu["aikavyohyke"]))
tallennettava = alkuaika.astimezone(timezone.utc)
Tekstikenttään syötettyä päivämäärää ei voi tulkita tietämättä käyttäjän aluetta:
1.2.2026 on Suomessa 1. helmikuuta, ja Yhdysvalloissa
1/2/2026 on 2. tammikuuta. Babel-kirjaston
parse_date tulkitsee päivämäärän annetun alueen mukaan, mutta varmin
ratkaisu on käyttää date-kenttää.
Viikot ja kalenteri
Suomessa viikko alkaa maanantaina ja viikot
numeroidaan ISO 8601:n mukaan. Vuoden
ensimmäinen viikko on se, jolla on vuoden ensimmäinen torstai, joten vuoden
vaihteessa viikko voi kuulua edelliseen vuoteen:
date(2027, 1, 1).isocalendar() on vuoden 2026 viikko 53. Monessa
maassa viikko alkaa sunnuntaina eikä viikkonumeroita käytetä lainkaan, joten
kalenterinäkymät on sovitettava alueen mukaan.
Unicode, vertailu ja lajittelu
Normalisointi
Sama näkyvä merkkijono voidaan esittää Unicodessa usealla tavalla. "Mäki" voi olla neljä koodipistettä, joista yksi on ä, tai viisi, joissa a ja sen päälle tulevat pisteet (treema) ovat erillisiä merkkejä. Ne näyttävät samalta, mutta eivät ole Pythonissa yhtä suuria. Ennen vertailua ja tallentamista merkkijonot kannattaa normalisoida NFC-muotoon.
Unicode määrittelee neljä normaalimuotoa. Tärkeimmät ovat NFC (Normalization Form Canonical Composition), jossa merkit yhdistetään valmiiksi kootuiksi merkeiksi aina kun mahdollista (a + treema → ä), ja NFD, jossa ne päinvastoin puretaan osiin. Näppäimistöltä kirjoitettu teksti on yleensä NFC-muodossa, ja W3C suosittelee sitä webiin. Hajotettua muotoa tulee vastaan esimerkiksi tekstistä, joka on kopioitu PDF-tiedostosta tai joissakin järjestelmissä tiedostonimistä.
Alla olevassa esimerkissä toisen merkkijonon ä on kirjoitettu kahdella koodipisteellä: kirjaimena a ja sen perään yhdistyvänä treemana (U+0308). Yhdistyvä merkki (engl. combining character) ei vie omaa paikkaa rivillä, vaan se piirretään edeltävän merkin päälle. Näin mikä tahansa kirjain voidaan varustaa millä tahansa tarkkeella, myös sellaisella, jolle Unicodessa ei ole valmista yhdistelmämerkkiä. Yhdistyvää treemaa ei pidä sekoittaa itsenäiseen treemaan ¨ (U+00A8), joka on tavallinen, oman paikkansa vievä merkki eikä yhdisty mihinkään.
import unicodedata
a = "M\u00e4ki" # ä yhtenä merkkinä
b = "Ma\u0308ki" # a + yhdistyvä treema
print(a == b, len(a), len(b)) # False 4 5
print(unicodedata.normalize("NFC", b) == a) # True
Tarkkeet
Tarke eli diakriittinen merkki on kirjaimeen liitettävä pieni merkki, joka muuttaa kirjaimen äännettä tai erottaa sen toisesta kirjaimesta. Tarkkeita ovat esimerkiksi treema (ä, ö, ü), rengas (å), akuutti (é), gravis (è), sirkumfleksi (ê), hattu (š, ž), tilde (ñ) ja sedilji (ç). Unicodessa tarkkeet esitetään yhdistyvinä merkkeinä, joten tarkkeellinen kirjain voidaan purkaa NFD-muotoon perusmerkiksi ja tarkkeeksi.
Se, onko tarkkeellinen kirjain oma kirjaimensa vai toisen kirjaimen muunnelma, riippuu kielestä. Suomessa ja ruotsissa å, ä ja ö ovat aakkoston omia kirjaimia, jotka lajitellaan z:n jälkeen. Saksassa ä on a:n muunnelma, ja se lajitellaan a:n kohdalle. Siksi myös tarkkeiden ohittaminen haussa on kielikysymys: englanninkieliselle käyttäjälle on luontevaa, että haku "aland" löytää Ålandin, mutta suomalaiselle se on yhtä outoa kuin se, että "sana" löytäisi sanan "sänä".
Jos tarkkeet halutaan ohittaa, merkkijono puretaan NFD-muotoon ja yhdistyvät merkit pudotetaan pois:
def ilman_tarkkeita(teksti):
hajotettu = unicodedata.normalize("NFD", teksti)
return "".join(m for m in hajotettu if not unicodedata.combining(m))
ilman_tarkkeita("Åland") # 'Aland'
ilman_tarkkeita("crème brûlée") # 'creme brulee'
ilman_tarkkeita("Øresund") # 'Øresund' — ei muutu
ilman_tarkkeita("Straße") # 'Straße' — ei muutu
Kaksi viimeistä esimerkkiä näyttävät tavan rajat. Ø:n vinoviiva näyttää tarkkeelta, mutta Unicode määrittelee Ø:n omaksi kirjaimekseen, jota ei voi purkaa o-kirjaimeksi ja yhdistyväksi merkiksi. Saksan ß puolestaan ei ole tarkkeellinen kirjain lainkaan, vaan alun perin kahden s-kirjaimen yhdistelmä. Tarkkeiden poistaminen ei siis tee hausta yleispätevästi "väljää"; siihen tarvittaisiin kielikohtaiset säännöt, jotka ICU-kirjasto tuntee.
Kirjainkoosta riippumaton vertailu
Kirjainkoosta riippumattomaan vertailuun kuuluu casefold(), ei
lower(). Ero näkyy kirjaimissa, joiden kirjainkoosta riippumaton
vastine eroaa pienestä kirjaimesta: "Straße".lower() on "straße", mutta
"Straße".casefold() on "strasse", jolloin se vastaa
kirjoitusasua "STRASSE". Sähköpostiosoitteiden, käyttäjänimien ja hakujen vertailuun
tarvitaan sekä normalisointi että casefold().
Kielikohtainen lajittelu
Aakkosjärjestys riippuu kielestä. Pythonin sorted() lajittelee
merkkien Unicode-koodipisteiden mukaan, mikä ei yleensä vastaa minkään kielen
aakkosjärjestystä:
| Tapa | Järjestys |
|---|---|
sorted(nimet) |
Zulu, aalto, zeta, Ähtäri, Ångström, Öljy |
sorted(nimet, key=str.casefold) |
aalto, zeta, Zulu, Ähtäri, Ångström, Öljy |
| suomi (ICU) | aalto, zeta, Zulu, Ångström, Ähtäri, Öljy |
| saksa (ICU) | aalto, Ähtäri, Ångström, Öljy, zeta, Zulu |
casefold() korjaa kirjainkoon mutta ei kielen sääntöjä: suomessa å
tulee ennen ä:tä, ja saksassa ä lajitellaan a:n kohdalle. Oikean kielikohtaisen
lajittelun tekee Unicoden lajittelualgoritmin toteuttava ICU-kirjasto, jota
Pythonissa käytetään PyICU-paketilla:
from icu import Collator, Locale
suomi = Collator.createInstance(Locale("fi_FI"))
nimet.sort(key=suomi.getSortKey)
Pythonin oma locale.strxfrm tekee saman, mutta se käyttää koko
prosessin yhteistä aluekohtaista asetusta. Web-palvelimessa, joka palvelee
samanaikaisesti erikielisiä käyttäjiä useissa säikeissä, sitä ei voi käyttää.
SQLiten omat vertailufunktiot käsiteltiin
tietokantaluennolla,
ja Firestore lajittelee aina koodipisteiden mukaan.
Nimet "Iltarastit 2024" ja "Iltarastit [peruttu]" lajittuvat ICU:n ja Pythonin mukaan eri järjestykseen, koska ICU asettaa välimerkit numeroiden edelle ja Python päinvastoin. Kumpikaan ei ole väärin. Jos järjestyksellä on väliä, se on määriteltävä vaatimuksissa tarkasti — "aakkosjärjestys" ei riitä.
Numeroiden ja päivämäärien muotoilu
Numerot, rahamäärät ja päivämäärät kirjoitetaan eri alueilla eri tavoin.
Babel-kirjastossa ovat Unicoden
CLDR-tietokannan muotoilusäännöt kaikille alueille.
CLDR (Common Locale Data Repository)
on Unicode-konsortion ylläpitämä avoin tietokanta, johon on koottu kielten ja
alueiden käytännöt: päivämäärien, numeroiden ja rahamäärien muodot, kuukausien ja
viikonpäivien nimet, monikkosäännöt ja kielten nimet eri kielillä. Samaa tietokantaa
käyttävät muun muassa ICU, selainten Intl-rajapinta ja
käyttöjärjestelmät, joten muotoilut ovat samat ohjelmointikielestä riippumatta.
from datetime import date
from babel.dates import format_date
from babel.numbers import format_currency, format_decimal
format_decimal(1234.5, locale="fi") # '1 234,5'
format_currency(1234.5, "EUR", locale="fi") # '1 234,50 €'
format_date(date(2026, 9, 22), format="long", locale="fi") # '22. syyskuuta 2026'
| Alue | Luku | Rahamäärä | Pitkä päivämäärä | Lyhyt päivämäärä |
|---|---|---|---|---|
fi | 1 234,5 | 1 234,50 € | 22. syyskuuta 2026 | 22.9.2026 |
sv_FI | 1 234,5 | 1 234,50 € | 22 september 2026 | 2026-09-22 |
en_GB | 1,234.5 | €1,234.50 | 22 September 2026 | 22/09/2026 |
en_US | 1,234.5 | €1,234.50 | September 22, 2026 | 9/22/26 |
de | 1.234,5 | 1.234,50 € | 22. September 2026 | 22.09.26 |
Suomalaisissa muodoissa tuhaterotin ja euromerkin edessä oleva väli ovat
sitovia välilyöntejä (U+00A0), jotta luku ei katkea kahdelle riville. Päivämäärän ja
kellonajan voi muotoilla samalla kertaa suoraan halutulle aikavyöhykkeelle:
format_datetime(aikaleima, tzinfo=SUOMI, locale="fi").
strftime("%A") ja locale.setlocale() käyttävät koko
prosessin yhteistä asetusta. Jos yksi pyyntö vaihtaa sen ruotsiksi, samaan aikaan
käsiteltävä suomenkielinen pyyntö saa ruotsinkieliset viikonpäivät. Babelin
funktioille alue annetaan jokaisella kutsulla erikseen.
Käännökset
gettext ja Flask-Babel
Käyttöliittymän tekstit käännetään GNU
gettext -järjestelmällä. Se on GNU-projektin 1990-luvulta peräisin oleva
käännösjärjestelmä, jota käyttävät useimmat avoimen lähdekoodin ohjelmat.
Käännökset kirjoitetaan tekstimuotoisiin .po-tiedostoihin
(Portable Object), jotka käännetään ohjelman luettavaan binäärimuotoon
.mo (Machine Object). Pythonin vakiokirjastossa on
gettext-moduuli, ja Flaskissa sitä käytetään
Flask-Babel-laajennuksella.
Käännettävät merkkijonot kirjoitetaan koodiin alkuperäisellä kielellä ja
merkitään funktiolla _():
from flask import Flask, request, session
from flask_babel import Babel, gettext as _
KIELET = ["fi", "en"]
def valitse_kieli():
kieli = session.get("kieli") # käyttäjän oma valinta ensin
if kieli in KIELET:
return kieli
return request.accept_languages.best_match(KIELET) or "fi"
app = Flask(__name__)
app.config["BABEL_DEFAULT_LOCALE"] = "fi"
app.config["BABEL_DEFAULT_TIMEZONE"] = "Europe/Helsinki"
babel = Babel(app, locale_selector=valitse_kieli)
Templateissa samat funktiot ovat valmiina käytössä. Muuttujat annetaan nimettyinä paikkamerkkeinä:
<h1>{{ _('Tulokset') }}</h1>
<p>{{ _('Kilpailu %(nimi)s alkaa %(aika)s.', nimi=k.nimi, aika=k.alkuaika) }}</p>
<p>{{ ngettext('%(num)d joukkue', '%(num)d joukkuetta', maara) }}</p>
Käännösten työnkulku
Käännökset kerätään ja käännetään pybabel-työkalulla. Tiedosto
babel.cfg kertoo, mistä merkkijonoja etsitään; XHTML-templatet on
lisättävä siihen erikseen:
# babel.cfg
[python: **.py]
[jinja2: **/templates/**.xhtml]
pybabel extract -F babel.cfg -k _l -o messages.pot . # kerää merkkijonot
pybabel init -i messages.pot -d translations -l en # uusi kieli (kerran)
pybabel update -i messages.pot -d translations # päivitä muutosten jälkeen
pybabel compile -d translations # käännä .po-tiedostot .mo-muotoon
Kääntäjä kirjoittaa käännökset .po-tiedostoon. Tiedosto on tavallista
tekstiä, jota voi muokata tekstieditorilla tai käännöstyökalulla:
msgid "Kilpailu %(nimi)s alkaa %(aika)s."
msgstr "The %(nimi)s competition starts at %(aika)s."
msgid "%(num)d joukkue"
msgid_plural "%(num)d joukkuetta"
msgstr[0] "%(num)d team"
msgstr[1] "%(num)d teams"
Hyvin käännettävä merkkijono
- Älä kokoa lauseita paloista.
_('Kilpailu') + ' ' + nimi + ' ' + _('alkaa')ei käänny, koska sanajärjestys vaihtelee kielittäin. Käännettävässä merkkijonossa on oltava kokonainen lause paikkamerkkeineen, jolloin kääntäjä voi siirtää ne oikeille paikoilleen. - Monikkomuodot tehdään
ngettext-funktiolla. Suomessa ja englannissa muotoja on kaksi, mutta esimerkiksi arabiassa kuusi. Ehtoif n == 1ei siksi riitä, vaan valinta on jätettävä kielen omille säännöille, jotka ovat.po-tiedostonPlural-Forms-otsakkeessa. - Asiayhteys annetaan
pgettext-funktiolla. "Sarja" tarkoittaa suunnistuksen tulospalvelussa kilpailusarjaa, mutta ilman asiayhteyttä kääntäjä ei tiedä, onko kyse sarjasta vai esimerkiksi televisiosarjasta:pgettext("kilpailu", "Sarja"). - Moduulitasolla määritellyt tekstit, kuten lomakkeiden
otsikot, merkitään
lazy_gettext-funktiolla, jolloin ne käännetään vasta käytettäessä. Funktion tavallinen lyhenne on_l. Siksi yllä olevassaextract-komennossa on valitsin-k _l.
Käyttöliittymä ja data
gettext kääntää vain ohjelmaan kirjoitetut tekstit. Tietokannan sisältö, kuten
sarjojen nimet, kilpailujen kuvaukset tai tuotteiden nimet, tarvitsee oman
ratkaisunsa: esimerkiksi Firestore-dokumentissa kentän
{"fi": "Pääruoka", "en": "Main course"} tai relaatiotietokannassa
erillisen käännöstaulun. Joukkueiden ja henkilöiden nimiä ei käännetä lainkaan.
Kielen valinta
Selaimen toive ja käyttäjän valinta
Selain kertoo käyttäjän kielitoiveet Accept-Language-otsakkeessa
laatuarvoineen, esimerkiksi sv-FI,sv;q=0.9,en;q=0.8,fi;q=0.7. Werkzeug
valitsee niistä parhaan tarjolla olevan: jos sovellus tarjoaa suomea ja englantia,
request.accept_languages.best_match(["fi", "en"]) palauttaa tässä
"en", koska englannin laatuarvo on suomea suurempi.
Otsake on hyvä oletus, mutta käyttäjän oma valinta on aina ensisijainen. Selaimen kieliasetus voi olla jonkun toisen tekemä, tai käyttäjä voi haluta lukea sivua toisella kielellä. Kielivalinta näytetään sivulla kunkin kielen omalla nimellä: "Suomi", "English", "Svenska" — ei lippuina, koska lippu kuvaa valtiota eikä kieltä.
Kieli osoitteessa
Paras paikka kielelle on osoite: /fi/kilpailu/42 ja
/en/kilpailu/42. Silloin jaettu linkki näyttää saman kielen, jolla
se jaettiin, hakukoneet löytävät kieliversiot ja välimuisti toimii ilman
lisätoimia. Flaskissa kieli poimitaan osoitteesta
URL-prosessoreilla,
jolloin sitä ei tarvitse käsitellä jokaisessa näkymässä erikseen:
from flask import abort, g, redirect, request, url_for
@app.url_value_preprocessor
def poimi_kieli(endpoint, arvot):
if arvot and "kieli" in arvot:
g.kieli = arvot.pop("kieli")
if g.kieli not in KIELET:
abort(404)
@app.url_defaults
def lisaa_kieli(endpoint, arvot):
# url_for lisää nykyisen kielen automaattisesti kaikkiin osoitteisiin
if "kieli" not in arvot and app.url_map.is_endpoint_expecting(endpoint, "kieli"):
arvot["kieli"] = g.get("kieli", "fi")
@app.route("/")
def juuri():
kieli = request.accept_languages.best_match(KIELET) or "fi"
return redirect(url_for("etusivu", kieli=kieli), code=303)
@app.route("/<kieli>/kilpailu/<kid>")
def kilpailu(kid):
...
Flask-Babelin kielivalinta palauttaa silloin yksinkertaisesti
g.get("kieli", "fi").
Kieli ja välimuisti
Jos kieli valitaan Accept-Language-otsakkeen perusteella, sama osoite
palauttaa eri käyttäjille erikielisen sivun. Silloin:
- vastaukseen on lisättävä otsake
Vary: Accept-Language, jotta selaimet ja välityspalvelimet eivät tarjoa yhden käyttäjän kieliversiota toiselle - sovelluksen oman välimuistin avaimeen on lisättävä kieli. Flask-Cachingin
@cache.cachedkäyttää oletuksena pelkkää polkua, joten ensimmäisen käyttäjän kieli näytettäisiin kaikille. Sama virhe syntyy, jos sivulla näytetään kirjautuneen käyttäjän nimi.
Kun kieli on osoitteessa, kumpaakaan ongelmaa ei synny. Vielä parempi on tallentaa välimuistiin kielestä riippumattomat tiedot, kuten tulosten laskennan tulos, ja muodostaa sivu kielen mukaan jokaisella pyynnöllä.
Kieli merkkauksessa
XHTML-sivulla kieli merkitään sekä lang- että
xml:lang-attribuuttiin. Kielimerkintä ei ole vain muodollisuus:
ruudunlukija valitsee sen perusteella ääntämyksen, ja selain tavutuksen ja
lainausmerkit. Flask-Babelin get_locale() palauttaa muodon
en_GB, joten alaviiva on muutettava yhdysmerkiksi. Sen voi tehdä
kerran kaikille templateille:
from flask_babel import get_locale
@app.context_processor
def sivun_kieli():
return {"kieli": str(get_locale()).replace("_", "-")}
<html xmlns="http://www.w3.org/1999/xhtml" lang="{{ kieli }}" xml:lang="{{ kieli }}">
...
<link rel="alternate" hreflang="en" href="https://…/en/kilpailu/42" />
...
<p>Joukkueen nimi on <span lang="en" xml:lang="en">Dynamic Duo</span>.</p>
Tekstin sisäiset kielivaihdokset merkitään samoilla attribuuteilla, ja
hreflang-linkit kertovat hakukoneille sivun muista kieliversioista.
Kts. W3C:n ohje Declaring
language in HTML.
Käyttöliittymä ja syötteet
- Tekstin pituus vaihtelee. Käännös voi olla huomattavasti alkuperäistä pidempi; saksankielinen teksti on usein kolmanneksen englantia pidempää. Kiinteät leveydet ja pikseleinä annetut koot rikkoutuvat, kun taas rem-yksiköihin ja joustaviin asetteluihin perustuva CSS sopeutuu.
- Oikealta vasemmalle kirjoitettavat kielet, kuten arabia ja
heprea, merkitään attribuutilla
dir="rtl". CSS:ssä kannattaa käyttää loogisia ominaisuuksia, esimerkiksimargin-inline-starteikämargin-left, jolloin sama tyylitiedosto toimii kumpaankin suuntaan. - Nimet ovat eri kulttuureissa erilaisia. Kaikilla ei ole etu- ja sukunimeä, sukunimi ei aina ole viimeisenä ja nimessä voi olla heittomerkkejä, yhdysmerkkejä ja välilyöntejä. Validointi ei saa hylätä ääkkösiä. Kts. Personal names around the world.
- Osoitteet ja puhelinnumerot vaihtelevat maittain. Puhelinnumero
kannattaa tallentaa kansainvälisessä E.164-muodossa.
Se on YK:n alaisen Kansainvälisen televiestintäliiton (ITU) suositus, jonka
mukaan numero kirjoitetaan plusmerkillä alkavana maatunnuksena ja kansallisena
numerona ilman etunollaa, välilyöntejä ja muita erottimia, enintään 15 numerolla:
suomalainen numero 040 123 4567 on
+358401234567. Myös postinumeron muoto riippuu maasta. - Tekstiä ei kirjoiteta kuviin, koska kuvia ei voi kääntää gettextillä.
Testaaminen
- Pseudolokalisointi korvaa käyttöliittymän tekstit pidennetyillä ja aksentoiduilla versioilla, esimerkiksi "Tulokset" → "[Ŧüłöķşëţ ẋẋẋ]". Ilman oikeaa käännöstä näkee heti, mitkä tekstit on unohtunut merkitä käännettäviksi ja mitkä elementit ovat liian kapeita.
- Aikavyöhykkeet testataan ajamalla sovellus toisella
aikavyöhykkeellä:
TZ=America/New_York python3 main.py. Jos jokin muuttuu, sovellus nojaa jossain palvelimen paikalliseen aikaan. - Kesäajan rajat kannattaa testata erikseen: 29.3. ja 25.10. klo 3.00–4.00 sekä vuorokauden lisäys niiden yli.
- Lajittelu ja vertailu testataan aineistolla, jossa on ääkkösiä, eri kirjainkokoja, samannäköisiä normalisoimattomia merkkijonoja ja erikoismerkkejä.
Esimerkkisovellus
Kansainvälinen suunnistuskalenteri on yhden sivun Flask-sovellus, joka kokoaa luennon tärkeimmät asiat. Se luettelee kuvitteellisia suunnistustapahtumia eri maista ja aikavyöhykkeiltä. Käyttöliittymä on suomeksi, ruotsiksi, englanniksi ja saksaksi, ja juuriosoite valitsee kielen selaimen asetusten mukaan. Kokeile esimerkiksi:
- Vaihda kieltä sivun yläreunasta ja katso, miten päivämäärät, rahamäärät, osallistujamäärien monikkomuodot ja maiden nimet muuttuvat.
- Valitse aikavyöhykkeeksi
Asia/TokyotaiAmerica/Los_Angeles. Tapahtumapaikan aika pysyy samana, mutta valitun aikavyöhykkeen aika ja joskus päivämääräkin muuttuvat. Syysyön rastien alkamisaika esiintyy kesäajan päättyessä kahdesti. - Hae nimillä "ÄNGELHOLM" ja "strasse", jotka löytyvät normalisoinnin ja
casefold()-kutsun ansiosta. Hae nimellä "aland" sekä tarkkeet ohittaen että ilman, ja nimellä "oresund", joka ei löydy lainkaan. - Vertaa sivun alareunassa Pythonin
sorted()-järjestystä ja kielikohtaista järjestystä suomeksi ja saksaksi.
Palvelimella ei ole PyICU-kirjastoa, joten sovellus lajittelee yksinkertaistetuilla säännöillä, jotka kattavat sovelluksen neljä kieltä. Sivu kertoo tämän lajitteluosiossa.
Lisätietoa
- W3C Internationalization — kattava kokoelma artikkeleita web-sovellusten kansainvälistämisestä
- Flask-Babel ja Babel
- zoneinfo ja
datetime —
aikavyöhykkeet ja
fold-attribuutti - MDN: <input type="datetime-local">
- Unicode Normalization Forms
Käyttäjien kommentit