i18n a l10n

internacionalizácia (i18n) a lokalizácia (l10n) projektu (aplikácie), rámec i18next, zistenie aktuálneho jazyka, zmena jazyka aplikácie, efektívna správa prekladov

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úť?

    I18n (zdroj)
  • (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:

    Different Aspects of i18n and l10n [@y20]
    • 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)
    • a iné
  • Na vyriešenie týchto problémov už samozrejme 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 bol však zvolený v systéme 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 aj vo svete JavaScript-u. K dispozícii je veľké množstvo knižníc a rámcov, 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 ;)

    i18next [@y17]

Inštalácia rámca i18next

  • Nainštalujeme niekoľko balíkov:

    $ yarn add react-i18next i18next
    $ yarn add react-native-localize

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({
        resources,
        fallbackLng: "en",
        lng: "en",
        compatibilityJSON: 'v3'
      });
    
      export default i18n;
  • Nakoniec už len importneme vytvorený modul. Môžeme to urobiť rovno v súbore index.js, keďže len potrebujeme, aby prebehla inicializácia:

    import App from './App';
    import './i18n';
    
    export default App;

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 funkciu t() 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 na sk, 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 funkcie getLocales() 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;
  • 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 volania getLocales():

    i18n
      .use(initReactI18next) // passes i18n down to react-i18next
      .init({
        resources: locales,
        fallbackLng: 'en',
        lng: RNLocalize.getLocales()[0].languageCode,
        compatibilityJSON: 'v3'
      });
  • 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ím i18n.changeLanguage():

    RNLocalize.addEventListener('change', () => {
      const language = RNLocalize.getLocales()[0].languageCode;
      console.log(`>> language has been changed to ${language}`);
      i18n.changeLanguage(language);
    });
  • 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, ktorá sa aktuálne nachádza 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 funkcie t().

  • 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úbor sk.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 objekt locales:

    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 modulu locales:

    import i18n from 'i18next';
    import {initReactI18next} from 'react-i18next';
    import * as RNLocalize from 'react-native-localize';
    import {locales} from './locales';
    
    RNLocalize.addEventListener('change', () => {
      const language = RNLocalize.getLocales()[0].languageCode;
      i18n.changeLanguage(language);
      console.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,
      });
    
    export default i18n;
  • Tým pádom pridať nový jazyk znamená vytvoriť nový súbor v module locales a do exportovaného objektu locales tohto modulu pridať riadok navyše, ktorý zabezpečí nahratie tohto jazyka.

Computerphile

Conclusion

  • Dnes sme sa pozreli na to, ako vieme zabezpečiť preklad používateľského rozhrania do iných jazykov. Ukázali sme si, kedy sa jazyk vyberie na základe jazyka systému a pozreli sme sa na to, ako organizovať preklady aplikácie v projekte. Tým sme aplikáciu pripravili pre nasadenie pre globálny trh ;)

  • Nabudúce sa pozrieme na to, ako je možné vytvárať a organizovať vlastné komponenty.