mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-04-25 19:08:04 +01:00
ICU types
This commit is contained in:
83
ts/util/getICUMessageParams.ts
Normal file
83
ts/util/getICUMessageParams.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { TYPE, parse } from '@formatjs/icu-messageformat-parser';
|
||||
import type {
|
||||
MessageFormatElement,
|
||||
PluralOrSelectOption,
|
||||
} from '@formatjs/icu-messageformat-parser';
|
||||
import { missingCaseError } from './missingCaseError';
|
||||
|
||||
export type ICUMessageParamType = Readonly<
|
||||
| {
|
||||
type: 'string' | 'date' | 'number' | 'jsx' | 'time';
|
||||
}
|
||||
| {
|
||||
type: 'select';
|
||||
validOptions: ReadonlyArray<string>;
|
||||
}
|
||||
>;
|
||||
|
||||
export function getICUMessageParams(
|
||||
message: string,
|
||||
defaultRichTextElementNames: Array<string> = []
|
||||
): Map<string, ICUMessageParamType> {
|
||||
const params = new Map();
|
||||
|
||||
function visitOptions(options: Record<string, PluralOrSelectOption>) {
|
||||
for (const option of Object.values(options)) {
|
||||
visit(option.value);
|
||||
}
|
||||
}
|
||||
|
||||
function visit(elements: ReadonlyArray<MessageFormatElement>) {
|
||||
for (const element of elements) {
|
||||
switch (element.type) {
|
||||
case TYPE.argument:
|
||||
params.set(element.value, { type: 'string' });
|
||||
break;
|
||||
case TYPE.date:
|
||||
params.set(element.value, { type: 'Date' });
|
||||
break;
|
||||
case TYPE.literal:
|
||||
break;
|
||||
case TYPE.number:
|
||||
params.set(element.value, { type: 'number' });
|
||||
break;
|
||||
case TYPE.plural:
|
||||
params.set(element.value, { type: 'number' });
|
||||
visitOptions(element.options);
|
||||
break;
|
||||
case TYPE.pound:
|
||||
break;
|
||||
case TYPE.select: {
|
||||
const validOptions = Object.entries(element.options)
|
||||
// We use empty {other ...} to satisfy smartling, but don't allow
|
||||
// it in the app.
|
||||
.filter(([key, { value }]) => key !== 'other' || value.length)
|
||||
.map(([key]) => key);
|
||||
params.set(element.value, { type: 'select', validOptions });
|
||||
visitOptions(element.options);
|
||||
break;
|
||||
}
|
||||
case TYPE.tag:
|
||||
params.set(element.value, { type: 'jsx' });
|
||||
visit(element.children);
|
||||
break;
|
||||
case TYPE.time:
|
||||
params.set(element.value, { type: 'time' });
|
||||
break;
|
||||
default:
|
||||
throw missingCaseError(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
visit(parse(message));
|
||||
|
||||
for (const defaultRichTextElementName of defaultRichTextElementNames) {
|
||||
params.delete(defaultRichTextElementName);
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import type { RawBodyRange } from '../types/BodyRange';
|
||||
import type { MessageAttributesType } from '../model-types.d';
|
||||
import type { ReplacementValuesType } from '../types/I18N';
|
||||
import type { ICUStringMessageParamsByKeyType } from '../types/Util';
|
||||
import * as Attachment from '../types/Attachment';
|
||||
import * as EmbeddedContact from '../types/EmbeddedContact';
|
||||
import * as GroupChange from '../groupChange';
|
||||
@@ -149,13 +149,13 @@ export function getNotificationDataForMessage(
|
||||
? conversation.getTitle()
|
||||
: window.i18n('icu:unknownContact');
|
||||
},
|
||||
renderString: (
|
||||
key: string,
|
||||
renderIntl: <Key extends keyof ICUStringMessageParamsByKeyType>(
|
||||
key: Key,
|
||||
_i18n: unknown,
|
||||
components: ReplacementValuesType<string | number> | undefined
|
||||
components: ICUStringMessageParamsByKeyType[Key]
|
||||
) => {
|
||||
// eslint-disable-next-line local-rules/valid-i18n-keys
|
||||
return window.i18n(key, components);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return window.i18n(key, components as any);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ export function getNotificationTextForMessage(
|
||||
if (shouldIncludeEmoji) {
|
||||
return window.i18n('icu:message--getNotificationText--text-with-emoji', {
|
||||
text: result.body,
|
||||
emoji,
|
||||
emoji: emoji ?? '',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,10 @@ import React from 'react';
|
||||
import type { IntlShape } from 'react-intl';
|
||||
import { createIntl, createIntlCache } from 'react-intl';
|
||||
import type { LocaleMessageType, LocaleMessagesType } from '../types/I18N';
|
||||
import type { LocalizerType, ReplacementValuesType } from '../types/Util';
|
||||
import type {
|
||||
LocalizerType,
|
||||
ICUStringMessageParamsByKeyType,
|
||||
} from '../types/Util';
|
||||
import { strictAssert } from './assert';
|
||||
import { Emojify } from '../components/conversation/Emojify';
|
||||
import * as log from '../logging/log';
|
||||
@@ -77,27 +80,25 @@ export function createCachedIntl(
|
||||
return intl;
|
||||
}
|
||||
|
||||
function normalizeSubstitutions(
|
||||
substitutions?: ReplacementValuesType
|
||||
): ReplacementValuesType | undefined {
|
||||
function normalizeSubstitutions<
|
||||
Substitutions extends Record<string, string | number | Date> | undefined
|
||||
>(substitutions?: Substitutions): Substitutions | undefined {
|
||||
if (!substitutions) {
|
||||
return;
|
||||
}
|
||||
const normalized: ReplacementValuesType = {};
|
||||
const keys = Object.keys(substitutions);
|
||||
if (keys.length === 0) {
|
||||
const normalized: Record<string, string | number | Date> = {};
|
||||
const entries = Object.entries(substitutions);
|
||||
if (entries.length === 0) {
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < keys.length; i += 1) {
|
||||
const key = keys[i];
|
||||
const value = substitutions[key];
|
||||
for (const [key, value] of entries) {
|
||||
if (typeof value === 'string') {
|
||||
normalized[key] = bidiIsolate(value);
|
||||
} else {
|
||||
normalized[key] = value;
|
||||
}
|
||||
}
|
||||
return normalized;
|
||||
return normalized as Substitutions;
|
||||
}
|
||||
|
||||
export function setupI18n(
|
||||
@@ -113,7 +114,12 @@ export function setupI18n(
|
||||
|
||||
const intl = createCachedIntl(locale, filterLegacyMessages(messages));
|
||||
|
||||
const localizer: LocalizerType = (key, substitutions) => {
|
||||
const localizer: LocalizerType = (<
|
||||
Key extends keyof ICUStringMessageParamsByKeyType
|
||||
>(
|
||||
key: Key,
|
||||
substitutions: ICUStringMessageParamsByKeyType[Key]
|
||||
) => {
|
||||
const result = intl.formatMessage(
|
||||
{ id: key },
|
||||
normalizeSubstitutions(substitutions)
|
||||
@@ -122,7 +128,7 @@ export function setupI18n(
|
||||
strictAssert(result !== key, `i18n: missing translation for "${key}"`);
|
||||
|
||||
return result;
|
||||
};
|
||||
}) as LocalizerType;
|
||||
|
||||
localizer.getIntl = () => {
|
||||
return intl;
|
||||
|
||||
Reference in New Issue
Block a user