Интернационализация одностраничных приложений

Константин Кривленя

Компания Taucraft

goo.gl/AC3jyS

Немного о себе

Разработчик в Taucraft.
Мейнтейнер опенсорсной JavaScript-библиотеки чартов Taucharts.
Twitter (https://twitter.com/Krivlenia/)
Github (https://github.com/Mavrin/)
Хабр (http://habrahabr.ru/users/mavrin/)

Трудности локализации

Аudi TT Coupe

По-французcки — «отрубленная голова»

Fiat Uno

По-фински — «кретин»

Chevrolet Nova

По-испански — «не едет»

Что такое интернационализация?

Что такое локализация?

Зачем нужна интернационализация?

i love you

251 страна

3000-6000 языков

Новые пользователи

Различие локалей

Проблемы и решения

Фразы

Фиксированный размер

Шрифты

Формы

Решение

Формат сообщений ICU

Select
    
    {gender, select,
        male {He}
        female {She}
        other {They}
    } will respond shortly.
    

Формат сообщений ICU

Plural (CLDR - Common Locale Data Repository)
cardinal (количественные числительные), ordinal (порядковые числительные), range (промежуток)
            
    You have {itemCount, plural,
        =0 {no items}
        one {1 item}
        other {{itemCount} items}
    }.
            
        

Английский: порядковые числительные

ordinal one 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … Take the 1st right.
two 2, 22, 32, 42, 52, 62, 72, 82, 102, 1002, … Take the 2nd right.
few 3, 23, 33, 43, 53, 63, 73, 83, 103, 1003, … Take the 3rd right.
other 0, 4~18, 100, 1000, 10000, 100000, 1000000, … Take the 4th right.

Русский: порядковые числительные

ordinal other 0~15, 100, 1000, 10000, 100000, 1000000, … Сверните направо на 15-м перекрестке.

FormatJS от

Числа и даты

Где взять форматы?

Спасибо браузерам: intl API

window.intl

Объект Intl является пространством имён для API интернационализации ECMAScript, предосталяющим языко-зависимое сравнение строк, форматирование чисел и дат со временем. Конструкторы объектов Collator, NumberFormat и DateTimeFormat являются свойствами объекта Intl.

Intl.Collator

            
    var collator = new Intl.Collator();
    console.log(collator.compare('a', 'c')); // → отрицательное значение
    console.log(collator.compare('c', 'a')); // → положительное значение
    console.log(collator.Collator().compare('a', 'a')); // → 0
    // В немецком буква ä идёт рядом с буквой a
    console.log(new Intl.Collator('de').compare('ä', 'z'));
    // → отрицательное значение
    // В шведском буква ä следует после буквы z
    console.log(new Intl.Collator('sv').compare('ä', 'z'));
    // → положительное значение
            
        

Intl.DateTimeFormat

            
    var date = new Date(Date.UTC(2012, 11, 20, 3, 0, 0));
    // Форматирование ниже предполагает, что местный часовой пояс равен
    // America/Los_Angeles для локали США
    // В американском английском используется порядок месяц-день-год
    console.log(new Intl.DateTimeFormat('en-US').format(date));
    // → "12/19/2012"
    // В британском английском используется порядок день-месяц-год
    console.log(new Intl.DateTimeFormat('en-GB').format(date));
    // → "20/12/2012"
    // В корейском используется порядок год-месяц-день
    console.log(new Intl.DateTimeFormat('ko-KR').format(date));
    // → "2012. 12. 20."
            
        

Intl.NumberFormat

            
    var number = 123456.789;
    // В Германии в качестве разделителя целой и дробной части используется запятая,
    // а в качестве разделителя разрядов - точка
    console.log(new Intl.NumberFormat('de-DE').format(number));
    // → 123.456,789
    // В России в качестве разделителя целой и дробной части используется запятая,
    // а в качестве разделителя разрядов - пробел
    console.log(new Intl.NumberFormat('ru-RU').format(number));
    // → 123 456,789
    // В большинстве арабоговорящих стран используют настоящие арабские цифры
    console.log(new Intl.NumberFormat('ar-EG').format(number));
    // → ١٢٣٤٥٦٫٧٨٩
            
        

Intl.NumberFormat

            
var number = 123456.789;
// Запрашиваем формат валюты
console.log(new Intl.NumberFormat('de-DE',
    { style: 'currency', currency: 'EUR' }).format(number)
);
// → 123.456,79 €
console.log(new Intl.NumberFormat('en-IN',
    { style: 'currency', currency: 'EUR' }
).format(number));
// → €123,456.79
            
        

intl-relativeformat

            
var english = new IntlRelativeFormat('en-US');
var russian = new IntlRelativeFormat('en-US');
var post = {
    id   : 1,
    title: 'Some Blog Post',
    date : new Date(1426271670524)
};
console.log(english.format(post.date)); // => "3 hours ago"
console.log(russian.format(post.date)); // => "3 часа назад"
            
        

Safari не поддерживается

Но есть полифил Intl.js

Направление текста и размещение блоков

Направление текста

Слева направо
Справа налево
Сверху вниз

Слева направо

            
    {
       direction: ltr;
    }
            

        

Справа налево

            
    {
        unicode-bidi: bidi-override;
        direction: rtl;
    }
            
        

Сверху вниз

            
    {
        writing-mode: vertical-lr
    }
            
        

Размещение блоков

С блоками сложнее

Но есть PostCSS плагин
            
.example {
  display:inline-block;
  padding:5px 10px 15px 20px;
  margin:5px 10px 15px 20px;
  border-style:dotted dashed double solid;
  border-width:1px 2px 3px 4px;
  border-color:red green blue black;
  box-shadow: -1em 0 0.4em gray, 3px 3px 30px black;
}
            
        
              
.example {
  display:inline-block;
  padding:5px 20px 15px 10px;
  margin:5px 20px 15px 10px;
  border-style:dotted solid double dashed;
  border-width:1px 4px 3px 2px;
  border-color:red black blue green;
  box-shadow: 1em 0 0.4em gray, -3px 3px 30px black;
}
              
        

Ещё примеры

Доклад Фреймворки локализации Антона Немцева на WSD

Давайте перейдём к коду

Обозначение локалей и стандарты

BCP 47
zh-Hans-CN
код языка, код письменности и код страны т. е.
"zh-Hans-CN": упрощённый китайский, используемый в Китае.

Основной принцип работы библиотек

                
var locale = 'ru';
localize(keys.hello, locale);
var dict = {
    ru: {
        hello:'Привет, мир'
    },
    default: {
        hello:'Hello world'
    }
};
                
            

Как хранить ключи к переводу

Синтетические ключи

                
var dict = {
    'greeting.message':'Hello world'
};
var message = __('greeting.message');
                
            

Проблемы

Плюсы

Ключом является оригинальная фраза

                
var message = __('Hello world');
/**
Метод ищет фразу в некотором dict,
если не нашел - возвращает оригинальную фразу
**/
                
            

Проблемы

Плюсы

Наш путь — оригинальные фразы

Решаем проблемы

К примеру, у нас есть следующий код

HTML

            
<span class="ui-label-remain ui-progressbar__data__remain">
    <%= fn.intl.formatHTMLMessage('Remain {timeRemain}',
        {
        timeRemain: '<b class="value">' + this.timeRemain + 'h</b>'
        }
    ) %>
</span>
            
        

React

            
    <FormattedMessage message="Action required"/>
            
        

JavaScript

            
  var tooltipTextMap = {
    'board': intl.formatMessage('Make this View visible…'),
    'group': intl.formatMessage('Make this Group visible…')
  };
            
        

Парсим шаблоны и код

tau-extract-gettext

Работает на регулярных выражения и любви. Для JSX используется AST parser.

Контекст для компонента, шаблона, JS-файла

Результат

            
        {
            "menu":["group"],
            "users":["group"]
        }
            
        

Что с серверными сообщениями?

Всё на клиенте

    
var response = {
    message: "You've added {count} user stories.",
    data: {count: 10}
}
intl.formatMessage(response.message, data);
// You've added 10 user stories.
    

Что дальше, где хранить?

JSON-файлы переводов в отдельном пакете

Подключение словаря

    
    loadPolyfillAndDictionary(currentLocale)
        .then(
            ()=>    {
             initApp();
            }
        )
    

Репозиторий:
удобно для разработчика,
неудобно для переводчика.

Загружаем словарь в систему перевода

Пишем сервис
автоматизации процесса

Запускаем сервисы с помощью CI

При выпуске новой версии продукта

    
        {
            "dependencies": {
                "dictionary": "1.100.1"
            }
        }
    

Безопасность

Было бы хорошо, если бы все фразы были простыми строками.

Привет, Минск!

Переводчик может добавить <script>

            
    _.escape('<script> alert("Ха") </script> полезная фраза');
    // все хорошо
            
        

Иногда нужно форматирование

            
<b>Attention!</b> Remove this task?
            
        

В словаре перевода

            
<b>Внимание!</b> Удалить эту задачу?
<script>
alert("Ха")
// XSS во фразе с форматированием
</script>
            
        

Или навесить обработчик события

            
<b>Click me!</b> Remove this task?
            
        

В словаре перевода

            
<b>Кликни меня!</b>  Удалить эту задачу?
<script>
alert("Ха");
// XSS, нужно навесить событие на элемент
</script>
            
        

Чистим текст по белому списку

Если нужен обработчик

    
<b class="i-role-action">Кликни меня!</b>

// В коде приложении навешиваем событие

<script>
   $(".i-role-action").click(()=>{action();})
</script>
    

FormattedMessage для React

    
<FormattedMessage
    message={"{name} has {numPhotos}"}
    name={<MyComponent>Annie</MyComponent>}
    numPhotos={1000}
/>
    

Автоматизируем поиск XSS

Поможет инструментация кода.

    
formatMessage(stringFromDictionary +
    '<script>window.XSS++</script>'
)
    

Забытое, но важное

Думайте над оригинальными фразами. Не усложняйте. Материальный дизайн writing guide.

Личная просьба

Не используйте флаги для выбора языков, пишите названия язык на целевом языке пользователя.

Вопросы?

goo.gl/AC3jyS

Контакты

Twitter (https://twitter.com/Krivlenia/)
Github (https://github.com/Mavrin/)
Хабр (http://habrahabr.ru/users/mavrin/)