Рано или поздно у крупных проектов наступает такой этап, когда достигнут потолок по аудитории или финансовым показателям и нужно как-то расти дальше. И вот в один прекрасный день сияющие коллеги приходят к тебе со словами «Мы тут решили выйти на рынок США/Китая/СНГ…».
Если архитектура сайта изначально проектировалась с учетом международного развития — это хорошо. Однако если за все время жизни проекта вы концентрировались на том, что нужно «здесь и сейчас» и никто не задумывался о том, чтобы когда-нибудь вынести бизнес за пределы России, то могут возникнуть сложности.
Я тимлид финансового маркетплейса «Выберу.ру», сейчас допиливаю казахстанскую версию сайта на .NET. Хочу рассказать о нюансах, которые всплывают при масштабировании на другую страну, и о способах их поправить.
Полагаю, в ближайшее время все больше компаний будут рассматривать для себя зарубежные рынки. Если вам тоже предстоит адаптировать огромные объемы кода, который до этого годами создавался только для России — буду рад, если мой опыт окажется полезен.
Первая проблема возникает на самом старте, если ваш сайт использует городские домены типа moskva.(домен).ru или tula.(домен).ru, а возможности приобрести домен первого уровня, например .kz, по каким-либо причинам нет. Как определять страну в этом случае?
Есть сущность регионов (Region), которая упрощенно выглядит так:
Под Region у нас были представлены все географические сущности (страны, республики, области, города и т. д.). Мы добавили свойство CountryId и записали туда идентификатор региона-страны. Россия была добавлена первой (у неё Id = 1), Казахстан добавили позже. Теперь в базе несколько записей с ParentId = null — запись уровня страны.
Теперь надо научить сайт определять страну, для которой отображается страница. Тут есть несколько моментов:
Если пользователь первый раз зашел на сайт и попал на страницу России (основной домен), то определяем его местоположение и предлагаем подтвердить наше решение.
Если пользователь сразу зашел на домен города, например, moskva.(домен).ru или tula.(домен).ru, то вопрос о местоположении пользователя не задаем, а принимаем в качестве него город домена.
Как только мы определили город пользователя, сразу можем определить и страну. Данные о городе и стране записываем в cookie. Эту запись пользователь может сменить, выбрав другой город или другую страну.
За счет этого мы можем реализовать механизм определения региона при каждом запросе по двум параметрам: из URL и из cookie, — обеспечивающим гибкость настроек. В случае с URL для определения страны и региона будем учитывать домены первого и третьего уровней соответственно. Для moskva.(домен).ru мы выберем город с Alias = «moskva». Для страны Alias = «ru» мы не найдем и используем значение по умолчанию — Россия (нюанс в том, что Alias России в нашей базе есть «www»).
Когда мы научились определять страну, то можем выводить локализованные данные.
Что это может быть? Есть универсальные параметры: денежные единицы, формат даты. В зависимости от тематики сайта могут добавиться и другие. Например, для автомобильных агрегаторов — единицы скорости и расхода топлива, для объектов недвижимости — площади и расстояния и т. д. На финансовом ресурсе, чтобы показать, скажем, вклады в России, по умолчанию нужны доллары, евро и рубли, а в Казахстане потребовалось скрыть значки рублей и показать тенге (₸).
Просто взять и сделать это может быть не так просто — у нас, например, значок рубля местами был вшит в верстку, т. к. раньше он нужен был всегда. В результате где-то не вычислялось, какую валюту надо вывести, где-то в инструментах выбора валют всегда присутствовал рубль. И это сотни страниц: отчеты, графики на калькуляторах, результаты поиска, сумма «от» и «до».
Чтобы корректно выводить такие параметры, выносим данные о странах в отдельный файл (в нашем случае localization.json) и включаем в него следующее:
название страны;
домен для страны;
имя культуры (для классов CultureInfo и RegionInfo во фреймворке .NET, помогающих форматировать даты, числа и другие параметры локализации в зависимости от страны, подробнее здесь CultureInfo Конструктор (System.Globalization) | Microsoft Learn);
список доступных валют (или других нужных вам элементов);
список разделов/функциональных возможностей сайта.
Загружаем файл при старте сайта. Данные из него доступны при выполнении запросов на сайт. Создаем для этого класс CultureOptions и получаем нужные данные для страны.
Выглядит так: LocalizationOptions localizationOptions = LocalizationOptions.Get(countryAlias).
Платформа .NET располагает расширенными методами для локализации/глобализации приложений. Приведенные классы платформы используем как:
CultureInfo — для правильного вывода сумм, разделителей списков, форматирования текста (CultureInfo Класс (System.Globalization) | Microsoft Learn).
RegionInfo — для получения значка валют (RegionInfo.CurrencySymbol Свойство (System.Globalization) | Microsoft Learn).
Большая боль локализации — потребность скрывать или показывать конкретные блоки на страницах или целые разделы, чтобы пользователи из разных стран видели актуальный для их страны контент.
Для этого можно создать систему управления конфигурацией. С ее помощью все разделы, настройки региональности, локализация вынесены в отдельный блок кода — отдельную систему, где легко указать, что показывать или скрывать (это тот самый список фич из файла localization.json).
Так можно скрыть любую опцию или раздел, который еще не работает в другой стране, — на главной и везде, откуда в него можно попасть. Если в дальнейшем его нужно будет включить, сделать это будет легче: завести в админке новые организацию/продукт для нужной страны, заполнить данные и включить раздел.
Вот пример:
LocalizationOptions localizationOptions = LocalizationFeatures.GetOptions(country.Alias);
bool requestFormEnabled = localizationOptions
.IsEnabled(LocalizationOptionsNames.ProductListing.EnableRequestForm);
Если requestFormEnabled равен true, то выводим блок.
С точки зрения масштабирования система конфигурации открывает большие преимущества. Если понадобится подключить еще одну страну, например, Таджикистан — нужно будет ходить не по всему коду, а только в его изолированную часть. С его помощью можно завести новую страну, указать ее название, домен, валюту по умолчанию и протестировать на тестовом стенде перед тем, как пустить в релиз.
Итак, мы определили страну, вывели на нужных страницах локализованные данные и настроили индивидуальный набор разделов/функциональных элементов. Остались страницы продуктов, не привязанные к конкретному региону — мы называем их региононезависимые или геонезависимые. По умолчанию они отображаются по адресу www.(домен).ru.
Раньше у нас было так:
зашли на moskva.(домен).ru — видим московские продукты;
tula.(домен).ru — тульские продукты;
(домен).ru — российские продукты.
Для Москвы родительский элемент геоструктуры — это www.(домен).ru — Россия. Если мы возьмем населенный пункт Снегири, он относится к Москве, у него родительский moskva, у того родительский www.(домен).ru — Россия. Все просто, тем более при отсутствии продуктов в Снегирях, можно показать продукты для всей России (для www). Однако теперь у нас несколько стран и просто выводить продукты для России (для www) не получится.
Если не менять логику и не научить продуктовый поиск учитывать разные страны, человек, находясь в разделе Казахстан, увидит содержимое, предназначенное для России. Для Казахстана нужно выводить не www.(домен).ru, а kz.(домен).ru как корневой узел страны пользователя. Выходит, если раньше поиск мог учитывать только один геопараметр (город), то теперь нужно закладывать два: город и страну.
Когда будем добавлять города, допустим, Астану, узловой элемент самого верхнего уровня будет kz, а не www. Соответственно, когда человек из Казахстана зайдет на страницу, которую мы считали раньше геонезависимой, он должен видеть не www.(домен).ru, а kz.(домен).ru как корневую страницу страны Казахстан.
Когда мы оперируем странами, а не городами, возникает еще одна проблема — замедление работы сайта.
В нашей базе данных хранятся города, регионы, их названия со склонениями. Эти данные загружались в Redis и вынимались при необходимости — регион использовался для формирования текстов на сайте и других вычислений. Но как только мы стали вводить новые страны и чаще обращаться к данным регионов, количество запросов к Redis выросло в разы.
Теперь данные хранятся как полноценное дерево в памяти приложения (в памяти каждого экземпляра сайта), и доступ к ним значительно ускорен.
В идеале при добавлении новой страны нужно перевести все тексты на иностранный язык. Это отдельная задача и много работы:
Вынести текст, который хранится в верстке и выдается (пункты меню, подписи на сайте) в ресурсы. Например, будем брать текст из ресурсов и выводить название меню. И вот этот ресурс мы будем писать для русского языка один, для иностранного — другой. В таком случае, переходя из страны в страну, мы будем видеть разный контент.
Подготовить SEO-тексты и описания продуктов. Тут доработка уже намного масштабнее. Сначала SEO-тексты нужно красиво написать на иностранном, оптимизировать. Потом описания продуктов в админке — проверить, адаптировать.
Нам повезло, что в Казахстане русский — официальный язык, поэтому адаптация текстов не стала критичным фактором для запуска и ушла на следующий этап доработок. Однако если вы масштабируете сайт для другого региона, скорее всего, эти работы придется внести в план до первого релиза.
К сожалению, заранее предусмотреть все возможные сценарии, в которые может занести ваш проект спустя годы, невозможно. Особенно это касается стартапов, которым «ничего не стоит» одномоментно принять решение выйти в новый продуктовый сегмент или страну.
Чаще всего в такой ситуации приходится пересматривать не отдельные части кода, а в целом логику и подход к работе — чтобы сайт не просто корректно отображался в конкретной стране, но и не упал в скорости загрузки и легко адаптировался под следующие сегменты/страны.