From 813a464543bb79c794487399ed9e85444eaa2611 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 15 Sep 2023 10:42:46 +0200 Subject: [PATCH] test-utils: refactor MockTranslationApi default messages handling + tests Signed-off-by: Patrik Oldsberg --- .../TranslationApi/MockTranslationApi.test.ts | 151 ++++++++++++++++++ .../apis/TranslationApi/MockTranslationApi.ts | 18 ++- 2 files changed, 164 insertions(+), 5 deletions(-) create mode 100644 packages/test-utils/src/testUtils/apis/TranslationApi/MockTranslationApi.test.ts diff --git a/packages/test-utils/src/testUtils/apis/TranslationApi/MockTranslationApi.test.ts b/packages/test-utils/src/testUtils/apis/TranslationApi/MockTranslationApi.test.ts new file mode 100644 index 0000000000..1ed06a9138 --- /dev/null +++ b/packages/test-utils/src/testUtils/apis/TranslationApi/MockTranslationApi.test.ts @@ -0,0 +1,151 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { MockTranslationApi } from './MockTranslationApi'; + +describe('MockTranslationApi', () => { + function snapshotWithMessages( + messages: TMessages, + ) { + const translationApi = MockTranslationApi.create(); + const ref = createTranslationRef({ + id: 'test', + messages, + }); + const snapshot = translationApi.getTranslation(ref); + if (!snapshot.ready) { + throw new Error('Translation snapshot is not ready'); + } + return snapshot; + } + + it('should format plain messages', () => { + const snapshot = snapshotWithMessages({ + foo: 'Foo', + bar: 'Bar', + baz: 'Baz', + }); + + expect(snapshot.t('foo')).toBe('Foo'); + expect(snapshot.t('bar')).toBe('Bar'); + expect(snapshot.t('baz')).toBe('Baz'); + }); + + it('should support interpolation', () => { + const snapshot = snapshotWithMessages({ + shallow: 'Foo {{ bar }}', + multiple: 'Foo {{ bar }} {{ baz }}', + deep: 'Foo {{ bar.baz }}', + }); + + expect(snapshot.t('shallow')).toBe('Foo {{ bar }}'); + expect(snapshot.t('shallow', { bar: 'Bar' })).toBe('Foo Bar'); + + expect(snapshot.t('multiple')).toBe('Foo {{ bar }} {{ baz }}'); + expect(snapshot.t('multiple', { bar: 'Bar' })).toBe('Foo Bar {{ baz }}'); + expect(snapshot.t('multiple', { bar: 'Bar', baz: 'Baz' })).toBe( + 'Foo Bar Baz', + ); + + expect(snapshot.t('deep')).toBe('Foo {{ bar.baz }}'); + expect(snapshot.t('deep', { bar: { baz: 'Baz' } })).toBe('Foo Baz'); + }); + + // Escaping isn't as useful in React, since we don't need to escape HTML in strings + it('should not escape by default', () => { + const snapshot = snapshotWithMessages({ + foo: 'Foo {{ foo }}', + }); + + expect(snapshot.t('foo', { foo: '
' })).toBe('Foo
'); + expect( + snapshot.t('foo', { + foo: '
', + interpolation: { escapeValue: true }, + }), + ).toBe('Foo <div>'); + }); + + it('should support nesting', () => { + const snapshot = snapshotWithMessages({ + foo: 'Foo $t(bar) $t(baz)', + bar: 'Nested', + baz: 'Baz {{ qux }}', + }); + + expect(snapshot.t('foo', { qux: 'Deep' })).toBe('Foo Nested Baz Deep'); + }); + + it('should support formatting', () => { + const snapshot = snapshotWithMessages({ + plain: '= {{ x }}', + number: '= {{ x, number }}', + numberFixed: '= {{ x, number(minimumFractionDigits: 2) }}', + relativeTime: '= {{ x, relativeTime }}', + relativeSeconds: '= {{ x, relativeTime(second) }}', + relativeSecondsShort: + '= {{ x, relativeTime(range: second; style: narrow) }}', + list: '= {{ x, list }}', + }); + + expect(snapshot.t('plain', { x: 5 })).toBe('= 5'); + expect(snapshot.t('number', { x: 5 })).toBe('= 5'); + expect(snapshot.t('number', { x: 5, minimumFractionDigits: 1 })).toBe( + '= 5.0', + ); + expect(snapshot.t('numberFixed', { x: 5 })).toBe('= 5.00'); + expect(snapshot.t('numberFixed', { x: 5, minimumFractionDigits: 3 })).toBe( + '= 5.000', + ); + expect(snapshot.t('relativeTime', { x: 3 })).toBe('= in 3 days'); + expect(snapshot.t('relativeTime', { x: -3 })).toBe('= 3 days ago'); + expect(snapshot.t('relativeTime', { x: 15, range: 'weeks' })).toBe( + '= in 15 weeks', + ); + expect( + snapshot.t('relativeTime', { x: 15, range: 'weeks', style: 'short' }), + ).toBe('= in 15 wk.'); + expect(snapshot.t('relativeSeconds', { x: 1 })).toBe('= in 1 second'); + expect(snapshot.t('relativeSeconds', { x: 2 })).toBe('= in 2 seconds'); + expect(snapshot.t('relativeSeconds', { x: -3 })).toBe('= 3 seconds ago'); + expect(snapshot.t('relativeSeconds', { x: 0 })).toBe('= in 0 seconds'); + expect(snapshot.t('relativeSecondsShort', { x: 1 })).toBe('= in 1s'); + expect(snapshot.t('relativeSecondsShort', { x: 2 })).toBe('= in 2s'); + expect(snapshot.t('relativeSecondsShort', { x: -3 })).toBe('= 3s ago'); + expect(snapshot.t('relativeSecondsShort', { x: 0 })).toBe('= in 0s'); + expect(snapshot.t('list', { x: ['a'] })).toBe('= a'); + expect(snapshot.t('list', { x: ['a', 'b'] })).toBe('= a and b'); + expect(snapshot.t('list', { x: ['a', 'b', 'c'] })).toBe('= a, b, and c'); + }); + + it('should support plurals', () => { + const snapshot = snapshotWithMessages({ + derp_one: 'derp', + derp_other: 'derps', + derpWithCount_one: '{{ count }} derp', + derpWithCount_other: '{{ count }} derps', + }); + + // TODO(Rugvip): Support plural keys + expect(snapshot.t('derp' as any, { count: 1 })).toBe('derp'); + expect(snapshot.t('derp' as any, { count: 2 })).toBe('derps'); + expect(snapshot.t('derp' as any, { count: 0 })).toBe('derps'); + expect(snapshot.t('derpWithCount' as any, { count: 1 })).toBe('1 derp'); + expect(snapshot.t('derpWithCount' as any, { count: 2 })).toBe('2 derps'); + expect(snapshot.t('derpWithCount' as any, { count: 0 })).toBe('0 derps'); + }); +}); diff --git a/packages/test-utils/src/testUtils/apis/TranslationApi/MockTranslationApi.ts b/packages/test-utils/src/testUtils/apis/TranslationApi/MockTranslationApi.ts index 19b4f08479..04fc55bcbc 100644 --- a/packages/test-utils/src/testUtils/apis/TranslationApi/MockTranslationApi.ts +++ b/packages/test-utils/src/testUtils/apis/TranslationApi/MockTranslationApi.ts @@ -49,6 +49,7 @@ export class MockTranslationApi implements TranslationApi { } #i18n: I18n; + #registeredRefs = new Set(); private constructor(i18n: I18n) { this.#i18n = i18n; @@ -60,15 +61,22 @@ export class MockTranslationApi implements TranslationApi { const internalRef = toInternalTranslationRef(translationRef); const t = this.#i18n.getFixedT(null, internalRef.id); - const defaultMessages = internalRef.getDefaultMessages() as TMessages; + + if (!this.#registeredRefs.has(internalRef.id)) { + this.#registeredRefs.add(internalRef.id); + this.#i18n.addResourceBundle( + DEFAULT_LANGUAGE, + internalRef.id, + internalRef.getDefaultMessages(), + false, // do not merge + true, // overwrite existing + ); + } return { ready: true, t: (key, options) => { - return t(key as string, { - ...options, - defaultValue: defaultMessages[key], - }); + return t(key as string, { ...options }); }, }; }