/* Copyright 2022 The Matrix.org Foundation C.I.C. 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 React from 'react'; import { _t, _tDom, TranslatedString, setLanguage, setMissingEntryGenerator, substitute, } from '../../src/languageHandler'; import { stubClient } from '../test-utils'; describe('languageHandler', function() { // See setupLanguage.ts for how we are stubbing out translations to provide fixture data for these tests const basicString = 'Rooms'; const selfClosingTagSub = 'Accept to continue:'; const textInTagSub = 'Upgrade to your own domain'; const plurals = 'and %(count)s others...'; const variableSub = 'You are now ignoring %(userId)s'; type TestCase = [string, string, Record, Record, TranslatedString]; const testCasesEn: TestCase[] = [ // description of the test case, translationString, variables, tags, expected result ['translates a basic string', basicString, {}, undefined, 'Rooms'], [ 'handles plurals when count is 0', plurals, { count: 0 }, undefined, 'and 0 others...', ], [ 'handles plurals when count is 1', plurals, { count: 1 }, undefined, 'and one other...', ], [ 'handles plurals when count is not 1', plurals, { count: 2 }, undefined, 'and 2 others...', ], [ 'handles simple variable substitution', variableSub, { userId: 'foo' }, undefined, 'You are now ignoring foo', ], [ 'handles simple tag substitution', selfClosingTagSub, {}, { 'policyLink': () => 'foo' }, 'Accept foo to continue:', ], ['handles text in tags', textInTagSub, {}, { 'a': (sub) => `x${sub}x` }, 'xUpgradex to your own domain'], [ 'handles variable substitution with React function component', variableSub, { userId: () => foo }, undefined, // eslint-disable-next-line react/jsx-key You are now ignoring foo, ], [ 'handles variable substitution with react node', variableSub, { userId: foo }, undefined, // eslint-disable-next-line react/jsx-key You are now ignoring foo, ], [ 'handles tag substitution with React function component', selfClosingTagSub, {}, { 'policyLink': () => foo }, // eslint-disable-next-line react/jsx-key Accept foo to continue:, ], ]; describe('when translations exist in language', () => { beforeEach(function(done) { stubClient(); setLanguage('en').then(done); setMissingEntryGenerator(key => key.split("|", 2)[1]); }); it('translates a string to german', function(done) { setLanguage('de').then(function() { const translated = _t(basicString); expect(translated).toBe('Räume'); }).then(done); }); it.each(testCasesEn)("%s", async (_d, translationString, variables, tags, result) => { expect(_t(translationString, variables, tags)).toEqual(result); }); it('replacements in the wrong order', function() { const text = '%(var1)s %(var2)s'; expect(_t(text, { var2: 'val2', var1: 'val1' })).toBe('val1 val2'); }); it('multiple replacements of the same variable', function() { const text = '%(var1)s %(var1)s'; expect(substitute(text, { var1: 'val1' })).toBe('val1 val1'); }); it('multiple replacements of the same tag', function() { const text = 'Click here to join the discussion! or here'; expect(substitute(text, {}, { 'a': (sub) => `x${sub}x` })) .toBe('xClick herex to join the discussion! xor herex'); }); }); describe('for a non-en language', () => { beforeEach(async () => { stubClient(); await setLanguage('lv'); // counterpart doesnt expose any way to restore default config // missingEntryGenerator is mocked in the root setup file // reset to default here const counterpartDefaultMissingEntryGen = function(key) { return 'missing translation: ' + key; }; setMissingEntryGenerator(counterpartDefaultMissingEntryGen); }); // mocked lv has only `"Uploading %(filename)s and %(count)s others|one"` const lvExistingPlural = 'Uploading %(filename)s and %(count)s others'; const lvNonExistingPlural = '%(spaceName)s and %(count)s others'; describe('pluralization', () => { const pluralCases = [ [ 'falls back when plural string exists but not for for count', lvExistingPlural, { count: 2, filename: 'test.txt' }, undefined, 'Uploading test.txt and 2 others', ], [ 'falls back when plural string does not exists at all', lvNonExistingPlural, { count: 2, spaceName: 'test' }, undefined, 'test and 2 others', ], ] as TestCase[]; describe('_t', () => { it('translated correctly when plural string exists for count', () => { expect(_t( lvExistingPlural, { count: 1, filename: 'test.txt' }, undefined)).toEqual('Качване на test.txt и 1 друг'); }); it.each(pluralCases)( "%s", async (_d, translationString, variables, tags, result) => { expect(_t(translationString, variables, tags)).toEqual(result); }, ); }); describe('_tDom()', () => { it('translated correctly when plural string exists for count', () => { expect(_tDom( lvExistingPlural, { count: 1, filename: 'test.txt' }, undefined)).toEqual('Качване на test.txt и 1 друг'); }); it.each(pluralCases)( "%s and translates with fallback locale, attributes fallback locale", async (_d, translationString, variables, tags, result) => { expect(_tDom(translationString, variables, tags)).toEqual({ result }); }, ); }); }); describe('when a translation string does not exist in active language', () => { describe('_t', () => { it.each(testCasesEn)( "%s and translates with fallback locale", async (_d, translationString, variables, tags, result) => { expect(_t(translationString, variables, tags)).toEqual(result); }, ); }); describe('_tDom()', () => { it.each(testCasesEn)( "%s and translates with fallback locale, attributes fallback locale", async (_d, translationString, variables, tags, result) => { expect(_tDom(translationString, variables, tags)).toEqual({ result }); }, ); }); }); }); describe('when languages dont load', () => { it('_t', async () => { const STRING_NOT_IN_THE_DICTIONARY = "a string that isn't in the translations dictionary"; expect(_t(STRING_NOT_IN_THE_DICTIONARY, {}, undefined)).toEqual(STRING_NOT_IN_THE_DICTIONARY); }); it('_tDom', async () => { const STRING_NOT_IN_THE_DICTIONARY = "a string that isn't in the translations dictionary"; expect(_tDom(STRING_NOT_IN_THE_DICTIONARY, {}, undefined)).toEqual( { STRING_NOT_IN_THE_DICTIONARY }); }); }); });