diff --git a/README.md b/README.md index 8282d31e6..f77c25174 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,32 @@ t("my-key", { }) ``` +#### HTML Interpolation + +Our translation system doesn't allow HTML tags in the translation strings, and it is recommended to use placeholders instead. For example, a string containing a link should be written like `This is a {{#a}}link{{/a}}` instead of `This is a link`, which is the default behavior of the `react-i18next` library. To achieve this, we use can use the `Trans` component provided by the `react-i18next`, using the [components prop](https://react.i18next.com/latest/trans-component#alternative-usage-which-lists-the-components-v11.6.0) and passing the `customMarkup` post processor that replaces the placeholders with the actual HTML tags. + +Example with a link: +```tsx +, + }} + tOptions={{ postProcess: "customMarkup" }} +/> +``` + +Example with a self-closing tag: +```tsx + }} + tOptions={{ postProcess: "customMarkup" }} +/> +``` + #### String extraction The `bin/extract-strings.mjs` script can be used to extract translation strings from the source code and put them in the YAML file that is picked up by our internal translation system. The usage of the script is documented in the script itself. diff --git a/src/modules/shared/i18n/customMarkupPlugin.test.ts b/src/modules/shared/i18n/customMarkupPlugin.test.ts new file mode 100644 index 000000000..79cddd131 --- /dev/null +++ b/src/modules/shared/i18n/customMarkupPlugin.test.ts @@ -0,0 +1,38 @@ +import { customMarkupPlugin } from "./customMarkupPlugin"; + +describe("customMarkupPlugin", () => { + it("should convert interpolation tags to HTML", () => { + const value = "{{#strong}}Hello{{/strong}} {{#br/}}"; + expect(customMarkupPlugin.process(value, "my_key", {}, null)).toBe( + "Hello
" + ); + }); + + it("should convert interpolation tags to HTML with multiple tags", () => { + const value = "{{#strong}}Hello{{/strong}} {{#br/}} {{#em}}World{{/em}}"; + expect(customMarkupPlugin.process(value, "my_key", {}, null)).toBe( + "Hello
World" + ); + }); + + it("should not convert interpolation tags to HTML with nested tags", () => { + const value = "{{#strong}}Hello {{#em}}World{{/em}}{{/strong}}"; + expect(customMarkupPlugin.process(value, "my_key", {}, null)).toBe( + "Hello {{#em}}World{{/em}}" + ); + }); + + it("should not convert interpolation tags to HTML with unclosed tags", () => { + const value = "{{#strong}}Hello"; + expect(customMarkupPlugin.process(value, "my_key", {}, null)).toBe( + "{{#strong}}Hello" + ); + }); + + it("should not convert interpolation tags to HTML with different opening an closing tags", () => { + const value = "{{#b}}Hello{{/strong}}"; + expect(customMarkupPlugin.process(value, "my_key", {}, null)).toBe( + "{{#b}}Hello{{/strong}}" + ); + }); +}); diff --git a/src/modules/shared/i18n/customMarkupPlugin.ts b/src/modules/shared/i18n/customMarkupPlugin.ts new file mode 100644 index 000000000..2e232fa86 --- /dev/null +++ b/src/modules/shared/i18n/customMarkupPlugin.ts @@ -0,0 +1,23 @@ +import type { PostProcessorModule } from "i18next"; + +const RANGE_REGEX = /{{#(\w+)}}(.*?){{\/\1}}/g; +const SELF_CLOSING_REGEX = /{{#(\w+)\/}}/g; + +/** + * Custom i18next post processor to replace range placeholders to HTML tags. + * + * For example, `{{#strong}}Hello{{/strong}}` will be converted to `Hello`, + * and `{{#br/}}` will be converted to `
`. + * + * The first format is used in our translations strings, while the second format is the one + * used in the Trans component from the `react-i18next` library. + */ +export const customMarkupPlugin: PostProcessorModule = { + type: "postProcessor", + name: "customMarkup", + process(value: string) { + return value + .replace(RANGE_REGEX, "<$1>$2") + .replace(SELF_CLOSING_REGEX, "<$1/>"); + }, +}; diff --git a/src/modules/shared/i18n/initI18next.ts b/src/modules/shared/i18n/initI18next.ts index c530867ee..a1119dba0 100644 --- a/src/modules/shared/i18n/initI18next.ts +++ b/src/modules/shared/i18n/initI18next.ts @@ -1,17 +1,21 @@ import i18next from "i18next"; import { initReactI18next } from "react-i18next"; +import { customMarkupPlugin } from "./customMarkupPlugin"; export function initI18next(locale: string) { - i18next.use(initReactI18next).init({ - resources: { - [`${locale}`]: {}, - }, - lng: locale, - lowerCaseLng: true, - interpolation: { - escapeValue: false, - }, - keySeparator: false, - pluralSeparator: ".", - }); + i18next + .use(initReactI18next) + .use(customMarkupPlugin) + .init({ + resources: { + [`${locale}`]: {}, + }, + lng: locale, + lowerCaseLng: true, + interpolation: { + escapeValue: false, + }, + keySeparator: false, + pluralSeparator: ".", + }); }