Skip to content

Commit

Permalink
chore(i18n): added support for custom markup
Browse files Browse the repository at this point in the history
  • Loading branch information
Fredx87 committed Jan 6, 2025
1 parent 6840432 commit 8e5f323
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 12 deletions.
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <a>link</a>`, 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
<Trans
i18nKey="my-key"
defaults="This is a link: {{#a}}Click here{{/a}}"
components={{
a: <Anchor href="https://example.com" target="_blank" isExternal />,
}}
tOptions={{ postProcess: "customMarkup" }}
/>
```

Example with a self-closing tag:
```tsx
<Trans
i18nKey="my-key"
defaults="This is an icon: {{#icon/}}"
components={{ icon: <MyIcon /> }}
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.
Expand Down
38 changes: 38 additions & 0 deletions src/modules/shared/i18n/customMarkupPlugin.test.ts
Original file line number Diff line number Diff line change
@@ -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(
"<strong>Hello</strong> <br/>"
);
});

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(
"<strong>Hello</strong> <br/> <em>World</em>"
);
});

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(
"<strong>Hello {{#em}}World{{/em}}</strong>"
);
});

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}}"
);
});
});
23 changes: 23 additions & 0 deletions src/modules/shared/i18n/customMarkupPlugin.ts
Original file line number Diff line number Diff line change
@@ -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 `<strong>Hello</strong>`,
* and `{{#br/}}` will be converted to `<br/>`.
*
* 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</$1>")
.replace(SELF_CLOSING_REGEX, "<$1/>");
},
};
28 changes: 16 additions & 12 deletions src/modules/shared/i18n/initI18next.ts
Original file line number Diff line number Diff line change
@@ -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: ".",
});
}

0 comments on commit 8e5f323

Please sign in to comment.