Реализация мультиязычности приложения - 1

Сегодня мы поговорим о мультиязычности. Итак, что это?

Мультиязычность, иначе говоря, интернационализация — это часть разработки приложения, которое может быть адаптировано для нескольких языков без перекройки логики программы. Рассмотрим ситуацию: вы создаете веб-приложение крупной торговой компании для жителей большого количества стран, в которых говорят, например, на таких языках как русский, английский, испанский. Вам необходимо сделать его удобным для всех пользователей. Задумка такая: у русскоязычного читателя должна быть возможность видеть ваши данные на русском языке, американцу будет удобнее прочитать материал на английском, а испанцу — на испанском (неожиданно, да?) Реализация мультиязычности приложения - 2Рассмотрим сегодня несколько моделей интернационализации, и для одной из них (которая больше всего нравится мне:) рассмотрим реализацию на Java. В качестве подопытного кролика сегодня у нас будет табличка с данными о фильмах. Не будем особо извращаться, поэтому колонок у нас будет немного. Для примера — самое ОК. И переводить мы будем названия фильмов (режиссеры — так, для массовки): Реализация мультиязычности приложения - 3

1. Таблица с переводом под каждый язык

Суть данной модели: у каждого языка есть отдельная таблица в БД, в которой содержатся все ячейки, требующие перевода. Недостаток данного способа заключается в том, что каждый раз, когда у нас добавляется новый язык, необходимо добавлять новую таблицу. То есть представим себе, что у нашего заказчика ну ооочень хорошо пошли дела, и он расширяет свое приложение для многих стран (собственно, и языков) мира. А значит, нужно будет добавлять по табличке на язык. В итоге у нас будет БД, наполовину или почти полностью состоящая из вспомогательных табличек переводов: Реализация мультиязычности приложения - 4 Схема самих фильмов: Реализация мультиязычности приложения - 5Таблицы переводов:
  • Русский Реализация мультиязычности приложения - 6
  • Испанский Реализация мультиязычности приложения - 7
  • Английский Реализация мультиязычности приложения - 8

2. Одна на всех

В каждой таблице, которая относится к определённой модели, добавляется поле с идентификатором для таблички языка. Соответственно, в БД есть и эта табличка с переводами. Проблема в том, что одному объекту может соответствовать несколько переводов (языков). В итоге произойдет дублирование сущностей, а это сильно путает и усложняет логику, и это не есть хорошо. Смотрим UML: Реализация мультиязычности приложения - 9 Таблица movies: Реализация мультиязычности приложения - 10 Таблица languages: Реализация мультиязычности приложения - 11

3. По колонке на язык

Для каждой колонки для каждого языка в таблице создается отдельная колонка перевода. Минус такого подхода в том, что опять же, если будет добавляться большое количество языков, нужно будет каждый раз менять структуру базы данных, а это считается плохим подходом. Также представьте, насколько будут раздуваться таблички, требующие интернационализации. Возможно, стоит задуматься о такой модели, когда заранее известно количество поддерживаемых языков, их не слишком много, и каждая модель должна существовать во всех языковых вариациях. UML: Реализация мультиязычности приложения - 12Таблица All inclusive: Реализация мультиязычности приложения - 13

4. Внешний перевод

Этот вариант реализуется за счет подключения внешних средств (Google translate, Bing translate, etc.). Его применяют, если нужно предоставить информацию как можно большему количеству посетителей, и этой информации много. Очень много. В таком случае можно принять решения не хранить прямо информацию в БД на всех языках, а динамически её переводить. Но при это стоит помнить, что качество машинного перевода зачастую оставляет желать лучшего. Вариант может рассматриваться лишь как очень экономный (когда нет ресурсов на перевод каждой публикации). Частой проблемой с точки зрения правильности перевода является то, что плохо знающие язык переводчики выбирают неправильное значение слова и путают пользователя, вынуждая его самостоятельно додумывать смысл написанного на кнопке. Ещё важно не только правильно перевести предложение, но и подвести его смысл к конкретному языку и национальности. У разработчиков возникает много проблем из-за родов в некоторых языках. Им приходится дублировать фразу в коде в зависимости от пола пользователя, а также учитывать, что не только у существительных есть род, но и прилагательные и глаголы по-разному склоняются. Бывают случаи, когда выбрав не английский язык в приложении, наряду со словами выбранного языка всё равно встречаются непереведенные элементы. Ещё хуже, если отображаются несколько языков и получается своеобразный Вавилон, где всё смешалось, и пользователь не может разобраться в приложении Например: https://cloud.google.com/translate/

5. Вспомогательные файлы на уровне приложения

Создаются отдельные файлы для хранения переводов. Может быть по файлу на язык или по файлу на язык на одну табличку (более мелкое дробление). Вариант часто используется в силу того, что в этих файлах можно хранить много текстов, а значит, таблицы и сама БД не будут раздуваться. Удобство заключается ещё в том, что не нужно за этими полями стучаться в БД, и в коде файлы можно динамически подменять в зависимости от запрошенного языка. В итоге файл служит для нас словарем, в котором ключ — язык, значение — текст. Но мы не ограниченны приведенным ниже форматом ".properties", а форматы этих файлов могут широко отличаться — JSON, XML, etc. Слабые стороны в том, что в этом случае сильно снижается нормализация базы данных. Также целостность данных зависит уже не только от базы данных, но и от механизма сериализации. Отличная статья на эту тему Пример файлов-словарей с переводами: Реализация мультиязычности приложения - 14
  • Английский Реализация мультиязычности приложения - 15
  • Русский Реализация мультиязычности приложения - 16
  • Испанский Реализация мультиязычности приложения - 17

6. Вспомогательная таблица переводов под каждую таблицу

Как по мне, наиболее гибкое решение. Суть данного подхода заключается в создании отдельной таблицы под языки. Когда нужно реализовать возможность переводов для рассматриваемой таблицы, создается связь с таблицей языков, а в таблице связей есть id языка, id элемента и колонки с переводами. Всё не так страшно, как звучит. Данный подход позволяет реализовать достаточно гибкую расширяемость поддерживаемых языков. Давайте посмотрим наглядно. UML: Реализация мультиязычности приложения - 18Таблица с фильмами: Реализация мультиязычности приложения - 19Таблица языков: Реализация мультиязычности приложения - 20Таблица переводов: Реализация мультиязычности приложения - 21И, как я говорил выше, давайте рассмотрим реализацию одного из вариантов в Java-коде (как вы поняли, это будет последний вариант). В самом приложении ничего такого: мы пройдёмся от контроллеров до слоев дао. Будем рассматривать на методе create — для примера этого хватит с головой. Итак поехали)) Наша сущность — фильм:

@Builder
@Getter
public class Movie {

   private Long id;

   private String producer;
}
Ничего интересного, просто реализация модели первой таблицы. Контроллер с конвертерами dto (Data Transfer Object):

@RestController
@RequiredArgsConstructor
@RequestMapping(path = "/cities")
public class MovieController {

   private final MovieService movieService;

   @PostMapping
   public ResponseEntity<moviedto> create(MovieDTO movieDTO) {
       return new ResponseEntity<>(toDTO(movieService.create(fromDTO(movieDTO), movieDTO.getNameTranslations()), movieDTO.getNameTranslations()), HttpStatus.CREATED);
   }

   private Movie fromDTO(MovieDTO dto) {
       return Movie.builder()
               .id(dto.getId())
               .producer(dto.getProducer())
               .build();
   }

   private MovieDTO toDTO(Movie movie, Map<string, string=""> nameTranslation) {
       return MovieDTO.builder()
               .id(movie.getId())
               .producer(movie.getProducer())
               .nameTranslations(nameTranslation)
               .build();
   }
}
В DTO мы передаем переводы в качестве мап, key — сокращение языка, value — значение перевода (имя фильма). DTO:

@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class MovieDTO {

   @JsonProperty("id")
   private Long id;

   @JsonProperty("name")
   private String producer;

   @JsonProperty("nameTranslations")
   private Map<String, String> nameTranslations;//example = "{'en': 'The Matrix', 'ru' : 'Матрица'}"
}
Здесь мы видим сам класс dto, как и написал выше, map для переводов, остальные поля — отображение модели Movie Переходим к сервису фильмов:

public interface MovieService {

   Movie create(Movie movie, Map nameList);
}
Его реализация:

@Service
@RequiredArgsConstructor
public class MovieServiceImpl implements MovieService {

   private final MovieDAO movieDAO;
   private LanguageService languageService;

   @Override
   public Movie create(Movie movie, Map<string, string=""> nameList) {
       movieDAO.create(movie);
       Map<Long, String> map = new HashMap<>();
       nameList.forEach((x, y) -> map.put(languageService.getIdByLangCode(x), y));
       movieDAO.createTranslator(movie.getId(), map);
       return movie;
   }
}
Здесь мы видим использование относительно стороннего сервиса LanguageService, для вытягивания id языка, по его сокращению. И уже с этим идентификатором мы сохраняем наши переводы (так же в виде map) в таблицу связи. Посмотрим на DAO:

public interface MovieDAO {

   void create(Movie movie);

   void createTranslator(Long movieId, Map<Long,String> nameTranslations);
}
Реализация:

@RequiredArgsConstructor
@Repository
public class MovieDAOImpl implements MovieDAO {
   private final JdbcTemplate jdbcTemplate;

   private static final String CREATE_MOVIE = "INSERT INTO movies(id, producer) VALUES(?, ?)";

   private static final String CREATE_TRANSLATOR = "INSERT INTO movies_translator(movies_id, language_id, name) VALUES(?, ?, ?)";

   @Override
   public void create(Movie movie) {
       jdbcTemplate.update(CREATE_MOVIE, movie.getId(), movie.getProducer());
   }

   @Override
   public void createTranslator(Long movieId, Map<Long, String> nameTranslations) {
       nameTranslations.forEach((x, y) -> jdbcTemplate.update(CREATE_TRANSLATOR, movieId, x, y));
   }
}
Здесь мы видим сохранение сущности и языков к ней (словаря). И да, здесь используется Spring JDBC: считаю его предпочтительней для новичков, так как он более прозрачен. Переходим к «стороннему» сервису. Сервис языка:

public interface LanguageService {

   Long getIdByLangCode(String lang);
}
Реализация:

@Service
@RequiredArgsConstructor
public class LanguageServiceImpl implements LanguageService {
   private final LanguageDAO languageDAO;

   @Override
   public Long getIdByLangCode(String lang) {
       return languageDAO.getIdByLangCode(lang);
   }
}
Ничего особенного, поиск по сокращенному имени. DAO:

public interface LanguageDAO {

   Long getIdByLangCode(String lang);
}
Реализация:

@RequiredArgsConstructor
@Repository
public class LanguageDAOImpl implements LanguageDAO {
   private final JdbcTemplate jdbcTemplate;

   private static final String FIND_ID_BY_LANG_CODE = "SELECT id FROM languages WHERE lang_code = ?";

   @Override
   public Long getIdByLangCode(String lang) {
       return jdbcTemplate.queryForObject(FIND_ID_BY_LANG_CODE, Long.class, lang);
   }
}
Структура: Реализация мультиязычности приложения - 23Все описанные выше модели имеют право на жизнь. Решать, какую именно использовать, нужно исходя из ситуации. Конечно, это не все: есть ещё много различных подходов, среди которых — использование разных БД для разных языков, использование кешей, различных фреймворков и так далее. На этом у меня сегодня сегодня все и… Реализация мультиязычности приложения - 24