Merge pull request #29786 from backstage/rugvip/i18n-jsx

core-{app,plugin}-api: add support for JSX in translation messages
This commit is contained in:
Patrik Oldsberg
2025-05-02 15:27:53 +02:00
committed by GitHub
11 changed files with 391 additions and 60 deletions
@@ -94,6 +94,52 @@ describe('MockTranslationApi', () => {
expect(snapshot.t('foo', { qux: 'Deep' })).toBe('Foo Nested Baz Deep');
});
it('should support jsx interpolation', () => {
const snapshot = snapshotWithMessages({
empty: 'derp',
jsx: '={{ x }}',
jsxNested: '={{ x.y.z }}',
jsxDeep: '<$t(jsx)>',
});
expect(snapshot.t('jsx', { x: <h1>hello</h1> })).toMatchInlineSnapshot(`
<React.Fragment>
=
<h1>
hello
</h1>
</React.Fragment>
`);
expect(snapshot.t('jsx', { replace: { x: <h1>hello</h1> } }))
.toMatchInlineSnapshot(`
<React.Fragment>
=
<h1>
hello
</h1>
</React.Fragment>
`);
expect(
snapshot.t('jsxNested', { replace: { x: { y: { z: <h1>hello</h1> } } } }),
).toMatchInlineSnapshot(`
<React.Fragment>
=
<h1>
hello
</h1>
</React.Fragment>
`);
expect(snapshot.t('jsxDeep', { x: <h1>hello</h1> })).toMatchInlineSnapshot(`
<React.Fragment>
&lt;=
<h1>
hello
</h1>
&gt;
</React.Fragment>
`);
});
it('should support formatting', () => {
const snapshot = snapshotWithMessages({
plain: '= {{ x }}',
@@ -16,7 +16,6 @@
import {
TranslationApi,
TranslationFunction,
TranslationRef,
TranslationSnapshot,
} from '@backstage/core-plugin-api/alpha';
@@ -27,6 +26,8 @@ import { Observable } from '@backstage/types';
// Internal import to avoid code duplication, this will lead to duplication in build output
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { toInternalTranslationRef } from '../../../../../core-plugin-api/src/translation/TranslationRef';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { JsxInterpolator } from '../../../../../core-app-api/src/apis/implementations/TranslationApi/I18nextTranslationApi';
const DEFAULT_LANGUAGE = 'en';
@@ -41,6 +42,8 @@ export class MockTranslationApi implements TranslationApi {
supportedLngs: [DEFAULT_LANGUAGE],
interpolation: {
escapeValue: false,
// Used for the JsxInterpolator format hook
alwaysFormat: true,
},
ns: [],
defaultNS: false,
@@ -55,14 +58,18 @@ export class MockTranslationApi implements TranslationApi {
throw new Error('i18next was unexpectedly not initialized');
}
return new MockTranslationApi(i18n);
const interpolator = JsxInterpolator.fromI18n(i18n);
return new MockTranslationApi(i18n, interpolator);
}
#i18n: I18n;
#interpolator: JsxInterpolator;
#registeredRefs = new Set<string>();
private constructor(i18n: I18n) {
private constructor(i18n: I18n, interpolator: JsxInterpolator) {
this.#i18n = i18n;
this.#interpolator = interpolator;
}
getTranslation<TMessages extends { [key in string]: string }>(
@@ -81,10 +88,9 @@ export class MockTranslationApi implements TranslationApi {
);
}
const t = this.#i18n.getFixedT(
null,
internalRef.id,
) as TranslationFunction<TMessages>;
const t = this.#interpolator.wrapT<TMessages>(
this.#i18n.getFixedT(null, internalRef.id),
);
return {
ready: true,