feat: window i18n types and docs with safe setup and fallbacks

pull/3206/head
Ryan Miller 8 months ago
parent ce282cdb98
commit f9fb345599

@ -4,14 +4,17 @@
const { ipcRenderer } = require('electron'); const { ipcRenderer } = require('electron');
const url = require('url'); const url = require('url');
const os = require('os'); const os = require('os');
const i18n = require('./ts/util/i18n'); const { setupI18n } = require('./ts/util/i18n');
const config = url.parse(window.location.toString(), true).query; const config = url.parse(window.location.toString(), true).query;
const { locale } = config; const { locale } = config;
const localeMessages = ipcRenderer.sendSync('locale-data'); const localeMessages = ipcRenderer.sendSync('locale-data');
window.theme = config.theme; window.theme = config.theme;
window.i18n = i18n.setupi18n(locale, localeMessages); window.i18n = setupI18n({
initialLocale: locale,
initialDictionary: localeMessages,
});
window.getOSRelease = () => window.getOSRelease = () =>
`${os.type()} ${os.release()}, Node.js ${config.node_version} ${os.platform()} ${os.arch()}`; `${os.type()} ${os.release()}, Node.js ${config.node_version} ${os.platform()} ${os.arch()}`;

@ -6,7 +6,7 @@ const url = require('url');
const os = require('os'); const os = require('os');
const i18n = require('./ts/util/i18n'); const { setupI18n } = require('./ts/util/i18n');
const config = url.parse(window.location.toString(), true).query; const config = url.parse(window.location.toString(), true).query;
const { locale } = config; const { locale } = config;
@ -16,7 +16,7 @@ window._ = require('lodash');
window.getVersion = () => config.version; window.getVersion = () => config.version;
window.theme = config.theme; window.theme = config.theme;
window.i18n = i18n.setupi18n(locale, localeMessages); window.i18n = setupI18n({ initialLocale: locale, initialDictionary: localeMessages });
// got.js appears to need this to successfully submit debug logs to the cloud // got.js appears to need this to successfully submit debug logs to the cloud
window.nodeSetImmediate = setImmediate; window.nodeSetImmediate = setImmediate;

@ -3,7 +3,7 @@
const { ipcRenderer } = require('electron'); const { ipcRenderer } = require('electron');
const url = require('url'); const url = require('url');
const i18n = require('./ts/util/i18n'); const { setupI18n } = require('./ts/util/i18n');
const config = url.parse(window.location.toString(), true).query; const config = url.parse(window.location.toString(), true).query;
const { locale } = config; const { locale } = config;
@ -12,7 +12,7 @@ const localeMessages = ipcRenderer.sendSync('locale-data');
// If the app is locked we can't access the database to check the theme. // If the app is locked we can't access the database to check the theme.
window.theme = 'classic-dark'; window.theme = 'classic-dark';
window.primaryColor = 'green'; window.primaryColor = 'green';
window.i18n = i18n.setupi18n(locale, localeMessages); window.i18n = setupI18n({ initialLocale: locale, initialDictionary: localeMessages });
window.getEnvironment = () => config.environment; window.getEnvironment = () => config.environment;
window.getVersion = () => config.version; window.getVersion = () => config.version;

@ -232,7 +232,7 @@ if (config.proxyUrl) {
window.nodeSetImmediate = setImmediate; window.nodeSetImmediate = setImmediate;
const data = require('./ts/data/dataInit'); const data = require('./ts/data/dataInit');
const { setupi18n } = require('./ts/util/i18n'); const { setupI18n } = require('./ts/util/i18n');
window.Signal = data.initData(); window.Signal = data.initData();
const { getConversationController } = require('./ts/session/conversations/ConversationController'); const { getConversationController } = require('./ts/session/conversations/ConversationController');
@ -255,7 +255,7 @@ window.getSeedNodeList = () =>
]; ];
const { locale: localFromEnv } = config; const { locale: localFromEnv } = config;
window.i18n = setupi18n(localFromEnv || 'en', localeMessages); window.i18n = setupI18n({ initialLocale: localFromEnv, initialDictionary: localeMessages });
window.addEventListener('contextmenu', e => { window.addEventListener('contextmenu', e => {
const editable = e && e.target.closest('textarea, input, [contenteditable="true"]'); const editable = e && e.target.closest('textarea, input, [contenteditable="true"]');

@ -22,6 +22,7 @@ import { THEME_GLOBALS } from '../../themes/globals';
import { SessionWrapperModal } from '../SessionWrapperModal'; import { SessionWrapperModal } from '../SessionWrapperModal';
import { SessionIcon, SessionIconButton } from '../icon'; import { SessionIcon, SessionIconButton } from '../icon';
import { SessionSpinner } from '../loading'; import { SessionSpinner } from '../loading';
import { getLocale } from '../../util/i18n';
export type StatusLightType = { export type StatusLightType = {
glowStartDelay: number; glowStartDelay: number;
@ -135,7 +136,7 @@ const OnionPathModalInner = () => {
<Flex container={true} flexDirection="column" alignItems="flex-start"> <Flex container={true} flexDirection="column" alignItems="flex-start">
{nodes.map((snode: Snode | any) => { {nodes.map((snode: Snode | any) => {
const country = reader?.get(snode.ip || '0.0.0.0')?.country; const country = reader?.get(snode.ip || '0.0.0.0')?.country;
const locale = (window.i18n as any).getLocale() as string; const locale = getLocale();
// typescript complains that the [] operator cannot be used with the 'string' coming from getLocale() // typescript complains that the [] operator cannot be used with the 'string' coming from getLocale()
const countryNamesAsAny = country?.names as any; const countryNamesAsAny = country?.names as any;

@ -22,7 +22,6 @@ const dictionarySlice = createSlice({
.then(dictionary => { .then(dictionary => {
state.dictionary = dictionary; state.dictionary = dictionary;
state.locale = action.payload; state.locale = action.payload;
window.locale = action.payload;
}) })
.catch(e => { .catch(e => {
window.log.error('Failed to load dictionary', e); window.log.error('Failed to load dictionary', e);

@ -6,7 +6,7 @@ import { Data } from '../../../data/data';
import { OpenGroupData } from '../../../data/opengroups'; import { OpenGroupData } from '../../../data/opengroups';
import { load } from '../../../node/locale'; import { load } from '../../../node/locale';
import { setupi18n } from '../../../util/i18n'; import { setupI18n } from '../../../util/i18n';
import * as libsessionWorker from '../../../webworker/workers/browser/libsession_worker_interface'; import * as libsessionWorker from '../../../webworker/workers/browser/libsession_worker_interface';
import * as utilWorker from '../../../webworker/workers/browser/util_worker_interface'; import * as utilWorker from '../../../webworker/workers/browser/util_worker_interface';
@ -141,5 +141,5 @@ export async function expectAsyncToThrow(toAwait: () => Promise<any>, errorMessa
/** You must call stubWindowLog() before using */ /** You must call stubWindowLog() before using */
export const stubI18n = () => { export const stubI18n = () => {
const locale = load({ appLocale: 'en', logger: window.log }); const locale = load({ appLocale: 'en', logger: window.log });
stubWindow('i18n', setupi18n('en', locale.messages)); stubWindow('i18n', setupI18n({ initialLocale: 'en', initialDictionary: locale.messages }));
}; };

81
ts/window.d.ts vendored

@ -6,7 +6,13 @@ import { Persistor } from 'redux-persist/es/types';
import { ConversationCollection } from './models/conversation'; import { ConversationCollection } from './models/conversation';
import { PrimaryColorStateType, ThemeStateType } from './themes/constants/colors'; import { PrimaryColorStateType, ThemeStateType } from './themes/constants/colors';
import type { GetMessageArgs, LocalizerDictionary, LocalizerToken } from './types/Localizer'; import {
GetMessageArgs,
I18nMethods,
LocalizerDictionary,
LocalizerToken,
SetupI18nReturnType,
} from './types/Localizer';
import type { Locale } from './util/i18n'; import type { Locale } from './util/i18n';
export interface LibTextsecure { export interface LibTextsecure {
@ -27,6 +33,8 @@ declare global {
clipboard: any; clipboard: any;
getSettingValue: (id: string, comparisonValue?: any) => any; getSettingValue: (id: string, comparisonValue?: any) => any;
setSettingValue: (id: string, value: any) => Promise<void>; setSettingValue: (id: string, value: any) => Promise<void>;
/** NOTE: Because of docstring limitations changes MUST be manually synced between {@link setupI18n.getMessage } and {@link window.i18n } */
/** /**
* Retrieves a localized message string, substituting variables where necessary. * Retrieves a localized message string, substituting variables where necessary.
* *
@ -35,22 +43,79 @@ declare global {
* *
* @returns The localized message string with substitutions applied. * @returns The localized message string with substitutions applied.
* *
* @link [i18n](./util/i18n.ts)
*
* @example * @example
* // The string greeting is 'Hello, {name}!' in the current locale * // The string greeting is 'Hello, {name}!' in the current locale
* window.i18n('greeting', { name: 'Alice' }); * window.i18n('greeting', { name: 'Alice' });
* // => 'Hello, Alice!' * // => 'Hello, Alice!'
*
* // The string search is '{count, plural, one [{found_count} of # match] other [{found_count} of # matches]}' in the current locale
* window.i18n('search', { count: 1, found_count: 1 });
* // => '1 of 1 match'
*/ */
i18n: (<T extends LocalizerToken, R extends LocalizerDictionary[T]>( i18n: (<T extends LocalizerToken, R extends LocalizerDictionary[T]>(
...[token, args]: GetMessageArgs<T> ...[token, args]: GetMessageArgs<T>
) => R) & { ) => R) & {
stripped: <T extends LocalizerToken, R extends LocalizerDictionary[T]>( /** NOTE: Because of docstring limitations changes MUST be manually synced between {@link setupI18n.getRawMessage } and {@link window.i18n.getRawMessage } */
...[token, args]: GetMessageArgs<T> /**
) => R; * Retrieves a localized message string, without substituting any variables. This resolves any plural forms using the given args
* @param token - The token identifying the message to retrieve.
* @param args - An optional record of substitution variables and their replacement values. This is required if the string has dynamic variables.
*
* @returns The localized message string with substitutions applied.
*
* NOTE: This is intended to be used to get the raw string then format it with {@link formatMessageWithArgs}
*
* @example
* // The string greeting is 'Hello, {name}!' in the current locale
* window.i18n.getRawMessage('greeting', { name: 'Alice' });
* // => 'Hello, {name}!'
*
* // The string search is '{count, plural, one [{found_count} of # match] other [{found_count} of # matches]}' in the current locale
* window.i18n.getRawMessage('search', { count: 1, found_count: 1 });
* // => '{found_count} of {count} match'
*/
getRawMessage: I18nMethods['getRawMessage'];
/** NOTE: Because of docstring limitations changes MUST be manually synced between {@link setupI18n.formatMessageWithArgs } and {@link window.i18n.formatMessageWithArgs } */
/**
* Formats a localized message string with arguments and returns the formatted string.
* @param rawMessage - The raw message string to format. After using @see {@link getRawMessage} to get the raw string.
* @param args - An optional record of substitution variables and their replacement values. This
* is required if the string has dynamic variables. This can be optional as a strings args may be defined in @see {@link LOCALE_DEFAULTS}
*
* @returns The formatted message string.
*
* @example
* // The string greeting is 'Hello, {name}!' in the current locale
* window.i18n.getRawMessage('greeting', { name: 'Alice' });
* // => 'Hello, {name}!'
* window.i18n.formatMessageWithArgs('greeting', { name: 'Alice' });
* // => 'Hello, Alice!'
*
* // The string search is '{count, plural, one [{found_count} of # match] other [{found_count} of # matches]}' in the current locale
* window.i18n.getRawMessage('search', { count: 1, found_count: 1 });
* // => '{found_count} of {count} match'
* window.i18n.formatMessageWithArgs('search', { count: 1, found_count: 1 });
* // => '1 of 1 match'
*/
formatMessageWithArgs: I18nMethods['formatMessageWithArgs'];
/** NOTE: Because of docstring limitations changes MUST be manually synced between {@link setupI18n.stripped } and {@link window.i18n.stripped } */
/**
* Retrieves a localized message string, substituting variables where necessary. Then strips the message of any HTML and custom tags.
*
* @param token - The token identifying the message to retrieve.
* @param args - An optional record of substitution variables and their replacement values. This is required if the string has dynamic variables.
*
* @returns The localized message string with substitutions applied. Any HTML and custom tags are removed.
*
* @example
* // The string greeting is 'Hello, {name}! <b>Welcome!</b>' in the current locale
* window.i18n.stripped('greeting', { name: 'Alice' });
* // => 'Hello, Alice! Welcome!'
*/
stripped: I18nMethods['stripped'];
}; };
/** NOTE: This locale is a readonly backup of the locale in the store. Use {@link getLocale} instead. */
locale: Readonly<Locale>;
log: any; log: any;
sessionFeatureFlags: { sessionFeatureFlags: {
useOnionRequests: boolean; useOnionRequests: boolean;

Loading…
Cancel
Save