Как стать автором
Обновить
111.55
Леруа Мерлен
Мы строим технологическую компанию-платформу.

Как зеленый Леруа Мерлен стал желтым Лемана ПРО

Время на прочтение13 мин
Количество просмотров1.2K

В последнее время ну очень популярна стала такая вещь, как ребрендинг. Одни меняют лого и название. Другие, вдобавок к этому, еще и кардинально меняют цвета.

Популярность эта, однако, не сопровождается должным количеством технических статей, которые мне, как разработчику, наиболее интересны. 

Будем это исправлять.

Всем привет! Меня зовут Слава. Я мобильный разработчик. И в этой статье я поделюсь своим уникальным опытом того, как Леруа Мерлен стал Лемана ПРО. Собственно, рассказывать буду про мобильную разработку 🤷🏽.

Спойлер. Уникальность заключается в том, что примерно в одного человека нужно было "отребрендить" и Android, и iOS. Ну и в статье будет рассказываться про обе платформы соответственно тоже.

Чего делать

Давайте определимся, в чем же заключается ребрендинг?

До -> После.(Скрины с Android)
До -> После.
(Скрины с Android)

Многим может показаться, что изменились только цвета. По большому счету так и есть, но это далеко не все визуальные изменения. Также нужно было:

  • Заменить брендовые картинки.

  • Обновить название.

  • Актуализировать lottie анимации.

Итого (вместе с цветами) — 4 простых действия.

Глава 1. Цвета

Немного цифр.

У нас есть 2 клиентских приложения. По одному на Android и iOS.

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

На момент начала ребрендинга в Android у нас было 145 скринов на Jetpack Compose и 4 скрина на Android View с версткой в .xml. Последнее — legacy и техдолг.

В iOS же дела обстояли следующим образом:

  • Storyboard33 скрина. Cюда включены все .storyboard, .nib, .xib файлы и так далее. Legacy, на котором не пишем ничего нового.

  • UIKit кодом — 72 скрина. Не декларатив, конечно, но достаточно просто и надежно.

  • SwiftUI21 скрин. Вечная технология будущего, которая у нас выбрана как ориентир.

Статистика по скринам. Слева — Android, справа — iOS.
Статистика по скринам. Слева — Android, справа — iOS.
Скрытый текст

в Android

в iOS

1

Android View с версткой в .xml

Storyboard

2

Android View без верстки в .xml

UIKit кодом

3

Jetpack Compose

SwiftUI

.storyboard в iOS — это в прямом смысле .xml файл. Код здесь, однако, куда менее читабелен, чем в Android.

Верстка на UIKit кодом напоминает верстку с ConstraintLayout в Android. Но на деле прямым аналогом будет верстка совсем без .xml.

Ну а про декларативные SwiftUI и Jetpack Compose вы, наверняка, и так слышите на каждом шагу. Концептуально вещи очень похожие, хоть и есть различия (и порой большие).

К чему все это? Код пишется по-разному и для разных способов верстки попросту несовместим. Точнее, трудно совместим.

Например, в Jetpack Compose используемые из дизайн-системы цвета у нас поддерживают смену светлой-темной темы, а в .xml — нет. Да, конечно, можно добавить этот функционал и для .xml, но зачем, если последнее — legacy...

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

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

Не уходя далеко хотелось бы познакомить с таким термином, как токен. Токен — это минимальный юнит дизайн-системы, который используют дизайнеры для интеграции стилистики в разработку. Он, как правило, имеет семантическое название. Так, токен может обзываться как цвет для основного текста, md.ref.palette.error40 или же Fronton.color.backgroundPrimary.

А вообще понятие токена очень растяжимое, и каждый трактует его как хочет.

Фрагмент Figma c токенами цветов.Токены разбиты на какие-то категории (здесь Brand, Background, Text).Токены называем как BackgroundPrimary, TextSecondary, BrandInvert и так далее.
Фрагмент Figma c токенами цветов.
Токены разбиты на какие-то категории (здесь Brand, Background, Text).
Токены называем как BackgroundPrimary, TextSecondary, BrandInvert и так далее.

Будь у нас идеальный вариант, задача по смене палитры заняла бы от силы день. Увы, все было не совсем так.

Что по Android части, что по iOS, в отношении цветов имели следующее:

  • Одни — из дизайн-системы.

  • Другие — захардкожены.

  • Третьи — вообще цвета из SDK.

Кейсы с цветами из и не из дизайн-системы.
Кейсы с цветами из и не из дизайн-системы.

Огромным "подарком" стало еще и то, что все цвета в .storyboard'ах были захардкожены. Вообще все. А дополнительную сложность накладывало то, что цвета здесь могли описываться по-разному, в зависимости от colorSpace.

Вообще разные colorSpace'ы теоретически можно было встретить и в .swift файлах, но у нас такого не было:

  • UIColor(red: 0.12, green: 0.34, blue: 0.56, alpha: 1.0)

  • UIColor(white: 0.5, alpha: 1.0)

<color 
  key = "textColor" 
  colorSpace="calibratedRGB" 
  alpha="1" 
  blue="0.58431372550000005" 
  green="0.58431372550000005" 
  red="0.58431372550000005"
/>
<color
  key = "backgroundColor"
  colorSpace = "custom"
  customColorSpace = "genericGamma22GrayColorSpace"
  alpha = "1"
  white = "1"
/>

Это еще отформатировано, чтобы проще было читать. А так все записывается в одну строку.

Вообще первая идея по перекраске (не моя) заключалась в том, чтобы взять и везде заменить все цвета А на цвета Б. Например, если раньше был #5AB030, а стал #FDC300, то нужно просто средствами IDE заменить все #5AB030 на #FDC300.

Это простейшее решение, но у него есть ряд недостатков:

  1. Лишены всех плюсов хранения цветов только в дизайн-системе.

  2. Добавившиеся или убавившиеся пересечения между токенами необходимо разрешать руками. На скрине ниже пример того, что это такое.

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

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

Пересечения на "До -> После".
Пересечения на "До -> После".

Недостатки недостатками, но с iOS в самом начале мы так и поступили. В качестве быстрой демки для дизайнеров — круто, но как конечное решение для пользователей — нет. Где-то цвета были не те. Где-то появились странные белые подложки, которых не должно было быть.

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

И снова к цифрам. Ниже приведена статистика по количеству бесхозных цветов, с которыми предстояло работать:

  • 69 — в .xml файлах

  • 113 — в .kt файлах (Jetpack Compose + Android View)

  • 573 — в .swift файлах (UIKit кодом + SwiftUI)

  • 857 — в .storybard файлах и в его товарищах

Не очень-то почитают использование цветов из дизайн-системы на iOS =(
Не очень-то почитают использование цветов из дизайн-системы на iOS =(

Сами по себе цифры и так достаточно большие. Неплохо бы еще и понимать то, как происходит привязка цвета. Для этого нужно:

  1. Получить его hex и alpha.

  2. Пойти в Figma дизайн-системы, где лежат все токены с цветами.

  3. Найти токен с идентичным или приближенным цветом (на основе hex и alpha из п.1).

  4. Заиспользовать найденный токен в коде.

Алгоритм вроде бы простой, однако есть нюансы.

Вернемся немного назад. Ранее я уже писал, что цвета в .storyboard'ах задаются по-разному в зависимости от colorSpace. Из-за этого и hex будет вычисляться по-разному.

Вообще нетривиальное получение hex характерно не только для .storyboard'ов, но также и для всего остального. Вот несколько примеров:

  • Color.DarkGray в Jetpack Compose.
    Соответствует #444444, но нужно лезть в SDK.

  • Color.gray в SwiftUI.
    Соответствует #8E8E93, но нужно еще знать, где смотреть.

  • Color.argb(12, 34, 56, 1) в Android View.
    Соответствует #0C2238, но нужно вычислять либо вручную, переводя каждую составляющую из decimal в hexadecimal, либо через Figma.

  • UIColor(red: 0.12, green: 0.34, blue: 0.56, alpha: 1.0) в UIKit. Соответствует #1F578F, но нужно сначала помножить значения на 255, а потом уже перевести, как в предыдущем пункте.

И даже если мы найдем hex, то одному значению может соответствовать сразу несколько токенов. Что в этом случае делать? Если есть понимание семантического значения — проставлять соответствующий токен. Например, (у нас) для текстов используются в первую очередь цвета из Text категории, но никак не из Background. Если же непонятно, что проставлять, нужно идти к дизайнерам.

Получили белый цвет.
Получили белый цвет.
Но какой?
Но какой?

Цифры по количеству бесхозных цветов и описание нюанса здесь присутствуют исключительно с одной целью: показать, что привязка руками потребует очень много времени и сил.

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

Были написаны скрипты, которые предоставляли аналитическую информацию: сканировали файлы проекта, по regex находили бесхозные цвета и выводили файл, номер строки, hex и alpha. Всего 4 скрипта, под каждый тип файлов: .storyboard, .swift, .kt и .xml. У каждого скрипта свои правила перевода в hex и alpha.

Пример работы скрипта:

LMFieldView.xib:51 / [color=#333333, alpha=1.00]
LMFieldView.xib:64 / [color=#F0F1F2, alpha=1.00]
LMFieldView.xib:70 / [color=#000000, alpha=0.00]
LMFieldView.xib:96 / [color=#FFFFFF, alpha=1.00]
FeedbackCheckLicenseTableViewCell.xib:26 / [color=#B7B7B7, alpha=1.00]
FeedbackCheckLicenseTableViewCell.xib:33 / [color=#53C43F, alpha=1.00]
FeedbackHeaderPhotosTableViewCell.xib:27 / [color=#000000, alpha=1.00]
FeedbackHeaderPhotosTableViewCell.xib:31 / [color=#000000, alpha=0.00]
FeedbackHeaderPhotosTableViewCell.xib:49 / [color=#959595, alpha=1.00]
FeedbackMultiTextTableViewCell.xib:27 / [color=#000000, alpha=0.00]
FeedbackPhotosTableViewCell.xib:34 / [color=#000000, alpha=0.00]
FeedbackSendButtonTableViewCell.xib:22 / [color=#53C43F, alpha=1.00]

А вот как выглядела LMFieldView на 51й строке (отформатировано, расписано в несколько строк для наглядности):

<color
  key="textColor"
  red="0.20000000000000001"
  green="0.20000000000000001"
  blue="0.20000000000000001"
  alpha="1"
  colorSpace="calibratedRGB"
/>

Стоит отметить, что скрипт также выводил цвета из дизайн-системы, чтобы было проще искать совпадения.

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

Оставались .storyboard'ы. К ним (наверное, и из-за сложившейся нелюбви) мне довелось подобраться в последнюю очередь.

Что здесь было не так? Оказалось, что нельзя где-то вовне объявить цвет и ссылаться на него из .storyboard'а. Такого механизма попросту нет (либо плохо искал). Цвет всегда будет храниться в каждом файле индивидуально.

Казалось, что здесь можно только поменять одни захардкоженные значения на другие. Оказалось, однако, что это не совсем так. Есть приемлемая альтернатива — Named colors:

<capability name="Named colors" minToolsVersion="9.0"/>

Данный функционал позволяет обзывать цвет и использовать его как ссылку в рамках одного файла. То есть мы можем сделать формальную привязку:

  • Определить hex и alpha заменяемого цвета (это мы уже умеем на основе скрипта).

  • Взять название цвета в дизайн-системе.

  • В файл добавить именованный цвет, имя и значения которого задаются на основе дизайн-системы.

Пример использования Named colors.
Пример использования Named colors.

Но фактической привязки у нас так и не состоялось. Если обновятся цвета в библиотеке дизайн-системы, то эти изменения никак не подтянутся в .storyboard файлы. По этой причине появился еще один скрипт, который гулял по подобным файлам, находил именованные цвета и, если они соответствовали дизайн-системе, обновлял их значения.

У нас используется Cocoapods. Можно было бы закрепить выполнение последнего скрипта за фазой post_install и вообще забыть про его существование.

Итак, все готово к тому, чтобы брать и менять цвета в .storyboard 'ах руками.

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

XCode. Интерактивная панель.
XCode. Интерактивная панель.

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

Проблема интерактивной панели в том, что такие замены занимают много времени. Даже если условно на 1 замену тратить по 3 минуты (а это приуменьшенное время), то потребуется 43 часа на рутинную работу по 857 кейсам. А еще стоит учесть, что некоторые кейсы занимали и по 15 минут. Это связано с тем, что иногда не совсем понятно, на какой View нужно поменять цвет, так как необходимо сопоставлять нечитабельный код с визуализацией. Более того, даже найдя View неплохо бы еще и найти характеристику, у которой меняется тот самый цвет. А характеристика, в свою очередь, могла тоже лежать на какой-то другой вкладке интерактивной панели.

В итоге скрипт для анализа .storyboard'ов научился не только выдавать информацию, но еще и подменять цвета. Причем местами хитрым способом:

  • Для текстов выбирал сначала цвета из Text категории.

  • Цвета Pressed категории игнорировались при наличии токенов с идентичным значением.

  • Hex и alpha могли отличаться от тех, которые в дизайн-системе, основываясь на небольшую допустимую погрешность.

Цвет в диапазоне с погрешностью 5%.
Цвет в диапазоне с погрешностью 5%.

Итог:

  1. Все цвета привязаны.

  2. Обновление цветов по приложению сводится к обновлению значений токенов в библиотеках дизайн-системы.

  3. Есть скрипты, проверяющие код на предмет привязок из п.1. Их можно переиспользовать в качестве линтеров.

Теперь потенциальная задача по перекраске приложения с зеленого, скажем, в оранжевый, займет порядка часа, а не месяца-двух. Как финал главы, перекрашиваем приложение за 2 минуты.

Глава 2. Картинки

Что в Android, что в iOS разработке (да и, наверное, в любой другой) все картинки можно разбить на две категории:

  • Те, которые подлежат покраске (он же tint). Такие штуки называю иконками.

  • Те, которые ни в коем случае нельзя красить. Представителей этой категории называю иллюстрациями.

Первые, на мой взгляд, должны всегда явно краситься в какой-то конкретный цвет. Таким образом, любая картинка либо всегда красится (и является иконкой), либо всегда не красится (и является иллюстрацией).

Иконки

Иконки у нас хранятся в библиотеках дизайн-системы.

Фрагмент Figma с иконками
Фрагмент Figma с иконками

Однако они используются не везде. Где-то используются legacy иконки, лежащие локально в проекте. Причем есть здесь несколько дополнительных проблем:

  • Такие иконки могут содержать дубликаты. Дубликаты эти могут либо незначительно отличаться, либо не отличаться вовсе.

  • Какие-то иконки уже содержат конкретный цвет и не красятся в дальнейшем (что, на мой взгляд, неверно).

  • Иконки могут отличаться в размерах.

15 дублей правого шеврона в iOS.С голубой рамкой — .pdf, без — .svg
15 дублей правого шеврона в iOS.
С голубой рамкой — .pdf, без — .svg
А это в приложении. Можно заметить разные размеры, скругления и толщину иконок.(Скрины с iOS)
А это в приложении. Можно заметить разные размеры, скругления и толщину иконок.
(Скрины с iOS)

Подобно цветам работа здесь заключалась в синхронизации с библиотеками дизайн-системы: нужно было избавиться от локальных иконок. Какие-то скрипты здесь показались избыточными, поэтому все было сделано руками.

Был, правда, один баг с .storyboard'ами. Иконки из библиотеки нормально отображались в интерактивной панели. Смена иконки также отрабатывала на ура и меняла визуальный результат.

XCode. Пример смены иконки.
XCode. Пример смены иконки.

Тем не менее при запуске приложения иконки не показывались. В лог сыпала ошибка c текстом:

Could not load the "arrows__chevron_right__basic" image referenced from a nib in the bundle with identifier "ru.leroymerlin.ios"

Фикс: скрипт, который в post_install фазу Cocoapods копирует .xcassets из библиотеки в проект.

Иллюстрации

Иллюстрации у нас не хранятся в дизайн-системе, поэтому алгоритм работы будет несколько другим:

  1. Избавляемся от идентичных дублей.

  2. Собираем все иллюстрации по приложениям.

  3. Отправляем собранные иллюстрации дизайнерам на обновление.

  4. Получаем новые иллюстрации.

  5. Заменяем.

  6. Готово.

С иллюстрациями и дублями был один интересный момент. Были такие, которые визуально выглядели по-разному, стилистически могли сильно отличаться, но служили для одной и той же миссии. Это то, что осталось как часть legacy.

В приложении.Слева — разные картинки при отсутствии интернета, как и UI в целом.Справа — разные пины карты в разных частях приложения.(Скрины с iOS / Android)
В приложении.
Слева — разные картинки при отсутствии интернета, как и UI в целом.
Справа — разные пины карты в разных частях приложения.
(Скрины с iOS / Android)

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

Стоит отметить, что к единообразию пришли и куда более значимые картинки: пины для карт. В приложении 3 места, где используются карты. В каждом из таких мест ранее использовались свои незначительно отличающиеся пины.

С дублями разобрались. Дальше нужно все собрать и выдать в дизайн на корректировку.

Выдать иллюстрации — это, конечно, хорошо. Но как насчет того, чтобы их можно было открыть (хотя бы для просмотра нужных размеров)? Часть выдаваемых векторок была в .pdf и .xml форматах, которые та же Figma просто так скушать не могла. На помощь пришли они:

Кроме векторных иллюстраций может потребоваться еще и выдать растровые. Последние, что на Android, что на iOS, образуют пак из нескольких идентичных картинок в разных размерах. Дизайну нужны файлы в размере x1. Но что есть файл в размере х1?

В iOS иллюстрации, в рамках пака, находятся в одной папке. Множители прописываются здесь же в Contents.json. На крайний случай там можно найти нужный файл. Но, как правило, это не требуется, так как названия картинок будут отличаться, а картинка в размере x1 будет иметь какой-нибудь суффикс типа @1, x1, -1 или не иметь суффикса вовсе.

{
  "images" : [
    {
      
      "filename" : "pro-card.png",
      "idiom" : "universal",
      "scale" : "1x"
    },
    {
      "filename" : "pro-card-x2.png",
      "idiom" : "universal",
      "scale" : "2x"
    },
    {
      "filename" : "pro-card-x3.png",
      "idiom" : "universal",
      "scale" : "3x"
    }
  ],
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}

В Android же иллюстрации разбрасываются по разным общим размерным папкам (drawable/hdpi, drawable/xhdpi и так далее), образуя что-то вроде "размерных маек". Здесь нужно помнить, что множителю x1 соответствует папка mdpi и именно оттуда нужно забирать и выдавать картинку на дизайн.

Множители.
Множители.

Эта часть, про растр, приводится в статье исключительно как памятка. В свое время я выдал картинки из Android в hdpi, а потом пришлось перезапрашивать.

Глава 3. Название

Одна из самых быстрых задач — поменять название.

Через IDE это делается ну очень быстро и очень приятно. Единственное, после такой замены нужно было откатить текст, используемый в качестве названия для самого launcher'a приложения.

И здесь не обошлось без приключений, пусть и небольших.

У нас очень недолго был баг с двойным названием. Суть была в том, что с сервера нам прилетал текст, содержащий имя компании в названии магазина. На клиенте эта часть затиралась. Получалась новая строка, без имени компании, которую использовали далее под свои нужды. Так Леруа Мерлен Выхино превращалось в Выхино.

Повсеместная замена имени бренда через IDE, к сожалению, "поправила" и магию, описанную выше. И из-за этого часть Леруа Мерлен не вырезалась. Более того, еще и добавлялось новое название...

Баг с двойным названием.
Баг с двойным названием.

Стоит отметить, что была еще и схожая проблема с deeplink'ами, в том плане, что логика с названием лежала где-то глубоко на клиенте.

Глава 4. Lottie анимации

Анимации во время ребрендинга нужно обновлять хотя бы по той причине, что они могут содержать в себе старый брендинг (цвета, названия, логотипы).

Здесь алгоритм максимально схож с кейсом с иллюстрациями: собираем, отдаем, получаем, обновляем.

Как и вообще со всем остальным, здесь пришлось поработать напильником. Дело в том, что от QA прилетел баг, что новая анимация работает криво.

LottieFiles — сервис для просмотра Lottie анимаций.
LottieFiles — сервис для просмотра Lottie анимаций.

На скрине представлены примеры новой и старой анимации. Выглядит синхронно, но есть нюанс. Дело в том, что почему-то "поехало" количество фреймов в анимации.

Решение: в коде смещаем критические фреймы в соответствии с новыми анимациями.

Более правильное решение: перезапрашиваем анимации.

За кулисами

Фактически на этом по задачам все. Однако были некоторые интересные моменты, о которых я бы и хотел еще немного поведать.

Компоненты не из дизайн системы

Серьезная проблема заключается в том, что в приложении используются компоненты не из дизайн системы. По дизайну в Figma используется компонент из дизайн системы, но в разработке все иначе. Формально проблему можно разбить на две:

  • Реализация компонента есть в библиотеке, но почему-то разработка ее не использует.

  • Реализации компонента нет в библиотеке и разработка имплементирует компонент в каждом месте индивидуально как хочет.

Первая проблема решается переездом на компонент из дизайн системы. Но само наличие такой проблемы должно насторожить и заставить задуматься о создании линтеров. Например, кейсом с такой проблемой в Android было использование material.Button, а не FrontonButton. Значит, пишем линтер, который на корню блокирует использование material.Button. И так можно поступить вообще со всеми компонентами. А можно поступить еще проще и в текущем примере выпилить зависимость на Material, однако это не всегда возможно.

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

Организационные моменты

Разработка велась под секретом. И от других разработчиков в том числе.

Пушить изменения было нельзя. В таких условиях QA не могли переключиться на какую-то ветку и что-то там потестировать. Приходилось скидывать архивы с исходниками. Причем в архиве были исходники не только проекта, но еще и библиотек дизайн-системы.

Из-за этой же секретности по готовности нельзя было просто взять и влить изменения в рабочую ветку. Нужна была отмашка по релизу. Пришлось достаточно долго поддерживать жизнеспособность данного творения локально. Чем больше проходило времени — тем больше конфликтов появлялось. А эти конфликты порой решались небыстро.

Старые скрипты — новые линтеры

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

Скрипты эти были написаны на Kotlin. В идеале их надо было писать на том же Python, чтобы потом спокойно можно было встроить в CI/CD. Я же в свое время не думал, что они потом когда-то пригодятся. Если (вдруг) вы столкнетесь с подобным кейсом, желание запускать с терминала Kotlin скрипты никуда не уйдет, а переписывать все это добро вы не хотите, то выход есть:

kotlinc myScript.kt -include-runtime -d myJar.jar
java -jar myJar.jar

На этом точно все! Спасибо за прочтение!

Теги:
Хабы:
+3
Комментарии9

Публикации

Информация

Сайт
leroymerlin.ru
Дата регистрации
Дата основания
2004
Численность
свыше 10 000 человек
Местоположение
Россия
Представитель
Nastianastasia