Skip to content

Commit 04a3362

Browse files
authored
fix(web): escaping html-encoded symbols like apostrophes in translations (#1002)
e.g. end user would see `'` from translations
1 parent 03e2fee commit 04a3362

File tree

3 files changed

+34
-3
lines changed

3 files changed

+34
-3
lines changed

web/components/I18nHost.ce.vue

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
<script lang="ts" setup>
2+
import en_US from '~/locales/en_US.json';
23
import { provide } from 'vue';
34
import { createI18n, I18nInjectionKey } from 'vue-i18n';
5+
import { createHtmlEntityDecoder } from '~/helpers/i18n-utils';
46
5-
import en_US from '~/locales/en_US.json';
67
// import ja from '~/locales/ja.json';
78
89
const defaultLocale = 'en_US'; // ja, en_US
@@ -34,7 +35,9 @@ const i18n = createI18n<false>({
3435
en_US,
3536
// ja,
3637
...(nonDefaultLocale ? parsedMessages : {}),
37-
}
38+
},
39+
/** safely decodes html-encoded symbols like &amp; and &apos; */
40+
postTranslation: createHtmlEntityDecoder(),
3841
});
3942
4043
provide(I18nInjectionKey, i18n);

web/helpers/i18n-utils.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* Creates a post-translation function that decodes HTML entities in translated strings.
3+
* This function is typically used with createI18n to handle HTML-encoded translations.
4+
*
5+
* @returns A function that takes a translated value and decodes any HTML entities if it's a string.
6+
* If the input is not a string, it returns the original value unchanged.
7+
*
8+
* @example
9+
* const decode = createHtmlEntityDecoder();
10+
* decode("&amp;"); // Returns "&"
11+
* decode(123); // Returns 123
12+
* const i18n = createI18n({
13+
* // ... other options
14+
* postTranslation(translated) {
15+
* return decode(translated);
16+
* },
17+
* });
18+
*/
19+
export const createHtmlEntityDecoder = () => {
20+
const parser = new DOMParser();
21+
return <T>(translated: T) => {
22+
if (typeof translated !== 'string') return translated;
23+
const decoded = parser.parseFromString(translated, 'text/html').documentElement.textContent;
24+
return decoded ?? translated;
25+
};
26+
};

web/plugins/i18n.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { createI18n } from 'vue-i18n';
22

3-
import en_US from '@/locales/en_US.json';
3+
import en_US from '@/locales/en_US.json';
4+
import { createHtmlEntityDecoder } from '~/helpers/i18n-utils';
45

56
export default defineNuxtPlugin(({ vueApp }) => {
67
const i18n = createI18n({
@@ -11,6 +12,7 @@ export default defineNuxtPlugin(({ vueApp }) => {
1112
messages: {
1213
en_US,
1314
},
15+
postTranslation: createHtmlEntityDecoder(),
1416
});
1517

1618
vueApp.use(i18n);

0 commit comments

Comments
 (0)