i18n and l10n
internacionalizácia (i18n) a lokalizácia (l10n) projektu (aplikácie)
Záznam z prednášky
Introduction
(slide) stále pracujeme na baterke
- baterka už svieti
- Dokonca vieme aj v prípade, že zariadenie baterkou nedisponuje, zobraziť správu a aplikáciu vypnúť. Dokonca to robíme už tesne po spustení aplikácie.
- Pred prístupom k baterke vieme dokonca overiť aj to, či máme povolený prístup ku kamere na OS Android.
- Dnes sa pozrieme na to, ako správne ovládnuť globálny trh s baterkami a na štruktúru nášho projektu.
I18n Stands for Internationalization
(slide) Dajme tomu, že máme s našou aplikáciou vyššie ciele, ako len urobiť baterku z telefónu - chceme ňou ovládnuť svetový trh bateriek ;) Aby sa nám to podarilo, rozhodne nesmieme podceniť otázku lokalizácie aplikácie pre použitie v iných krajinách. V našom prípade sa jedná o pomerne jednoduchý proces, pretože skončíme len pri lokalizovaní/preklade textov do jazyka cieľovej krajiny. Na čo všetko však nesmieme v prípade lokalizácie zabudnúť?
(slide) Pri prvom priblížení si môžeme myslieť, že otázka lokalizácie a internacionalizácie sa týka len prekladov. Nie je to však pravda. Lokalizácia a internacionalizácia so sebou totiž nesie ďalšie dôsledky, ako:
- iné jazyky môžu vyžadovať iné množstvo miesta na obrazovke (krátky reťazec v pôvodnom jazyku a dlhý reťazec v preklade)
- iný jazyk môže používať iný smer písma (RTL jazyky)
- v inej krajine sa môže používať iná abeceda a teda iné znaky
- formátovanie čísiel (oddeľovanie desatinných miest, tisícok), času a dátumu, meny
- používanie titulov pri menách
- formátovanie kontaktných informácií (telefónne čísla, adresy)
Na vyriešenie týchto problém samozrejme už existujú hotové riešenia. Napr. v linuxových/unixových riešeniach sa dlhodobo používa nástroj gettext, ktorý oddeľuje program od prekladu a elegantne rieši mnohé problémy spojené s lokalizáciou. Jeho podpora je v rozličných programovacích jazykoch, čo z neho robí univerzálny nástroj.
Iný prístup k tejto problematike má aj samotný systém Android. Je však jednotný pre všetky aplikácie napísané pre tento systém.
i18next Framewok
(slide) Odlišný prístup je však vo svete JavaScript-u. K dispozícii je veľké množstvo knižníc, pomocou ktorých je možné tento problém riešiť. Každý má samozrejme svoje vlastné špecifiká. My sa pozrieme konkrétne na rámec i18next. Jeho výhodou je, že sa dá použiť aj inde, ako len v prípade tvorby mobilných aplikácií pomocou rámca React Native. Jeho heslom je learn once - translate everywhere ;)
Installation
Nainštalujeme niekoľko balíkov:
$ npm install react-i18next i18next --save $ npm install react-native-localize --save
Poznámka
Pravdepodobne bude potrebné opäť projekt aj vyčistiť pomocou:
$ cd android $ ./gradlew clean $ cd .. $ npx react-native run-android
Configure i18next
Začneme podľa pokynov v Quick Start Guide: vytvoríme súbor
i18n.js
a upravíme ho do nasledovnej podoby:import i18n from "i18next"; import { initReactI18next } from "react-i18next"; // the translations const resources = { sk: { translation: { "Turn On": "Zasvietiť", "Turn Off": "Zhasnúť", } }; } i18n.use(initReactI18next) // passes i18n down to react-i18next .init({ , resourceslng: "en", // we do not use keys in form messages.welcome keySeparator: false, interpolation: { // react already safes from xss escapeValue: false }; }) export default i18n;
Translate your content
(slide) Rámec i18next poskytuje niekoľko spôsobov, ako je možné lokalizovať text. Pre naše riešenie použijeme hook
useTranslation()
, pomocou ktorého získame funkciut()
na prekladanie reťazcov:import { useTranslation } from 'react-i18next'; export default function App() { const { t, i18n } = useTranslation(); // code ... }
Preklad následne vykonáme použitím funkcie
t()
napr. na popisku tlačidla na rozsvietenie a zhasnutie baterky:<Button onPress={toggleState} title={isOn === true ? t('Turn Off') : t('Turn On')} />
Tu je možné vidieť, ako lokalizácia pomocou rámca i18next funguje. Preklad je reprezentovaný pomocou JSON objektu, kde sa ku každému kľúču viaže nejaký preklad. Týmto spôsobom pracuje množstvo ďalších lokalizačných rámcov pre jazyk JavaScript.
Rámec i18next však ponúka viac, ako napr. používanie menných priestorov na zoskupovanie prekladov pre konkrétne moduly aplikácie. Predvoleným menným priestorom je
translation
a min. jeden musí existovať. My však ako kľúče budeme používať celé texty, ktoré je potrebné preložiť. To nám dá tú výhodu, že v prípade neexistujúceho prekladu sa na mieste použíje priamo tento text. Tým pádom nemusíme vytvárať osobitne preklad predvoleného jazyka, ktorým bude angličtina.Vytvoríme teda preklad všetkých textov do slovenčiny v podobe JSON objektu:
{ "translation": { "Turn On": "Zasvietiť", "Turn Off": "Zhasnúť", "Missing Flashlight": "Chýba blesk", "No camera available. Go and buy a device with some " + "(or two) and come back later.": "Zariadeniu chýba " + "blesk. Kúp si najprv zariadenie s bleskom (alebo " + "dve) a potom to skús znova.", "Camera Permissions": "Povoliť kameru", "We require camera permissions to use the torch on the " + "back of your phone.": "Aby baterka svietila, je " + "potrebné povoliť kameru.", "Quit": "Ukončiť" } }
Každý reťazec v aplikácii zabalíme do funkcie
t()
.Ak teraz spustíme aplikáciu, nič sa nezmení. Ak však v konfigurácii modulu
i18next
zmeníme jazyk nask
, uvidíme jednotlivé texty preložené do slovenčiny. Ako však proces prepínania zautomatizovať tak, aby sa jazyk aplikácie zvolil na základe jazyka systému zariadenia?
Get the Current Locale
Na zistenie aktuálneho jazyka systému použijeme modul s názvom
react-native-localize
. V ňom pomocou funkciegetLocales()
získame zoznam preferovaných jazykov, kde ten prvý bude aktuálne používaný. Kód bude vyzerať takto:import * as RNLocalize from 'react-native-localize'; const deviceLanguage = RNLocalize.getLocales()[0].languageCode;
Poznámka
Pre získanie aktuálneho jazyka môžeme použiť aj
NativeModules
napr. takto (zdroj):import { NativeModules, Platform } from 'react-native'; const deviceLanguage = .OS === 'ios' Platform? NativeModules.SettingsManager.settings.AppleLocale || // iOS 13 .SettingsManager.settings.AppleLanguages[0] NativeModules: NativeModules.I18nManager.localeIdentifier; console.log(deviceLanguage); //en_US
Vďaka tomu môžeme upraviť inicializáciu objektu
i18n
, kde miesto jazyka napevno môžeme práve použiť premennúdeviceLanguage
alebo priamo získanie kódu jazyka z výsledku volaniagetLocales()
:i18n.use(initReactI18next) // passes i18n down to react-i18next .init({ resources: locales, fallbackLng: 'en', lng: RNLocalize.getLocales()[0].languageCode, // we do not use keys in form messages.welcome keySeparator: false, interpolation: { escapeValue: false, // react already safes from xss , }; })
Ak teraz aplikáciu spustíme, priamo po štarte bude použitý jazyk systému. Ak teda budeme experimentovať a pred spustením aplikácie jazyk zmeníme, uvidíme buď slovenčinu, ak vyberieme slovenský jazyk, alebo angličtinu, ak vyberieme ktorýkoľvek iný jazyk.
Language Change on the Fly
Ku zmene jazyka však môže dôjsť aj počas behu aplikácie. Ak sa pokúsime zmeniť jazyk systému pri spustenej aplikácii, k žiadnej zmene jazyka nedôjde. Potrebujeme ju totiž znova vypnúť a zapnúť, aby bol objekt
i18n
inicializovaný nanovo na základe aktuálneho nastavenia systému. Ako však zabezpečiť to, aby sa jazyk prepol automaticky, keď ho zmeníme v systéme?Tu nám pomôže nastaviť listener z modulu
react-native-localize
na udalosťchange
, ktorá nastane práve vtedy, keď k zmene jazyka dôjde. Následne jazyk v aplikácii zmeníme volaními18n.changeLanguage()
:.addEventListener('change', () => { RNLocalizeconst language = RNLocalize.getLocales()[0].languageCode; console.log(`>> language has been changed to ${language}`); .changeLanguage(language); i18n; })
Ak teraz aplikáciu vyskúšame, zmena jazyka sa prejaví okamžite po zmene jazyka v systéme.
Effective Resource Management
Pozrime sa však späť na to, ako vyzerá organizácia prekladu - nachádza sa vo vnútri súboru
i18n.js
. Aktuálne sme využili vlastnosťi18next
, že ak neexistuje preklad, použije sa predvolený reťazec, ktorý je parametrom funkciet()
.Preklad do slovenčiny má aktuálne 7 reťazcov. Aplikácie však majú bežne stovky až tisícky reťazcov, ktoré je potrebné preložiť. To znamená, že údržba všetkých jazykov v jednom súbore, ktorý súčasne obsahuje aj kód, je veľmi neefektívna a nepraktická. Rozumnejšie by bolo udržiavať každý jazyk v osobitnom súbore.
Vytvoríme preto osobitný priečinok s názvom
locales/
, ktorý bude pre každý jeden jazyk obsahovať samostatný súbor vo formáte JSON s prekladom všetkých reťazcov. V našom prípade to znamená, že v ňom vytvoríme len súborsk.json
, ktorého obsah bude prekladom všetkých reťazcov do slovenčiny:{ "translation": { "Turn On": "Zasvietiť", "Turn Off": "Zhasnúť", "Missing Flashlight": "Chýba blesk", "No camera available. Go and buy a device with some " + "(or two) and come back later.": "Zariadeniu chýba " + "blesk. Kúp si najprv zariadenie s bleskom (alebo dve) " + "a potom to skús znova.", "Camera Permissions": "Povoliť kameru", "We require camera permissions to use the torch on the " + "back of your phone.": "Aby baterka svietila, je " + "potrebné povoliť kameru.", "Quit": "Ukončiť" } }
Aby sa nám manažment prekladov robil jednoduchšie, tak vytvoríme z tohto priečinku modul. Vytvoríme teda súbor
index.js
, v ktorom načítame všetky preklady a exportneme ich von ako jeden objektlocales
:export const locales = { sk: require('./sk.json'), ; }
Následne už len upravíme pôvodný súbor
i18n.js
, v ktorom odstránime preklad do slovenčiny a zameníme ho za preklady získané z modululocales
:import i18n from 'i18next'; import {initReactI18next} from 'react-i18next'; import * as RNLocalize from 'react-native-localize'; import {locales} from './locales'; .addEventListener('change', () => { RNLocalizeconst language = RNLocalize.getLocales()[0].languageCode; .changeLanguage(language); i18nconsole.log(`>> language has been changed to ${language}`); ; }) i18n.use(initReactI18next) // passes i18n down to react-i18next .init({ resources: locales, fallbackLng: 'en', lng: RNLocalize.getLocales()[0].languageCode, // we do not use keys in form messages.welcome keySeparator: false, interpolation: { escapeValue: false, // react already safes from xss , }; }) export default i18n;
Tým pádom pridať nový jazyk znamená vytvoriť nový súbor v module
locales
a do exportovaného objektulocales
tohto modulu pridať riadok navyše, ktorý zabezpečí nahratie tohto jazyka.
Conclusion
Dnes sme sa pozreli na to, ako vieme zabezpečiť preklad UI do iných jazykov, kedy sa jazyk vyberie na základe jazyka systému. Tým sme aplikáciu pripravili pre nasadenie pre globálny trh ;)
Projekt nám začína rásť. Nabudúce sa pozrieme na to, ako projekt udržiavať vo vhodnej štruktúre, aby sme sa v ňom nestratili, a aby pridávanie nových vlastností, resp. nových komponentov nebolo bolestivé.