Если в вашем Vue-приложении переводы работают через Vue-i18n, то вы наверняка сталкивались с ситуациями, в которых приходилось добавлять перевод для фразы, которая содержит html-разметку. Чаще всего это ссылки (тег a
) или строковые контейнеры (тег span
), позволяющие сделать акцент на определенных частях фразы. В таких ситуациях самым легким решением кажется добавить фразу вместе с разметкой в JSON
с переводами, а вывод сделать через v-html
. Давайте посмотрим какие с этим есть проблемы и как можно сделать лучше.
Содержание
- Полезные ссылки
Проблемы решения с v-html
Несмотря на кажущиеся безобидность и удобство, у решения с v-html есть ряд недостатков. Давайте рассмотрим их поближе.
Наш код:
{
"payment_message": "Вы можете <a href='charge.php'>оплатить заказ</a> в течение <span>{minutes}</span> мин."
}
<template>
<p v-html="t('payment_message', { minutes: 2 })"></p>
</template>
<script setup>
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
</script>
Трудность с дальнейшей модификации
Если бы ваша разметка была внутри компонента, то при необходимости ее немного изменить (например, поменять какой-то css-класс), вы бы просто поменяли компонент. В случае с разметкой внутри фразы в файлах локализации придется менять это во всех переводах. Получится количество изменений равное числу поддерживаемых языков.
Необходимость экранирования некоторых символов
Переводы могут содержать <
, >
, &
и другие специальные символы, которые нужно экранировать или заменять на html enitities (<
, >
, &
) если вы используете v-html
. Это делает переводы менее удобными для редактирования.
Нельзя использовать компоненты вместо html-элементов
Если в определенный момент вам нужно будет заменить какой-то тег, на Vue-компонент, то это не будет работать. Vue не может работать с фразой как с шаблоном.
XSS-уязвимости
Если вы используете v-html
для вывода переведенной строки, злоумышленник может воспользоваться этим для внедрения вредоносного кода, при условии, что он имеет возможность изменять переводы. Такая опасность может возникнуть, если ваши переводы хранятся не в статическом файле, а передаются через API (хранятся в БД, например).
Для того, чтобы уязвимостью воспользоваться надо также иметь проблемы с безопасностью в других местах (например, уязвимости интерфейса редактирования фраз). Но если мы плюем на безопасность здесь, плюем на безопасность там, то, вероятно, лазеек для злоумышленника будет в избытке.
Решение 2. Разбиваем строку на части
Описание решения
И так, если мы выбрали не использовать v-html
, то возможны некоторые варианты решений. Например, мы могли бы разбить строку на части, разметку расположить в коде компонента, а фразу собирать из частей вокруг тегов html-разметки.
Предположим нам надо вывести следующую строку:
Вы можете <a href="charge.php">оплатить заказ</a> в течение <span>2</span> мин.
И тут нам понадобится не менее 4 отдельных фраз в файле с переводами, чтобы составить эту строку из частей:
{
"payment_message": {
"text_before_link": "Вы можете",
"link_text": "оплатить заказ",
"text_after_link": "в течение",
"time_unit": " мин."
}
}
В итоге в компоненте строка формировалась бы так:
<template>
<p>
{{ t("payment_message.text_before_link") }}
<a href="charge.php">{{ t("payment_message.link_text") }}</a>
{{ t("payment_message.text_after_link") }}
<span>{{ minutes }}</span>{{ t("payment_message.time_unit") }}
</p>
</template>
<script setup>
import { ref } from "vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const minutes = ref(2);
</script>
Плюсы и минусы решения
Основные проблемы этого варианта:
- Фрагментов строк слишком много, сложнее работать
- Строка в
JSON
не представляет собой единое целое, прочитать такую строку при переводе сложнее - В некоторых языках правильный порядок слов (частей фразы) будет отличаться и эту проблему вы не решите
Демонстрация последнего минуса:
Вы можете <a href="charge.php">оплатить заказ</a> в течение <span>2</span> мин.
<span>2</span>分以内に<a href="charge.php">注文を支払う</a>ことができます。
Как такое поддерживать в этом варианте?
Однако мы избавились от проблем v-html
, что уже немалый плюс.
Решение 3. Используем i18n-t
Есть решение лишенное всех предыдущих недостатков – компонент i18n-t
. Он позволят использовать синтаксис слотов, чтобы вставлять в основную фразу нужные части.
Описание решения
Посмотрим на наше примере.
{
"order_text": "оплатить заказ",
"payment_message": "Вы можете {order_action} в течение {minutes} мин."
}
{
"order_text": "注文を支払う",
"payment_message": "{minutes}分以内に{order_action}ことができます。"
}
<template>
<i18n-t keypath="payment_message" tag="p">
<template #order_action>
<a href="charge.php">{{ t("order_text") }}</a>
</template>
<template #minutes>
<span>{{ minutes }}</span>
</template>
</i18n-t>
</template>
<script setup>
import { ref } from "vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const minutes = ref(2);
</script>
Как вы можете видеть, здесь есть основная фраза в payment_message
, которая выводится благодаря i18n-t
и указанию пути к фразе в keypath
. А также здесь есть текст ссылки в order_text
. Необязательный атрибут tag="p"
указывает в какой тег нужно обернуть строку.
Плюсы и минусы решения
Здесь у нас сплошные плюсы. Решены проблемы предыдущих способов.
- Нет XSS уязвимостей
- Строка сохранила свою целостность в файлах локализации. Отдельно вынесен только текст ссылки. С этим легко работать
- Строк не так много как в предыдущем варианте
- Можно иметь свой порядок слов в любом языке
- Если надо немного изменить разметку (например, добавить css-класс к ссылке), то это можно сделать в 1 месте – компоненте. Фразы в файлах с переводами меняться не будут
Минус пока видно только один – нужно ознакомиться с тем как это работает и понимать как используются слоты.