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:
Michael Telatynski 2023-08-22 15:07:16 +01:00 committed by GitHub
parent 3684c77cfe
commit 4de315fb6c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 304 additions and 193 deletions

View file

@ -70,7 +70,7 @@ export default class CountryDropdown extends React.Component<IProps, IState> {
const locale = new Intl.Locale(navigator.language ?? navigator.languages[0]);
const code = locale.region ?? locale.language ?? locale.baseName;
const displayNames = new Intl.DisplayNames(["en"], { type: "region" });
const displayName = displayNames.of(code)?.toUpperCase();
const displayName = displayNames.of(code)!.toUpperCase();
defaultCountry = COUNTRIES.find(
(c) => c.iso2 === code.toUpperCase() || c.name.toUpperCase() === displayName,
);

View file

@ -16,6 +16,7 @@ limitations under the License.
*/
import React, { ReactElement } from "react";
import classNames from "classnames";
import * as languageHandler from "../../../languageHandler";
import SettingsStore from "../../../settings/SettingsStore";
@ -24,9 +25,10 @@ import Spinner from "./Spinner";
import Dropdown from "./Dropdown";
import { NonEmptyArray } from "../../../@types/common";
type Languages = Awaited<ReturnType<typeof languageHandler.getAllLanguagesFromJson>>;
type Languages = Awaited<ReturnType<typeof languageHandler.getAllLanguagesWithLabels>>;
function languageMatchesSearchQuery(query: string, language: Languages[0]): boolean {
if (language.labelInTargetLanguage.toUpperCase().includes(query.toUpperCase())) return true;
if (language.label.toUpperCase().includes(query.toUpperCase())) return true;
if (language.value.toUpperCase() === query.toUpperCase()) return true;
return false;
@ -56,23 +58,30 @@ export default class LanguageDropdown extends React.Component<IProps, IState> {
public componentDidMount(): void {
languageHandler
.getAllLanguagesFromJson()
.getAllLanguagesWithLabels()
.then((langs) => {
langs.sort(function (a, b) {
if (a.label < b.label) return -1;
if (a.label > b.label) return 1;
if (a.labelInTargetLanguage < b.labelInTargetLanguage) return -1;
if (a.labelInTargetLanguage > b.labelInTargetLanguage) return 1;
return 0;
});
this.setState({ langs });
})
.catch(() => {
this.setState({ langs: [{ value: "en", label: "English" }] });
this.setState({
langs: [
{
value: "en",
label: "English",
labelInTargetLanguage: "English",
},
],
});
});
if (!this.props.value) {
// If no value is given, we start with the first
// country selected, but our parent component
// doesn't know this, therefore we do this.
// If no value is given, we start with the first country selected,
// but our parent component doesn't know this, therefore we do this.
const language = languageHandler.getUserLanguage();
this.props.onOptionChange(language);
}
@ -89,7 +98,7 @@ export default class LanguageDropdown extends React.Component<IProps, IState> {
return <Spinner />;
}
let displayedLanguages: Awaited<ReturnType<typeof languageHandler.getAllLanguagesFromJson>>;
let displayedLanguages: Awaited<ReturnType<typeof languageHandler.getAllLanguagesWithLabels>>;
if (this.state.searchQuery) {
displayedLanguages = this.state.langs.filter((lang) => {
return languageMatchesSearchQuery(this.state.searchQuery, lang);
@ -99,7 +108,7 @@ export default class LanguageDropdown extends React.Component<IProps, IState> {
}
const options = displayedLanguages.map((language) => {
return <div key={language.value}>{language.label}</div>;
return <div key={language.value}>{language.labelInTargetLanguage}</div>;
}) as NonEmptyArray<ReactElement & { key: string }>;
// default value here too, otherwise we need to handle null / undefined
@ -116,7 +125,7 @@ export default class LanguageDropdown extends React.Component<IProps, IState> {
return (
<Dropdown
id="mx_LanguageDropdown"
className={this.props.className}
className={classNames("mx_LanguageDropdown", this.props.className)}
onOptionChange={this.props.onOptionChange}
onSearchChange={this.onSearchChange}
searchEnabled={true}

View file

@ -19,12 +19,14 @@ import React, { ReactElement } from "react";
import Dropdown from "../../views/elements/Dropdown";
import PlatformPeg from "../../../PlatformPeg";
import SettingsStore from "../../../settings/SettingsStore";
import { _t } from "../../../languageHandler";
import { _t, getUserLanguage } from "../../../languageHandler";
import Spinner from "./Spinner";
import * as languageHandler from "../../../languageHandler";
import { NonEmptyArray } from "../../../@types/common";
type Languages = Awaited<ReturnType<typeof languageHandler.getAllLanguagesFromJson>>;
type Languages = {
value: string;
label: string; // translated
}[];
function languageMatchesSearchQuery(query: string, language: Languages[0]): boolean {
if (language.label.toUpperCase().includes(query.toUpperCase())) return true;
if (language.value.toUpperCase() === query.toUpperCase()) return true;
@ -58,6 +60,7 @@ export default class SpellCheckLanguagesDropdown extends React.Component<
public componentDidMount(): void {
const plaf = PlatformPeg.get();
if (plaf) {
const languageNames = new Intl.DisplayNames([getUserLanguage()], { type: "language", style: "short" });
plaf.getAvailableSpellCheckLanguages()
?.then((languages) => {
languages.sort(function (a, b) {
@ -68,7 +71,7 @@ export default class SpellCheckLanguagesDropdown extends React.Component<
const langs: Languages = [];
languages.forEach((language) => {
langs.push({
label: language,
label: languageNames.of(language)!,
value: language,
});
});
@ -79,7 +82,7 @@ export default class SpellCheckLanguagesDropdown extends React.Component<
languages: [
{
value: "en",
label: "English",
label: languageNames.of("en")!,
},
],
});

View file

@ -433,10 +433,7 @@ export function setMissingEntryGenerator(f: (value: string) => void): void {
}
type Languages = {
[lang: string]: {
fileName: string;
label: string;
};
[lang: string]: string;
};
export function setLanguage(preferredLangs: string | string[]): Promise<void> {
@ -467,7 +464,7 @@ export function setLanguage(preferredLangs: string | string[]): Promise<void> {
logger.error("Unable to find an appropriate language");
}
return getLanguageRetry(i18nFolder + availLangs[langToUse].fileName);
return getLanguageRetry(i18nFolder + availLangs[langToUse]);
})
.then(async (langData): Promise<ICounterpartTranslation | undefined> => {
counterpart.registerTranslations(langToUse, langData);
@ -481,7 +478,7 @@ export function setLanguage(preferredLangs: string | string[]): Promise<void> {
// Set 'en' as fallback language:
if (langToUse !== "en") {
return getLanguageRetry(i18nFolder + availLangs["en"].fileName);
return getLanguageRetry(i18nFolder + availLangs["en"]);
}
})
.then(async (langData): Promise<void> => {
@ -492,21 +489,23 @@ export function setLanguage(preferredLangs: string | string[]): Promise<void> {
type Language = {
value: string;
label: string;
label: string; // translated
labelInTargetLanguage: string; // translated
};
export function getAllLanguagesFromJson(): Promise<Language[]> {
return getLangsJson().then((langsObject) => {
const langs: Language[] = [];
for (const langKey in langsObject) {
if (langsObject.hasOwnProperty(langKey)) {
langs.push({
value: langKey,
label: langsObject[langKey].label,
});
}
}
return langs;
export async function getAllLanguagesFromJson(): Promise<string[]> {
return Object.keys(await getLangsJson());
}
export async function getAllLanguagesWithLabels(): Promise<Language[]> {
const languageNames = new Intl.DisplayNames([getUserLanguage()], { type: "language", style: "short" });
const languages = await getAllLanguagesFromJson();
return languages.map<Language>((langKey) => {
return {
value: langKey,
label: languageNames.of(langKey)!,
labelInTargetLanguage: new Intl.DisplayNames([langKey], { type: "language", style: "short" }).of(langKey)!,
};
});
}