From f9fb345599161bdcc7d072e59c469500264d0ec9 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Wed, 21 Aug 2024 17:32:38 +1000 Subject: [PATCH] feat: window i18n types and docs with safe setup and fallbacks --- about_preload.js | 7 +- debug_log_preload.js | 4 +- password_preload.js | 4 +- preload.js | 4 +- .../dialog/OnionStatusPathDialog.tsx | 3 +- ts/state/ducks/dictionary.tsx | 1 - ts/test/test-utils/utils/stubbing.ts | 4 +- ts/window.d.ts | 81 +++++++++++++++++-- 8 files changed, 88 insertions(+), 20 deletions(-) diff --git a/about_preload.js b/about_preload.js index be3cb1ba8..223ebafaa 100644 --- a/about_preload.js +++ b/about_preload.js @@ -4,14 +4,17 @@ const { ipcRenderer } = require('electron'); const url = require('url'); 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 { locale } = config; const localeMessages = ipcRenderer.sendSync('locale-data'); window.theme = config.theme; -window.i18n = i18n.setupi18n(locale, localeMessages); +window.i18n = setupI18n({ + initialLocale: locale, + initialDictionary: localeMessages, +}); window.getOSRelease = () => `${os.type()} ${os.release()}, Node.js ${config.node_version} ${os.platform()} ${os.arch()}`; diff --git a/debug_log_preload.js b/debug_log_preload.js index ff5e03054..ba31cfced 100644 --- a/debug_log_preload.js +++ b/debug_log_preload.js @@ -6,7 +6,7 @@ const url = require('url'); 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 { locale } = config; @@ -16,7 +16,7 @@ window._ = require('lodash'); window.getVersion = () => config.version; 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 window.nodeSetImmediate = setImmediate; diff --git a/password_preload.js b/password_preload.js index 7c6912dc3..377d46b64 100644 --- a/password_preload.js +++ b/password_preload.js @@ -3,7 +3,7 @@ const { ipcRenderer } = require('electron'); 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 { 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. window.theme = 'classic-dark'; window.primaryColor = 'green'; -window.i18n = i18n.setupi18n(locale, localeMessages); +window.i18n = setupI18n({ initialLocale: locale, initialDictionary: localeMessages }); window.getEnvironment = () => config.environment; window.getVersion = () => config.version; diff --git a/preload.js b/preload.js index d061e1f07..5d3753b76 100644 --- a/preload.js +++ b/preload.js @@ -232,7 +232,7 @@ if (config.proxyUrl) { window.nodeSetImmediate = setImmediate; const data = require('./ts/data/dataInit'); -const { setupi18n } = require('./ts/util/i18n'); +const { setupI18n } = require('./ts/util/i18n'); window.Signal = data.initData(); const { getConversationController } = require('./ts/session/conversations/ConversationController'); @@ -255,7 +255,7 @@ window.getSeedNodeList = () => ]; const { locale: localFromEnv } = config; -window.i18n = setupi18n(localFromEnv || 'en', localeMessages); +window.i18n = setupI18n({ initialLocale: localFromEnv, initialDictionary: localeMessages }); window.addEventListener('contextmenu', e => { const editable = e && e.target.closest('textarea, input, [contenteditable="true"]'); diff --git a/ts/components/dialog/OnionStatusPathDialog.tsx b/ts/components/dialog/OnionStatusPathDialog.tsx index 670c7551d..446bb7332 100644 --- a/ts/components/dialog/OnionStatusPathDialog.tsx +++ b/ts/components/dialog/OnionStatusPathDialog.tsx @@ -22,6 +22,7 @@ import { THEME_GLOBALS } from '../../themes/globals'; import { SessionWrapperModal } from '../SessionWrapperModal'; import { SessionIcon, SessionIconButton } from '../icon'; import { SessionSpinner } from '../loading'; +import { getLocale } from '../../util/i18n'; export type StatusLightType = { glowStartDelay: number; @@ -135,7 +136,7 @@ const OnionPathModalInner = () => { {nodes.map((snode: Snode | any) => { 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() const countryNamesAsAny = country?.names as any; diff --git a/ts/state/ducks/dictionary.tsx b/ts/state/ducks/dictionary.tsx index 691f23652..a49d3317b 100644 --- a/ts/state/ducks/dictionary.tsx +++ b/ts/state/ducks/dictionary.tsx @@ -22,7 +22,6 @@ const dictionarySlice = createSlice({ .then(dictionary => { state.dictionary = dictionary; state.locale = action.payload; - window.locale = action.payload; }) .catch(e => { window.log.error('Failed to load dictionary', e); diff --git a/ts/test/test-utils/utils/stubbing.ts b/ts/test/test-utils/utils/stubbing.ts index 7a6e5c02b..142b438c7 100644 --- a/ts/test/test-utils/utils/stubbing.ts +++ b/ts/test/test-utils/utils/stubbing.ts @@ -6,7 +6,7 @@ import { Data } from '../../../data/data'; import { OpenGroupData } from '../../../data/opengroups'; 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 utilWorker from '../../../webworker/workers/browser/util_worker_interface'; @@ -141,5 +141,5 @@ export async function expectAsyncToThrow(toAwait: () => Promise, errorMessa /** You must call stubWindowLog() before using */ export const stubI18n = () => { const locale = load({ appLocale: 'en', logger: window.log }); - stubWindow('i18n', setupi18n('en', locale.messages)); + stubWindow('i18n', setupI18n({ initialLocale: 'en', initialDictionary: locale.messages })); }; diff --git a/ts/window.d.ts b/ts/window.d.ts index 888f2725a..f29b89618 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -6,7 +6,13 @@ import { Persistor } from 'redux-persist/es/types'; import { ConversationCollection } from './models/conversation'; 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'; export interface LibTextsecure { @@ -27,6 +33,8 @@ declare global { clipboard: any; getSettingValue: (id: string, comparisonValue?: any) => any; setSettingValue: (id: string, value: any) => Promise; + + /** 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. * @@ -35,22 +43,79 @@ declare global { * * @returns The localized message string with substitutions applied. * - * @link [i18n](./util/i18n.ts) - * * @example * // The string greeting is 'Hello, {name}!' in the current locale * window.i18n('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('search', { count: 1, found_count: 1 }); + * // => '1 of 1 match' */ i18n: (( ...[token, args]: GetMessageArgs ) => R) & { - stripped: ( - ...[token, args]: GetMessageArgs - ) => R; + /** NOTE: Because of docstring limitations changes MUST be manually synced between {@link setupI18n.getRawMessage } and {@link window.i18n.getRawMessage } */ + /** + * 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}! Welcome!' 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; log: any; sessionFeatureFlags: { useOnionRequests: boolean;