Transition languageHandler to Typescript

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski 2020-07-02 23:15:08 +01:00
parent 1ab0a1a1de
commit 48ce294a49
5 changed files with 57 additions and 29 deletions

View file

@ -120,6 +120,7 @@
"@babel/register": "^7.7.4",
"@peculiar/webcrypto": "^1.0.22",
"@types/classnames": "^2.2.10",
"@types/counterpart": "^0.18.1",
"@types/flux": "^3.1.9",
"@types/lodash": "^4.14.152",
"@types/modernizr": "^3.5.3",

View file

@ -47,6 +47,10 @@ declare global {
hasStorageAccess?: () => Promise<boolean>;
}
interface Navigator {
userLanguage?: string;
}
interface StorageEstimate {
usageDetails?: {[key: string]: number};
}

View file

@ -14,13 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {ReactChild} from "react";
import React, {ReactNode} from "react";
import FormButton from "../elements/FormButton";
import {XOR} from "../../../@types/common";
export interface IProps {
description: ReactChild;
description: ReactNode;
acceptLabel: string;
onAccept();

View file

@ -1,7 +1,7 @@
/*
Copyright 2017 MTRNord and Cooperative EITA
Copyright 2017 Vector Creations Ltd.
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
@ -20,10 +20,11 @@ limitations under the License.
import request from 'browser-request';
import counterpart from 'counterpart';
import React from 'react';
import SettingsStore, {SettingLevel} from "./settings/SettingsStore";
import PlatformPeg from "./PlatformPeg";
// $webapp is a webpack resolve alias pointing to the output directory, see webpack config
// @ts-ignore - $webapp is a webpack resolve alias pointing to the output directory, see webpack config
import webpackLangJsonUrl from "$webapp/i18n/languages.json";
const i18nFolder = 'i18n/';
@ -37,27 +38,31 @@ counterpart.setSeparator('|');
// Fall back to English
counterpart.setFallbackLocale('en');
interface ITranslatableError extends Error {
translatedMessage: string;
}
/**
* Helper function to create an error which has an English message
* with a translatedMessage property for use by the consumer.
* @param {string} message Message to translate.
* @returns {Error} The constructed error.
*/
export function newTranslatableError(message) {
const error = new Error(message);
export function newTranslatableError(message: string) {
const error = new Error(message) as ITranslatableError;
error.translatedMessage = _t(message);
return error;
}
// Function which only purpose is to mark that a string is translatable
// Does not actually do anything. It's helpful for automatic extraction of translatable strings
export function _td(s) {
export function _td(s: string): string {
return s;
}
// Wrapper for counterpart's translation function so that it handles nulls and undefineds properly
// Takes the same arguments as counterpart.translate()
function safeCounterpartTranslate(text, options) {
function safeCounterpartTranslate(text: string, options?: object) {
// Horrible hack to avoid https://github.com/vector-im/riot-web/issues/4191
// The interpolation library that counterpart uses does not support undefined/null
// values and instead will throw an error. This is a problem since everywhere else
@ -89,6 +94,13 @@ function safeCounterpartTranslate(text, options) {
return translated;
}
interface IVariables {
count?: number;
[key: string]: number | string;
}
type Tags = Record<string, (sub: string) => React.ReactNode>;
/*
* Translates text and optionally also replaces XML-ish elements in the text with e.g. React components
* @param {string} text The untranslated text, e.g "click <a>here</a> now to %(foo)s".
@ -105,7 +117,9 @@ function safeCounterpartTranslate(text, options) {
*
* @return a React <span> component if any non-strings were used in substitutions, otherwise a string
*/
export function _t(text, variables, tags) {
export function _t(text: string, variables?: IVariables): string;
export function _t(text: string, variables: IVariables, tags: Tags): React.ReactNode;
export function _t(text: string, variables?: IVariables, tags?: Tags): string | React.ReactNode {
// Don't do substitutions in counterpart. We handle it ourselves so we can replace with React components
// However, still pass the variables to counterpart so that it can choose the correct plural if count is given
// It is enough to pass the count variable, but in the future counterpart might make use of other information too
@ -141,23 +155,25 @@ export function _t(text, variables, tags) {
*
* @return a React <span> component if any non-strings were used in substitutions, otherwise a string
*/
export function substitute(text, variables, tags) {
let result = text;
export function substitute(text: string, variables?: IVariables): string;
export function substitute(text: string, variables: IVariables, tags: Tags): string;
export function substitute(text: string, variables?: IVariables, tags?: Tags): string | React.ReactNode {
let result: React.ReactNode | string = text;
if (variables !== undefined) {
const regexpMapping = {};
const regexpMapping: IVariables = {};
for (const variable in variables) {
regexpMapping[`%\\(${variable}\\)s`] = variables[variable];
}
result = replaceByRegexes(result, regexpMapping);
result = replaceByRegexes(result as string, regexpMapping);
}
if (tags !== undefined) {
const regexpMapping = {};
const regexpMapping: Tags = {};
for (const tag in tags) {
regexpMapping[`(<${tag}>(.*?)<\\/${tag}>|<${tag}>|<${tag}\\s*\\/>)`] = tags[tag];
}
result = replaceByRegexes(result, regexpMapping);
result = replaceByRegexes(result as string, regexpMapping);
}
return result;
@ -172,7 +188,9 @@ export function substitute(text, variables, tags) {
*
* @return a React <span> component if any non-strings were used in substitutions, otherwise a string
*/
export function replaceByRegexes(text, mapping) {
export function replaceByRegexes(text: string, mapping: IVariables): string;
export function replaceByRegexes(text: string, mapping: Tags): React.ReactNode;
export function replaceByRegexes(text: string, mapping: IVariables | Tags): string | React.ReactNode {
// We initially store our output as an array of strings and objects (e.g. React components).
// This will then be converted to a string or a <span> at the end
const output = [text];
@ -189,7 +207,7 @@ export function replaceByRegexes(text, mapping) {
// and everything after the match. Insert all three into the output. We need to do this because we can insert objects.
// Otherwise there would be no need for the splitting and we could do simple replacement.
let matchFoundSomewhere = false; // If we don't find a match anywhere we want to log it
for (const outputIndex in output) {
for (let outputIndex = 0; outputIndex < output.length; outputIndex++) {
const inputText = output[outputIndex];
if (typeof inputText !== 'string') { // We might have inserted objects earlier, don't try to replace them
continue;
@ -216,7 +234,7 @@ export function replaceByRegexes(text, mapping) {
let replaced;
// If substitution is a function, call it
if (mapping[regexpString] instanceof Function) {
replaced = mapping[regexpString].apply(null, capturedGroups);
replaced = (mapping as Tags)[regexpString].apply(null, capturedGroups);
} else {
replaced = mapping[regexpString];
}
@ -277,11 +295,11 @@ export function replaceByRegexes(text, mapping) {
// Allow overriding the text displayed when no translation exists
// Currently only used in unit tests to avoid having to load
// the translations in riot-web
export function setMissingEntryGenerator(f) {
export function setMissingEntryGenerator(f: (value: string) => void) {
counterpart.setMissingEntryGenerator(f);
}
export function setLanguage(preferredLangs) {
export function setLanguage(preferredLangs: string | string[]) {
if (!Array.isArray(preferredLangs)) {
preferredLangs = [preferredLangs];
}
@ -358,8 +376,8 @@ export function getLanguageFromBrowser() {
* @param {string} language The input language string
* @return {string[]} List of normalised languages
*/
export function getNormalizedLanguageKeys(language) {
const languageKeys = [];
export function getNormalizedLanguageKeys(language: string) {
const languageKeys: string[] = [];
const normalizedLanguage = normalizeLanguageKey(language);
const languageParts = normalizedLanguage.split('-');
if (languageParts.length === 2 && languageParts[0] === languageParts[1]) {
@ -380,7 +398,7 @@ export function getNormalizedLanguageKeys(language) {
* @param {string} language The language string to be normalized
* @returns {string} The normalized language string
*/
export function normalizeLanguageKey(language) {
export function normalizeLanguageKey(language: string) {
return language.toLowerCase().replace("_", "-");
}
@ -396,7 +414,7 @@ export function getCurrentLanguage() {
* @param {string[]} langs List of language codes to pick from
* @returns {string} The most appropriate language code from langs
*/
export function pickBestLanguage(langs) {
export function pickBestLanguage(langs: string[]): string {
const currentLang = getCurrentLanguage();
const normalisedLangs = langs.map(normalizeLanguageKey);
@ -408,13 +426,13 @@ export function pickBestLanguage(langs) {
{
// Failing that, a different dialect of the same language
const closeLangIndex = normalisedLangs.find((l) => l.substr(0, 2) === currentLang.substr(0, 2));
const closeLangIndex = normalisedLangs.findIndex((l) => l.substr(0, 2) === currentLang.substr(0, 2));
if (closeLangIndex > -1) return langs[closeLangIndex];
}
{
// Neither of those? Try an english variant.
const enIndex = normalisedLangs.find((l) => l.startsWith('en'));
const enIndex = normalisedLangs.findIndex((l) => l.startsWith('en'));
if (enIndex > -1) return langs[enIndex];
}
@ -422,7 +440,7 @@ export function pickBestLanguage(langs) {
return langs[0];
}
function getLangsJson() {
function getLangsJson(): Promise<object> {
return new Promise(async (resolve, reject) => {
let url;
if (typeof(webpackLangJsonUrl) === 'string') { // in Jest this 'url' isn't a URL, so just fall through
@ -443,7 +461,7 @@ function getLangsJson() {
});
}
function weblateToCounterpart(inTrs) {
function weblateToCounterpart(inTrs: object): object {
const outTrs = {};
for (const key of Object.keys(inTrs)) {
@ -463,7 +481,7 @@ function weblateToCounterpart(inTrs) {
return outTrs;
}
function getLanguage(langPath) {
function getLanguage(langPath: string): object {
return new Promise((resolve, reject) => {
request(
{ method: "GET", url: langPath },

View file

@ -1257,6 +1257,11 @@
resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.10.tgz#cc658ca319b6355399efc1f5b9e818f1a24bf999"
integrity sha512-1UzDldn9GfYYEsWWnn/P4wkTlkZDH7lDb0wBMGbtIQc9zXEQq7FlKBdZUn6OBqD8sKZZ2RQO2mAjGpXiDGoRmQ==
"@types/counterpart@^0.18.1":
version "0.18.1"
resolved "https://registry.yarnpkg.com/@types/counterpart/-/counterpart-0.18.1.tgz#b1b784d9e54d9879f0a8cb12f2caedab65430fe8"
integrity sha512-PRuFlBBkvdDOtxlIASzTmkEFar+S66Ek48NVVTWMUjtJAdn5vyMSN8y6IZIoIymGpR36q2nZbIYazBWyFxL+IQ==
"@types/fbemitter@*":
version "2.0.32"
resolved "https://registry.yarnpkg.com/@types/fbemitter/-/fbemitter-2.0.32.tgz#8ed204da0f54e9c8eaec31b1eec91e25132d082c"