Одна из причин, по которой мы выбрали Scala в Foursquare, - это ее выразительная система шрифтов. Мы можем использовать систему типов для устранения больших классов ошибок во время выполнения. Недавно мы устранили сотни ошибок, связанных с переводами, сделав наше внутреннее представление переведенной копии более безопасным по типу.

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

  • Информация о поле
  • Информация о множественном числе
  • Пояснения к любым заполнителям

среди прочего.

Раньше у нас было фактическое определение копии отдельно от этой метаинформации. Программист добавлял копию в базу кода, а затем добавлял всю метаинформацию в отдельный файл. При проверке кода я заметил, что инженеры часто забывали редактировать отдельный файл или запоминали, но не добавляли туда всю информацию.

Это привело к тому, что плохие переводы стали распространяться среди конечного пользователя ...

Здесь мы оставили некоторую информацию о множественном числе и закончили переводом «1 подсказка».

Здесь мы отказались от некоторой гендерной информации и назвали этого пользователя мэром-мужчиной «эль алькальде», а не женщиной «ла алькальдеса».

Это фрагмент электронного письма, которое нам написал пользователь. «Почта» переводилась как «Почта».

Конкретнее с проблемами

Во-первых, мы хотели автоматизировать процесс добавления метаданных в отдельный файл. Мы решили сделать это, прочитав метаданные непосредственно из исходного кода scala и используя компилятор, чтобы потребовать от инженера добавить дополнительную информацию. Это позволило бы нам опираться на компилятор с мощной системой типов scala и дать нам возможность сделать вещи более безопасными по типу. Это также делает вещи более автоматизированными и удобочитаемыми.

Строим Строителя

Следовательно, мы создали LocalStringBuilder.

Этот построитель имеет 2 внутренних параметра типа State и Goal. State - это текущее состояние LocalStringBuilder, а Goal - это состояние, в котором вы хотите закончить.

Вот пример использования построителя для создания переводимой строки:

LocalStringBuilder
 .baseCopy("Your Swarm friend %1$s has checked in here")
 .baseComment("%1$s is the name of a friend with unknown gender")
 .femaleCopy("Your Swarm friend %1$s has checked in here")
 .femaleComment("%1$s is the name of a female friend")
 .maleCopy("Your Swarm friend %1$s has checked in here")
 .maleComment("%1$s is the name of a male friend")
 .result()

Эта конкретная копия является гендерной, но английская копия одинакова для всех гендерных значений. Однако перевод гендерных ценностей на испанском языке отличается:

Когда вы впервые создаете LocalStringBuilder, State это InitialState, что в основном является пустым состоянием, а цель - это базовое состояние, в котором есть только состояния, которые потребуются каждому фрагменту локализованной копии - какая копия вы хотите перевести, и поясняющий комментарий к копии.

Если вы попытаетесь вызвать .result на этом недавно созданном LocalStringBuilder, вы получите ошибку компиляции, потому что State и Goal не одного типа.

Вам может быть интересно, почему нет только State. Когда мы вызываем .result, мы могли бы просто проверить, есть ли у State свойство comment и свойство copy, и готово. У нас есть параметр Goal, потому что состояние Goal может меняться в зависимости от того, какие функции вы вызываете в построителе.

Напомним, что гендерная информация - это то, что переводчикам может понадобиться для правильного перевода строки. Например, возьмем английскую копию «She was all like…». Это текст, который появляется перед подсказкой, оставленной пользователем-женщиной. У нас есть разные копии в одних и тех же обстоятельствах для пользователей, которые являются мужчинами («Он был все как…»), и для пользователей, которые не указали свой пол («Они все были как…»). В этом случае английская копия отличается для каждого пола, но во многих случаях английская копия одинакова, но будет отличаться на других языках.

У нас есть .gender функция на LocalStringBuilder, но, как вы можете видеть выше, вы также должны предоставить различные варианты части копии. Итак, если вы вызываете .gender или добавляете конкретную мужскую или женскую копию в конструктор, тип Goal изменяется с Basic на Gendered. Gendered имеет черты для женской и мужской копии, а также для определенного пола во время создания. Если у вас есть хотя бы одна из этих частей информации, но отсутствует хотя бы одна часть этой информации, код не будет компилироваться.

То же самое и с функцией .pluralize. Вам нужно указать версию копии в единственном числе, а также конкретное число, которое вы используете во множественном числе. Если вы предоставите одно без другого, код не скомпилируется.

У вас может быть LocalStringBuilder одновременно Gendered и Pluralized. Это создает новый Goal с именем GenderedAndPluralized. Эта цель требует дополнительной информации в единственном экземпляре как для мужских, так и для женских случаев.

В дополнение ко всему этому, мы также гарантируем, что вы не вызовете одну и ту же функцию дважды в конструкторе.

Все это мы можем делать, используя фантомные типы и имплициты. Вот фрагмент кода функции, в которой мы указываем пол этой конкретной копии:

def gender[S2](g: Gender.Value)(
    implicit addGender: LocalStringBuilder.AddGender[State, S2],
    checkNotAddedTwice: LocalStringBuilder.NotAlready[State, LocalStringBuilder.HasGender]
  ): LocalStringBuilder[LocalStringBuilder.Gendered, S2] = {
    this.copy(genderOpt = Some(g))
  }

Неявный параметр addGender принимает State и создает S2 = State с HasGender.

Неявный параметр checkNotAlreadyAdded проверяет наличие HasGender <: State. В этом случае код не будет компилироваться.

Обратите внимание, что тип цели изменился с Goal на LocalStringBuilder.Gendered.

На этом этапе мы немного углубились в код, поэтому позвольте мне вернуть его пользователю. Помните, наверху Рой сказал мне, что я мэр-мужчина? Это произошло потому, что инженер передал пол пользователя через функцию gender(), но не смог указать отдельную копию для разных полов. Это привело к тому, что испанские переводчики получали для перевода только средний вариант.

Теперь этот тип ошибки невозможен. Программист не может указать пол, не предоставив переводчикам отдельную копию.

Тестирование

Вам может быть интересно, как вы проверяете правильность этого кода. Очевидно, вы можете просто добавить примеры правильных случаев, и если код компилируется, то все в порядке. Но как проверить код, который не должен компилироваться?

Для этого мы использовали то, что было открыто от Foursquare несколько лет назад в рамках проекта Spindle. По сути, мы запускаем scala repl в нашем тестовом коде и проверяем вывод, чтобы убедиться, что у нас есть ошибка компилятора, и она соответствует тому, что мы ожидаем.

Вывод

После внедрения этого изменения мы превратили кучу пользовательских ошибок в сотни ошибок компиляции. После того, как код скомпилирован и пройдены тесты, мы смогли протолкнуть этот код, чтобы предотвратить бесчисленное количество ошибок в будущем. Мы не смогли бы сделать это на любом языке. Система типов Scala действительно была нашей секретной сверхдержавой для нашего конвейера интернационализации. Если вы тоже заинтересованы в превращении ошибок в ошибки компиляции, мы нанимаем!

- Марьям Али остальная часть команды Foursquare # i18n