Поддержка мультиязычности в веб-проектах — базовые варианты реализации


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

    1. Создание полей для каждого языка


    Реализация: для каждого поля для каждого языка в таблице создается отдельная колонка
    Особенности: при большом количестве языков, или при заране неизвестно количестве языков такой подход будет требовать изменения структуры БД каждый раз, когда понадобиться реализовать подержку нового языка.
    Когда стоит использовать: когда заранее четко известно количество поддерживаемых языков, и каждая сущность должна сущестовать во всех языковых вариантах.

    2. Создание таблицы локализации


    Реализация: для каждой сущности требующей локализации создается две таблицы. Основная таблица, содержащая поля, которые не зависят от конкретного языка и таблица содержащая поля требующие перевода. Также в БД создается таблица со списком доступных языков.
    Особенности: данный подход ползволяет реализовать достаточно гибкую расширяемость поддерживаемых языков. Структура этого варианта немного сложнее предыдущего и предполагает создание таблицы-саттелита для каждой таблицы содержащей локализуемые поля.

    3. Использование сериализованных данных сложной структуры


    Реализация: в каждое поле требующее перевода пишется информация в сериализованном виде, например в JSON, XML, binary, etc. Объект при этом может быть например словарем, в котором ключ — язык, значение — текст. Или любой другой структуры.
    Особенности: главная особенность состоит в том, что целостоность данных зависит уже не только от базы данных, но и от механизма сериализации. Кроме того, в таком варианте также очень сильно снижается нормализация базы данных.

    4. Отдельная запись в таблице для каждого языка


    Реализация: в каждой таблице, которая описывает сущность требующую локализации поле с указанием языка, к которому относится запись. Также в БД создается таблица со списком доступных языков.
    Особенности: данный вариант примечателен тем, что по сути одному объекту предметной области может соответствовать несколько объектов в БД и несколько связей. ЧТо может достаточно сильно усложнить бизнес-логику.

    5. Отсутствие локализации на уровне БД. Использование внешних средств локлаизации

    Данный вариант не относится напрямую к теме статьи, но думаю что упомянуть его стоит.
    Реализация: способ реализуется за счет внешних подключаемых модулей (Google translate, Bing translate, etc.).
    Особенности: данный вариант может применятся в том случае, если владелец ресурса хочет иметь возможность предоставить информацию как можно большему количеству посетителей. При этом следует понимать, что качество машинного перевода зачастую оставляет жлеать лучшего. Вариант может рассматриваться лишь как очень бюджетный (когда нет ресурсов на перевод каждой публикации) и перед его применением я бы рекомендовал хорошо подумать — а стоит ли вообще его реализовывать.

    6. Создание таблицы перевода для каждого языка


    Вариант предложенный хабрапользователем VolCh.
    Реализация: для каждого языка в БД создается отдельная таблица, содержащая поля требующие перевода.
    Особенности: при добавлении нового языка необходимо вносить изменения в базу. При большом количестве поддерживаемых языков количество таблиц может быть очень велико.

    Хранение «статической» текстовой информации

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


    Другие варианты


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

    Полезная информация


    Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

    Какой вариант реализации поддержки мультиязычности вы предпочитаете?
    Где нужно хранить статические текстовые блоки?
    Поделиться публикацией
    Комментарии 34
      +13
      По поводу текста в ресурсных файлах, это зависит от того что именно мы там храним, если это переводы для полей форм, различных ошибок (то есть впринципе они не меняются) то это файлы однозначно. Если же храним перевод контента, то тут без баз никак.
        +1
        Использую схему больше всего похожую на таблицу локализации, но для каждого языка (вернее локали) своя таблица. То есть не EntityTranslate, а EntityRuRu, EntityEnUs и т. д.

        Статический текст обычно в исполняемых файлах, но обращение идёт через функцию/хелпер типа _() и способ хранения может быть легко заменен на любой другой.
          +1
          Используем gettext, на каждую локаль — свой файл с терминами. При загрузке приложения вся локализация кешируется.
            +1
            Самый «ровный» вариант — это, конечно, второй. Но при наличии в приложении большого числа вложенных связей между таблицами, получаем кучу зубодробительных SQL запросов, которые мало того, что тормозят, так еще и сильно усложняют настройку кэша. Задача локализации контента пользователей до сих пор остается одной из самых неприятных (по крайней мере в случае использования реляционной БД.)
              0
              Методом проб и ошибок лично я пришел к решению в виде нескольких БД. К примеру: одна для русского, другая — для английского + простенький скрипт для синхронизаций изменений главных настроек сайта. Материалы тоже синхронизируются при создании, т.е. мы создаем материал в одной версии, она дублируется в другую, но в статусе «не опубликовано». Переключать БД при запросах в очень просто, у меня это базовая функция в фреймворке.
              Плюсы: отдельные данные для разных версий сайта в виде нескольких БД. Возможность вести не совпадающие по контенту версии сайта (в русском статья есть, в англ. не нужна, к примеру), использовать разные настройки для сайтов. На все версии сайта у нас один код фреймворка и вообще никаких хаков, вся реализация не более 100 строк. Никаких сложных связей и запросов к БД
                +2
                Как по мне — очень спорный вариант. Для поддержаия целостности данных затрачивается много ресурсов. Однако, как мне кажется такой вариант может быть реализован в очень крупном проекте, но там это будет уже скорее шагом к масштабированию, нежели к локализации.
                  0
                  Хабр такой хабр… Заминусовали комментарий по делу, предлагающий вполне жизненную альтернативу (да, спорную, но действенную) — зато стабильно плюсуют комментарии типа «Nokia RIP» в статье о покупке нокии мелкософтом… Как будто у нас тут камеди клаб какой-нибудь.
                  +1
                  Используем 2й и 4й.
                  2й вариант для таблиц, активно учавствующих в бизнес логике, где нельзя держать несколько одинаковых записей рядом.
                  4й вариант больше для контента (новости, статические страницы, статьи).
                  Видел еще реализации с несколькими таблицами (news_ru, news_en)

                  Начинали с gettext, но потом ушли все же в базу (больше тысячи ключей).
                    –3
                    Я предпочитаю об этом не думать и использовать стандартные средства, предоставляемые фреймворком. Если вариантов больше одного, то выбираю наиболее популярный (читай: активно поддерживаемый, стабильный для данной версии, с минимальным количеством негативных отзывов и т.д.) А как там оно решается внутри — уже не моё дело. В большинстве случаев считаю такой подход самым верным. Ну а если это меганагрузка, много данных и всё такое, то там уже можно и подумать, потестировать, но, думается мне, в большинстве случаев это будет не переделывание, а оптимизация уже существующего решения.
                      +1
                      Вы, очевидно, политик
                      +4
                      Проблема действительно достаточно нетривиальная.
                      У одного проекта (достаточно сложная CRM) долго думали и пробовали делать разные варианты.
                      В том числе и с хранением сериализованных данных и массивов (Postgre позволяет).

                      Остановились на 2-м варианте. В результате после работы и саппорта в течении несольких лет вывод был неутишителен — такой подход, хоть и гибок, очень сильно осложняет работу по бизнес-логике. Т.е. даже простые запросы превращаются в жутких монстров, разобраться потом с которыми дикая мука + есессно тормозит.

                      Для себя решили, что гораздо проще и приятнее использовать вариант1, даже не смотря на его ограничения.
                      Ибо при многоязычности практически всегда все сущности должны быть переведены и присутствовать в базе + есть отличная возможность сделать запрос типа «select a_defLng, a_ru, b_defLng, b_ru» и при незаполненных a_ru, брать a_defLng — достаточно удобно, ричем это даже можно делать на стороне SQL запроса.

                      Кроме того, недостаток про «изменения БД при добавлении нового языка» не такой уж страшный ИМХО — ибо БД все равно меняется в процессе эволюции ПО, да и отнести этот недостаток нужно больше к проблеме архитектинга.

                      P.S. всю статику хранили в отдельных языковых шаблонах в файлах.
                        +1
                        Практически 1-в-1 был и у нас. Иногда добавить несколько полей в каждую таблицу намного проще, чем поддерживать запросы с огромным числом джойнов.
                        0
                        Свой вариант- напишу в коментариях

                        Некоторое время писал на C++ для Windows, для себя создал приложение которое работает с .ini файлами, в которых хранятся статические переводы (в формате key=value). При этом количество языков — неограниченно.
                          0
                          А что по поводу стандартных *.po локализаций? В том же втором zend frameworke работают, так сказать «на ура». В файл сериализуются значения по столбцам «английскаяверсия-китайскаяверсия», фишка в том, что если язык приложения изначально английский, то код транслятора даже не запускается, если же нужно показать вид на китайском, то уже вызываем транслятор
                            +3
                            Это ресурсные файлы. Для интерфейса нормально, для, например, записей в блогах — очень накладно.
                            0
                            Если java и подход с ресурсными файлами (в формате key=value), то вот есть решение — http://zaval.org/products/jrc-editor/
                              0
                              Для перевода использую yandex.translate.api, кэш языковых данных хранится в /lang/md5(исходный_текст+язык_на_который_переводим).lang, если такой файл есть то никуда не лезем за переводом, и всегда можно вызвать редактор перевода на сайте и откорректировать автоматический перевод, оптимальнее варианта мультиязычности я еще не видел
                                0
                                Тоесть, наример для портала у которого есть скажем 100 публикаций — оригиалы хранятся в БД, 20 ручных переводов хранятся в файлах, для остальных 80 в случае обращения переводы гененируются автоматически и также сохраняются в файлы?
                                Нет ли у вас проблем с контролем целостности данных при таком подходе?
                                  0
                                  есть 100 статей, при первом просмотре сайта на языке отличного от исходного делаем переводы через yandex.translate.api всей информации и кэшируем, в дальнейшем берем данные из кэша, если что то в переводе не устраивает то есть интерфейс правки любой текстовой информации на странице, то есть правим не исходный текст в базе а содержимое кэш-файла, md5(исходный_текст+язык_на_который_переводим).lang
                                0
                                Тоже достаточно плотно занимался этой проблемой, особенно для заранее неопределенного количества поддерживаемых языков.

                                Живучим оказался подход, когда данные находятся во внешних ресурсах (файлах языковых меток), а их идентефикаторы — в базе. Удобство: прозрачно, легко обслуживается и поддерживается, легко подключается новый язык. Неудобство: потребляется много памяти, так как все содержимое всех файлов кешируется, веб-ориентирование такого подхода (отсутствие универсальности), сложность работы со списками.

                                Остановился на варианте 2 из предложенных. В одном проекте точно такую. В другом с изменениями: основная таблица не содержит дефолтного (обычно на англ.) значения, а языки имеют приоритеты. И при выборке значения если такового нет для нужного языка, выбирается метка с наибольшим приоритетом. Конечно, запросы сложные, но универсальность на высоте.

                                P.S. Почему везде В и С? Зачем использовать по два локализованных поля? Не понял смысл…
                                  0
                                  B и С только для примера. В таблице может быть много полей требующих перевода на разные языки. Напрмиер — Title, Description, Content и так далее.
                                  Я так понимаю в вашем случае речь идет только о переводе интерфейса? Данные не переводятся? Или просто нет необходимости одну и ту же сущность отображать на разных языках?
                                    0
                                    Сложно, кстати, разобраться в схемах из-за это. Однобуквенные переменные зло :)
                                      0
                                      Сначала как раз были вариант Field1, Field2 и Title, Description но потом подумал, что так будет лаконичнее. В реальной практике же поля в БД так никогда не заываю )
                                  +2
                                  Используем схему схожую со 2 вариантом, только таблица с переводами 1, в ней хранятся записи для всех сущностей, в формате сущность-id-свойство-язык-значение и при приходе запроса строятся временные таблицы одинаковой с основной структуры (из raw_topics на русском -> topics на нужном языке), но с переведенными значениями (кол-во таблиц и записей в них не огромно). Так было удобно локализовать уже готовую развитую систему, плюс нет необходимости в жестком наличии перевода, все будет работать, хотя запись будет не переведена. При этом переводы для шаблонов хранятся в файлах и используется gettext.
                                    0
                                    Выборка перевода идет не по ключу, а по like текстового поля, я првильно понял?
                                      0
                                      по ключу, таблица+поле+id_записи+язык (типа topics+name+11+en)
                                      0
                                      Реализовывал подобную схему, только без временных таблиц — просто выборка данных из основной таблицы и потом замена текстовых полей данными из таблицы переводов.
                                      Дополнительный плюс — не нужно модифицировать структуру БД в случае появления нового языка/сущности/колонки — достаточно просто обновить конфиг.
                                      Из минусов — для больших таблиц решение «в лоб» вряд ли подойдет, нужно отдельно думать над вопросом производительности.
                                      +1
                                      Относительно недавно был пост Локализация ASP.NET MVC приложений на эту тему. Мы используем свой фрэймворк для поддержки мультиязычности, ничего не храним в ресурсах, а все переводы добавляются прямо у нас на сайте с помощью редактора доступного для переводчиков.
                                        0
                                        Для статических переводов пока ничего лучше, чем gettext не придумали (не забываем про plural forms например).

                                        С переводами контента пока лично не сталкивался, трудно что-то предложить. Но думаю какая бы схема БД не была задействована, есть смысл спрятать реализацию за абстракцией в ORM.
                                          0
                                          Локализация интерфейса — Get text
                                          Локализация контента — метка языка в табличке в БД
                                            0
                                            Перепробовав много разных вариантов, пришел к тому, что самый простой в поддержке и реализации вариант — использование PgSQL с типом данных hstore для переводимых полей. В hstore можно хранить полноценный хеш переводов (en => name, ru => имя, ...), а не просто сериализованный массив, хранящийся как строка. При этом средствами SQL можно делать практически любые запросы, в том числе на наличие/отсутствие нужного перевода.
                                              0
                                              Добавлю ещё: можно на значения ключей повесить индексы, сделать полнотекстовый поиск опять же.
                                              А так — лучше получается вариант 3а: в таблице Entity отдельные hstore поля для каждого языка (например, ruLang, enLang), а в этих полях: ключ — имя поля таблицы (например, name, title, description), значение — перевод. И это очень удобно использовать в объектной модели и в коде (например есть возможность переопределить стандартные поля у объектов переводами из полей).
                                              +1
                                              Вариант 1.

                                              Преимущество относительно Варианта 2 — скорость выборки данных по нескольким языкам, так как избегаем перекрестных запросов.

                                              Гибкость реализуется с помощью грамотного интерфейса управления языками на сайте. При добавлении/удалении нового языка на сайте система знает какие поля необходимо добавить/удалить в таблицах.
                                                0
                                                Про локализацию БД пропущен еще один способ: когда таблица локализации Translation одна и та же для всех сущностей базы Entity1, Entity2 и т.д., и либо Entity на нее ссылается с ограничением 1:1 и BEFORE DELETE удаляющим триггером, либо же она ссылается на первичный ключ Entiry (но тогда средствами СУБД внешний ключ не создать, нужно делать на триггерах проверку, плюс надо обеспечить глобально-уникальные ID во всех табличах Entiry1, Entity2 и т.д.

                                                А про локализацию статики пропущено деление на 2 подвида: когда ключом строки выступает сама строка на базовом языке (идеология gettext), и когда ключ языковой строки — синтетический (типа index_greeting, enter_your_email и т.д.). Второй способ по моему опыту оказывается удобнее gettext, потому что исчезает базовый язык, исправление опечатки в котором приводит к проблемам в остальных локализациях (плюс можно делать легко веб-интерфейс для realtime-перевода или правки текстов на сайте).

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

                                                Самое читаемое