L10n строк в приложениях (JavaScript)

    image

    В рамках изучения нового стандарта наткнулся на Tagged Template Literals, по-русски часто обзывают строковые шаблоны.
    За рубежом советуют переименовать в tagged string literals, а Template Literals тем временем в interpolated string literals или просто interpoliterals . Обожаю js тусовку, где халивар начинают даже из-за название фичи :)

    Что, куда, зачем


    Прежде чем перейдем непосредственно к тому о чем я хотел рассказать исходя из заголовка, пару слов о Template Literals.
    Наверняка каждый из вас их уже попробовал:
    var text =
    `Now is the time for all good men
    to come to the aid of their
    country!`;
    
    console.log( text );
    // Now is the time for all good men
    // to come to the aid of their
    // country!
    


    Первая их крутость, заключается в том, что текст такого литерала можно писать на нескольких строчках и он сам где надо вставит переход на новую строку.
    Конечно же вторая крутость заключается в возможности вставки выражений в литерал, что значительно упрощает построение строк в которые вы хотите вставить какое-либо значение.
    До ES6:
    var name = "Kyle";
    
    var greeting = "Hello " + name + "!";
    
    console.log( greeting );            // "Hello Kyle!"
    console.log( typeof greeting );     // "string"
    

    Решение проблемы с помощью Template Literals:
    var name = "Kyle";
    
    var greeting = `Hello ${name}!`;
    
    console.log( greeting );            // "Hello Kyle!"
    console.log( typeof greeting );     // "string"
    

    Тут конечно надо заметить, что переиспользовать данный литерал с другим значением нельзя. Поэтому ходят споры об именовании. Куда было бы разумней назвать не используя слово шаблон, а интерполяция, что бы не вводить в заблуждение. Данный литерал подобен Immediately-Invoked Function Expression (IIFE), он сразу же запекается в строку. Поэтому передать его и переиспользовать к сожалению возможности нет:
    function foo(str) {
        var name = "foo";
        console.log( str ); //name уже давно в str и str typeof 'string'
    }
    
    function bar() {
        var name = "bar";
        foo( `Hello from ${name}!` );
    }
    
    var name = "global";
    
    bar(); 
    

    Как я писал выше, вставить в строку можно и результат выражения:
    function upper(s) {
        return s.toUpperCase();
    }
    
    var who = "reader";
    
    var text =
    `A very ${upper( "warm" )} welcome
    to all of you ${upper( `${who}s` )}!`;
    
    console.log( text );
    // A very WARM welcome
    // to all of you READERS!
    


    Tagged Template Literals


    А теперь самое интересное, рассмотрим с вами использование Tagged Template Literals:
    function foo(strings, ...values) {
        console.log( strings );
        console.log( values );
    }
    
    var desc = "awesome";
    
    foo`Everything is ${desc}!`;
    // [ "Everything is ", "!"]
    // [ "awesome" ]
    

    В данном случае tag foo , своего рода вызов функции без (...). Таким образом, мы можем обработать строку дополнительно.
    В качестве аргументов, функция foo получает массив строк, которые были вокруг выражения ${desk}, и собственно сами значения выражений.
    С помощью такого сахарного синтаксиса, очень просто представить себе перевод строк:
    const greeting = lang`Hello ${ name }!`
    

    Как по мне куда проще нежели — учитывая, что в строке есть динамические данные:
    const  greeting  = `${ lang('Hello') } ${ name }!`
    

    Или совсем сносный пример который можно часто увидеть:
    const greeting  = lang('Hello') + name + '!';
    

    Так как мы получаем в функции по факту разобранную строку на части, нам придется ее самим собрать, с этим нам поможет reduce:
    function lang(strings, ...values) {
        return strings.reduce( function(s,v,idx){
            return s + (idx > 0 ? values[idx-1] : "") + v;
        }, "" );
    }
    

    Добавим вызов функции перевода и сделаем пример чуть более читабельным, получив вот такой сниппет:
    const lang = l = (strings, ...values) =>
        strings.reduce((prevString, nextString, index) =>
            prevString + (index > 0 ? values[index - 1] : '') + translate(nextString), ''); //Реализация функции translate в ваших руках :)
    

    Использование:
    const name = 'Дмитрий';
    
    let greeting = lang`Hello ${ name }, how are you?`;
    
    //Или сокращенный
    greeting = l`Hello ${ name }, how are you?`;
    //Привет Дмитрий, как дела?
    

    Еще один очень полезный пример перевода чисел в формат US $:
    function dollabillsyall(strings, ...values) {
        return strings.reduce( function(s,v,idx){
            if (idx > 0) {
                if (typeof values[idx-1] == "number") {
                    // look, also using interpolated
                    // string literals!
                    s += `$${values[idx-1].toFixed( 2 )}`;
                }
                else {
                    s += values[idx-1];
                }
            }
    
            return s + v;
        }, "" );
    }
    
    var amt1 = 11.99,
        amt2 = amt1 * 1.08,
        name = "Kyle";
    
    var text = dollabillsyall
    `Thanks for your purchase, ${name}! Your
    product cost was ${amt1}, which with tax
    comes out to ${amt2}.`
    
    console.log( text );
    // Thanks for your purchase, Kyle! Your
    // product cost was $11.99, which with tax
    // comes out to $12.95.
    

    Собственно говоря у пост обработки строк Tagged Template Literals есть и другие применения, но это уже за рамками темы, пишите в комментариях если есть, что добавить :)
    Источник вдохновения:
    github.com/getify/You-Dont-Know-JS/blob/master/es6%20%26%20beyond/ch2.md

    UPD: спасибо dotme
    UPD1: спасибо alip

    UPD2: спасибо frol
    Кроме того, часто нужен контекст (чтобы «name» в одних случаях переводилось как «имя», а в других — «наименование»), где можно было бы добавить в том числе и внешнюю информацию (пол {М/Ж}, число {единственное/множественное}, и тд и тп).

    Интересное замечание по поводу более продвинутого перевода. В целом в статье я попытался показать именно сахар, а не способы и подходы перевода, но кое-что все же забыл упомянуть:
    function bar() {
        return function foo(strings, ...values) {
            console.log( strings );
            console.log( values );
        }
    }
    
    const desc = 'OK';
    bar()`Everything is ${desc}!`;
    

    Тегом может быть выражение которое возвращает функцию. Благодаря этому возможно передать какие-либо дополнительные аргументы:
    lang('ru')`Everything is ${desc}!`;
    //
    lang('name', 'number')`${name}'s team is about ${count}`;
    
    • +13
    • 12.7k
    • 9
    Share post

    Comments 9

      +2
      Или i18n или l10n.
        +1
        Спасибо, поправил :)
          0
          Вы "поправили" на I10n (первая буква — заглавная i), а там либо i18n, либо L10n должно быть. (В теги можно оба варианта добавить.)
            +9
            Немного ясности для тех, кто еще не в курсе:
            'i' + 'nternationalizatio'.length + 'n';  // 'i18n'
            'L' + 'ocalizatio'.length + 'n';          // 'L10n'
            

        +1
        Всё-таки мне кажется, что для переводов важен контекст, поэтому переводить нужно всю шаблонизированную строку, а не отдельные её части.

        Например: "${name}'s team" ("Vasya Pupkin's team") -> "Команда ${name}" ("Команда Васи Пупкина"), конечно, тут ещё проблема со склонением имён, но даже без проблемы склонения имён, порядок слов изменился.

        Кроме того, часто нужен контекст (чтобы "name" в одних случаях переводилось как "имя", а в других — "наименование"), где можно было бы добавить в том числе и внешнюю информацию (пол {М/Ж}, число {единственное/множественное}, и тд и тп).
          0
          Я думаю на основе аргументов которые попадают в функцию, вполне можно провести подобный перевод :) Еще упустил момент, можно использовать например так:

          bar()`Everything is ${desc}!`;

          Допустим передать язык или формат:

          lang('ru')`Everything is ${desc}!`;
          lang('name', 'number')`${name}'s team is about ${count}`;
            0
            Такой вызов лично для меня выглядит ужасно, тем не менее, спасибо за демонстрацию такой возможности ES6. В своём проекте я бы такой синтаксис не использовал, а взял бы i18next:

            {
              "en": {
                "translation": {
                  "key": "item",
                  "key_plural": "items",
                  "keyWithCount": "{{count}} item",
                  "keyWithCount_plural": "{{count}} items"
                }
              }
            }

            i18next.t('key', {count: 0}); // output: 'items'
            i18next.t('key', {count: 1}); // output: 'item'
            i18next.t('key', {count: 5}); // output: 'items'
            i18next.t('key', {count: 100}); // output: 'items'
            i18next.t('keyWithCount', {count: 0}); // output: '0 items'
            i18next.t('keyWithCount', {count: 1}); // output: '1 item'
            i18next.t('keyWithCount', {count: 5}); // output: '5 items'
            i18next.t('keyWithCount', {count: 100}); // output: '100 items'

            P.S. В i18next учтено много нюансов, просто для комментария выбрал один из готовых примеров.
          0
          Спасибо за статью, интересно было почитать про TTL. А для локализации я для себя кроме gettext-совместимых решений пока что ничего не нашёл.
            0
            Спасибо автору! Статья пришлась очень к стати. Реализовал подобный функционал у себя в библиотеке (i18n-for-browser).

            Only users with full accounts can post comments. Log in, please.