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}`;

Комментарии (9)


  1. Halt
    18.03.2016 16:45
    +2

    Или i18n или l10n.


    1. auine
      18.03.2016 16:49
      +1

      Спасибо, поправил :)


      1. frol
        18.03.2016 17:02

        Вы "поправили" на I10n (первая буква — заглавная i), а там либо i18n, либо L10n должно быть. (В теги можно оба варианта добавить.)


        1. denis_g
          18.03.2016 19:46
          +9

          Немного ясности для тех, кто еще не в курсе:

          'i' + 'nternationalizatio'.length + 'n';  // 'i18n'
          'L' + 'ocalizatio'.length + 'n';          // 'L10n'
          


  1. frol
    18.03.2016 16:57
    +1

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

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

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


    1. auine
      18.03.2016 17:07

      Я думаю на основе аргументов которые попадают в функцию, вполне можно провести подобный перевод :) Еще упустил момент, можно использовать например так:

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

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

      lang('ru')`Everything is ${desc}!`;
      lang('name', 'number')`${name}'s team is about ${count}`;


      1. frol
        18.03.2016 17:28

        Такой вызов лично для меня выглядит ужасно, тем не менее, спасибо за демонстрацию такой возможности 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 учтено много нюансов, просто для комментария выбрал один из готовых примеров.


  1. denis_g
    18.03.2016 19:50

    Спасибо за статью, интересно было почитать про TTL. А для локализации я для себя кроме gettext-совместимых решений пока что ничего не нашёл.


  1. dangreen
    19.03.2016 09:30

    Спасибо автору! Статья пришлась очень к стати. Реализовал подобный функционал у себя в библиотеке (i18n-for-browser).