Use Intl for names of languages (#11427)
* Use Intl for names of languages * Tweak Intl language style from "American English" -> "US English" * Update tests * Fix tests * Consolidate languageHandler-test files * Improve coverage * Consistent casing for languages in dropdown * Update LanguageDropdown.tsx * Delint & update snapshot * Fix tests * Improve coverage `of` will fallback to the given code with fallback=code (default)
This commit is contained in:
parent
3684c77cfe
commit
4de315fb6c
15 changed files with 304 additions and 193 deletions
|
@ -37,7 +37,7 @@ exports[`<MatrixChat /> with a soft-logged-out session should show the soft-logo
|
|||
Matrix
|
||||
</aside>
|
||||
<div
|
||||
class="mx_Dropdown mx_AuthBody_language"
|
||||
class="mx_Dropdown mx_LanguageDropdown mx_AuthBody_language"
|
||||
>
|
||||
<div
|
||||
aria-describedby="mx_LanguageDropdown_value"
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
Copyright 2023 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 { render, screen, waitForElementToBeRemoved } from "@testing-library/react";
|
||||
|
||||
import SpellCheckLanguagesDropdown from "../../../../src/components/views/elements/SpellCheckLanguagesDropdown";
|
||||
import PlatformPeg from "../../../../src/PlatformPeg";
|
||||
|
||||
describe("<SpellCheckLanguagesDropdown />", () => {
|
||||
it("renders as expected", async () => {
|
||||
const platform: any = { getAvailableSpellCheckLanguages: jest.fn().mockResolvedValue(["en", "de", "qq"]) };
|
||||
PlatformPeg.set(platform);
|
||||
|
||||
const { asFragment } = render(
|
||||
<SpellCheckLanguagesDropdown
|
||||
className="mx_GeneralUserSettingsTab_spellCheckLanguageInput"
|
||||
value="en"
|
||||
onOptionChange={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,32 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<SpellCheckLanguagesDropdown /> renders as expected 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_Dropdown mx_GeneralUserSettingsTab_spellCheckLanguageInput"
|
||||
>
|
||||
<div
|
||||
aria-describedby="mx_LanguageDropdown_value"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="listbox"
|
||||
aria-label="Language Dropdown"
|
||||
aria-owns="mx_LanguageDropdown_input"
|
||||
class="mx_AccessibleButton mx_Dropdown_input mx_no_textinput"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_Dropdown_option"
|
||||
id="mx_LanguageDropdown_value"
|
||||
>
|
||||
<div>
|
||||
English
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
class="mx_Dropdown_arrow"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -1,6 +1,3 @@
|
|||
{
|
||||
"en": {
|
||||
"fileName": "en_EN.json",
|
||||
"label": "English"
|
||||
}
|
||||
"en": "en_EN.json"
|
||||
}
|
||||
|
|
|
@ -1,121 +0,0 @@
|
|||
/*
|
||||
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 SdkConfig from "../src/SdkConfig";
|
||||
import {
|
||||
_t,
|
||||
CustomTranslationOptions,
|
||||
ICustomTranslations,
|
||||
registerCustomTranslations,
|
||||
setLanguage,
|
||||
UserFriendlyError,
|
||||
} from "../src/languageHandler";
|
||||
|
||||
async function setupTranslationOverridesForTests(overrides: ICustomTranslations) {
|
||||
const lookupUrl = "/translations.json";
|
||||
const fn = (url: string): ICustomTranslations => {
|
||||
expect(url).toEqual(lookupUrl);
|
||||
return overrides;
|
||||
};
|
||||
|
||||
SdkConfig.add({
|
||||
custom_translations_url: lookupUrl,
|
||||
});
|
||||
CustomTranslationOptions.lookupFn = fn;
|
||||
await registerCustomTranslations({
|
||||
testOnlyIgnoreCustomTranslationsCache: true,
|
||||
});
|
||||
}
|
||||
|
||||
describe("languageHandler", () => {
|
||||
afterEach(() => {
|
||||
SdkConfig.reset();
|
||||
CustomTranslationOptions.lookupFn = undefined;
|
||||
});
|
||||
|
||||
it("should support overriding translations", async () => {
|
||||
const str = "This is a test string that does not exist in the app.";
|
||||
const enOverride = "This is the English version of a custom string.";
|
||||
const deOverride = "This is the German version of a custom string.";
|
||||
|
||||
// First test that overrides aren't being used
|
||||
await setLanguage("en");
|
||||
expect(_t(str)).toEqual(str);
|
||||
await setLanguage("de");
|
||||
expect(_t(str)).toEqual(str);
|
||||
|
||||
await setupTranslationOverridesForTests({
|
||||
[str]: {
|
||||
en: enOverride,
|
||||
de: deOverride,
|
||||
},
|
||||
});
|
||||
|
||||
// Now test that they *are* being used
|
||||
await setLanguage("en");
|
||||
expect(_t(str)).toEqual(enOverride);
|
||||
|
||||
await setLanguage("de");
|
||||
expect(_t(str)).toEqual(deOverride);
|
||||
});
|
||||
|
||||
describe("UserFriendlyError", () => {
|
||||
const testErrorMessage = "This email address is already in use (%(email)s)";
|
||||
beforeEach(async () => {
|
||||
// Setup some strings with variable substituations that we can use in the tests.
|
||||
const deOverride = "Diese E-Mail-Adresse wird bereits verwendet (%(email)s)";
|
||||
await setupTranslationOverridesForTests({
|
||||
[testErrorMessage]: {
|
||||
en: testErrorMessage,
|
||||
de: deOverride,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("includes English message and localized translated message", async () => {
|
||||
await setLanguage("de");
|
||||
|
||||
const friendlyError = new UserFriendlyError(testErrorMessage, {
|
||||
email: "test@example.com",
|
||||
cause: undefined,
|
||||
});
|
||||
|
||||
// Ensure message is in English so it's readable in the logs
|
||||
expect(friendlyError.message).toStrictEqual("This email address is already in use (test@example.com)");
|
||||
// Ensure the translated message is localized appropriately
|
||||
expect(friendlyError.translatedMessage).toStrictEqual(
|
||||
"Diese E-Mail-Adresse wird bereits verwendet (test@example.com)",
|
||||
);
|
||||
});
|
||||
|
||||
it("includes underlying cause error", async () => {
|
||||
await setLanguage("de");
|
||||
|
||||
const underlyingError = new Error("Fake underlying error");
|
||||
const friendlyError = new UserFriendlyError(testErrorMessage, {
|
||||
email: "test@example.com",
|
||||
cause: underlyingError,
|
||||
});
|
||||
|
||||
expect(friendlyError.cause).toStrictEqual(underlyingError);
|
||||
});
|
||||
|
||||
it("ok to omit the substitution variables and cause object, there just won't be any cause", async () => {
|
||||
const friendlyError = new UserFriendlyError("foo error");
|
||||
expect(friendlyError.cause).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -15,18 +15,160 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
|
||||
import SdkConfig from "../src/SdkConfig";
|
||||
import {
|
||||
_t,
|
||||
_tDom,
|
||||
TranslatedString,
|
||||
CustomTranslationOptions,
|
||||
getAllLanguagesWithLabels,
|
||||
ICustomTranslations,
|
||||
registerCustomTranslations,
|
||||
setLanguage,
|
||||
setMissingEntryGenerator,
|
||||
substitute,
|
||||
} from "../../src/languageHandler";
|
||||
import { stubClient } from "../test-utils";
|
||||
TranslatedString,
|
||||
UserFriendlyError,
|
||||
} from "../src/languageHandler";
|
||||
import { stubClient } from "./test-utils";
|
||||
import { setupLanguageMock } from "./setup/setupLanguage";
|
||||
|
||||
describe("languageHandler", function () {
|
||||
async function setupTranslationOverridesForTests(overrides: ICustomTranslations) {
|
||||
const lookupUrl = "/translations.json";
|
||||
const fn = (url: string): ICustomTranslations => {
|
||||
expect(url).toEqual(lookupUrl);
|
||||
return overrides;
|
||||
};
|
||||
|
||||
SdkConfig.add({
|
||||
custom_translations_url: lookupUrl,
|
||||
});
|
||||
CustomTranslationOptions.lookupFn = fn;
|
||||
await registerCustomTranslations({
|
||||
testOnlyIgnoreCustomTranslationsCache: true,
|
||||
});
|
||||
}
|
||||
|
||||
describe("languageHandler", () => {
|
||||
beforeEach(async () => {
|
||||
await setLanguage("en");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
SdkConfig.reset();
|
||||
CustomTranslationOptions.lookupFn = undefined;
|
||||
});
|
||||
|
||||
it("should support overriding translations", async () => {
|
||||
const str = "This is a test string that does not exist in the app.";
|
||||
const enOverride = "This is the English version of a custom string.";
|
||||
const deOverride = "This is the German version of a custom string.";
|
||||
|
||||
// First test that overrides aren't being used
|
||||
await setLanguage("en");
|
||||
expect(_t(str)).toEqual(str);
|
||||
await setLanguage("de");
|
||||
expect(_t(str)).toEqual(str);
|
||||
|
||||
await setupTranslationOverridesForTests({
|
||||
[str]: {
|
||||
en: enOverride,
|
||||
de: deOverride,
|
||||
},
|
||||
});
|
||||
|
||||
// Now test that they *are* being used
|
||||
await setLanguage("en");
|
||||
expect(_t(str)).toEqual(enOverride);
|
||||
|
||||
await setLanguage("de");
|
||||
expect(_t(str)).toEqual(deOverride);
|
||||
});
|
||||
|
||||
describe("UserFriendlyError", () => {
|
||||
const testErrorMessage = "This email address is already in use (%(email)s)";
|
||||
beforeEach(async () => {
|
||||
// Setup some strings with variable substituations that we can use in the tests.
|
||||
const deOverride = "Diese E-Mail-Adresse wird bereits verwendet (%(email)s)";
|
||||
await setupTranslationOverridesForTests({
|
||||
[testErrorMessage]: {
|
||||
en: testErrorMessage,
|
||||
de: deOverride,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("includes English message and localized translated message", async () => {
|
||||
await setLanguage("de");
|
||||
|
||||
const friendlyError = new UserFriendlyError(testErrorMessage, {
|
||||
email: "test@example.com",
|
||||
cause: undefined,
|
||||
});
|
||||
|
||||
// Ensure message is in English so it's readable in the logs
|
||||
expect(friendlyError.message).toStrictEqual("This email address is already in use (test@example.com)");
|
||||
// Ensure the translated message is localized appropriately
|
||||
expect(friendlyError.translatedMessage).toStrictEqual(
|
||||
"Diese E-Mail-Adresse wird bereits verwendet (test@example.com)",
|
||||
);
|
||||
});
|
||||
|
||||
it("includes underlying cause error", async () => {
|
||||
await setLanguage("de");
|
||||
|
||||
const underlyingError = new Error("Fake underlying error");
|
||||
const friendlyError = new UserFriendlyError(testErrorMessage, {
|
||||
email: "test@example.com",
|
||||
cause: underlyingError,
|
||||
});
|
||||
|
||||
expect(friendlyError.cause).toStrictEqual(underlyingError);
|
||||
});
|
||||
|
||||
it("ok to omit the substitution variables and cause object, there just won't be any cause", async () => {
|
||||
const friendlyError = new UserFriendlyError("foo error");
|
||||
expect(friendlyError.cause).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAllLanguagesWithLabels", () => {
|
||||
it("should handle unknown language sanely", async () => {
|
||||
fetchMock.getOnce(
|
||||
"/i18n/languages.json",
|
||||
{
|
||||
en: "en_EN.json",
|
||||
de: "de_DE.json",
|
||||
qq: "qq.json",
|
||||
},
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
await expect(getAllLanguagesWithLabels()).resolves.toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"label": "English",
|
||||
"labelInTargetLanguage": "English",
|
||||
"value": "en",
|
||||
},
|
||||
{
|
||||
"label": "German",
|
||||
"labelInTargetLanguage": "Deutsch",
|
||||
"value": "de",
|
||||
},
|
||||
{
|
||||
"label": "qq",
|
||||
"labelInTargetLanguage": "qq",
|
||||
"value": "qq",
|
||||
},
|
||||
]
|
||||
`);
|
||||
setupLanguageMock(); // restore language mock
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("languageHandler JSX", function () {
|
||||
// See setupLanguage.ts for how we are stubbing out translations to provide fixture data for these tests
|
||||
const basicString = "Rooms";
|
||||
const selfClosingTagSub = "Accept <policyLink /> to continue:";
|
|
@ -32,24 +32,18 @@ const lv = {
|
|||
// de_DE.json
|
||||
// lv.json - mock version with few translations, used to test fallback translation
|
||||
|
||||
fetchMock
|
||||
.get("/i18n/languages.json", {
|
||||
en: {
|
||||
fileName: "en_EN.json",
|
||||
label: "English",
|
||||
},
|
||||
de: {
|
||||
fileName: "de_DE.json",
|
||||
label: "German",
|
||||
},
|
||||
lv: {
|
||||
fileName: "lv.json",
|
||||
label: "Latvian",
|
||||
},
|
||||
})
|
||||
.get("end:en_EN.json", en)
|
||||
.get("end:de_DE.json", de)
|
||||
.get("end:lv.json", lv);
|
||||
export function setupLanguageMock() {
|
||||
fetchMock
|
||||
.get("/i18n/languages.json", {
|
||||
en: "en_EN.json",
|
||||
de: "de_DE.json",
|
||||
lv: "lv.json",
|
||||
})
|
||||
.get("end:en_EN.json", en)
|
||||
.get("end:de_DE.json", de)
|
||||
.get("end:lv.json", lv);
|
||||
}
|
||||
setupLanguageMock();
|
||||
|
||||
languageHandler.setLanguage("en");
|
||||
languageHandler.setMissingEntryGenerator((key) => key.split("|", 2)[1]);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue