Don't import emoji-datasource in main

This commit is contained in:
Fedor Indutny 2024-03-12 10:32:11 -07:00 committed by GitHub
parent f5953d0986
commit ab226f29a9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 162 additions and 124 deletions

View file

@ -6,7 +6,8 @@ import { readFileSync } from 'fs';
import { merge } from 'lodash'; import { merge } from 'lodash';
import * as LocaleMatcher from '@formatjs/intl-localematcher'; import * as LocaleMatcher from '@formatjs/intl-localematcher';
import { z } from 'zod'; import { z } from 'zod';
import { setupI18n } from '../ts/util/setupI18n'; import { setupI18n } from '../ts/util/setupI18nMain';
import { shouldNeverBeCalled } from '../ts/util/shouldNeverBeCalled';
import type { LoggerType } from '../ts/types/Logging'; import type { LoggerType } from '../ts/types/Logging';
import type { HourCyclePreference, LocaleMessagesType } from '../ts/types/I18N'; import type { HourCyclePreference, LocaleMessagesType } from '../ts/types/I18N';
@ -142,7 +143,9 @@ export function load({
// We start with english, then overwrite that with anything present in locale // We start with english, then overwrite that with anything present in locale
const finalMessages = merge(englishMessages, matchedLocaleMessages); const finalMessages = merge(englishMessages, matchedLocaleMessages);
const i18n = setupI18n(matchedLocale, finalMessages); const i18n = setupI18n(matchedLocale, finalMessages, {
renderEmojify: shouldNeverBeCalled,
});
const direction = const direction =
localeDirectionTestingOverride ?? getLocaleDirection(matchedLocale, logger); localeDirectionTestingOverride ?? getLocaleDirection(matchedLocale, logger);
logger.info(`locale: Text info direction for ${matchedLocale}: ${direction}`); logger.info(`locale: Text info direction for ${matchedLocale}: ${direction}`);

View file

@ -1,44 +1,18 @@
// Copyright 2018 Signal Messenger, LLC // Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import type { IntlShape } from 'react-intl'; import type { IntlShape } from 'react-intl';
import { createIntl, createIntlCache } from 'react-intl'; import React from 'react';
import type { LocaleMessageType, LocaleMessagesType } from '../types/I18N'; import type { LocaleMessagesType } from '../types/I18N';
import type { import type { LocalizerType } from '../types/Util';
LocalizerType,
ICUStringMessageParamsByKeyType,
} from '../types/Util';
import { strictAssert } from './assert';
import { Emojify } from '../components/conversation/Emojify'; import { Emojify } from '../components/conversation/Emojify';
import * as log from '../logging/log'; import {
import * as Errors from '../types/errors'; createCachedIntl as createCachedIntlMain,
import { Environment, getEnvironment } from '../environment'; setupI18n as setupI18nMain,
import { bidiIsolate } from './unicodeBidi'; } from './setupI18nMain';
import { strictAssert } from './assert';
export function isLocaleMessageType( export { isLocaleMessageType } from './setupI18nMain';
value: unknown
): value is LocaleMessageType {
return (
typeof value === 'object' &&
value != null &&
Object.hasOwn(value, 'messageformat')
);
}
function filterLegacyMessages(
messages: LocaleMessagesType
): Record<string, string> {
const icuMessages: Record<string, string> = {};
for (const [key, value] of Object.entries(messages)) {
if (isLocaleMessageType(value) && value.messageformat != null) {
icuMessages[key] = value.messageformat;
}
}
return icuMessages;
}
export function renderEmojify(parts: ReadonlyArray<unknown>): JSX.Element { export function renderEmojify(parts: ReadonlyArray<unknown>): JSX.Element {
strictAssert(parts.length === 1, '<emojify> must contain only one child'); strictAssert(parts.length === 1, '<emojify> must contain only one child');
@ -51,96 +25,12 @@ export function createCachedIntl(
locale: string, locale: string,
icuMessages: Record<string, string> icuMessages: Record<string, string>
): IntlShape { ): IntlShape {
const intlCache = createIntlCache(); return createCachedIntlMain(locale, icuMessages, { renderEmojify });
const intl = createIntl(
{
locale: locale.replace('_', '-'), // normalize supported locales to browser format
messages: icuMessages,
defaultRichTextElements: {
emojify: renderEmojify,
},
onError(error) {
log.error('intl.onError', Errors.toLogFormat(error));
},
onWarn(warning) {
if (
getEnvironment() === Environment.Test &&
warning.includes(
// This warning is very noisy during tests
'"defaultRichTextElements" was specified but "message" was not pre-compiled.'
)
) {
return;
}
log.warn('intl.onWarn', warning);
},
},
intlCache
);
return intl;
}
function normalizeSubstitutions<
Substitutions extends Record<string, string | number | Date> | undefined
>(substitutions?: Substitutions): Substitutions | undefined {
if (!substitutions) {
return;
}
const normalized: Record<string, string | number | Date> = {};
const entries = Object.entries(substitutions);
if (entries.length === 0) {
return;
}
for (const [key, value] of entries) {
if (typeof value === 'string') {
normalized[key] = bidiIsolate(value);
} else {
normalized[key] = value;
}
}
return normalized as Substitutions;
} }
export function setupI18n( export function setupI18n(
locale: string, locale: string,
messages: LocaleMessagesType messages: LocaleMessagesType
): LocalizerType { ): LocalizerType {
if (!locale) { return setupI18nMain(locale, messages, { renderEmojify });
throw new Error('i18n: locale parameter is required');
}
if (!messages) {
throw new Error('i18n: messages parameter is required');
}
const intl = createCachedIntl(locale, filterLegacyMessages(messages));
const localizer: LocalizerType = (<
Key extends keyof ICUStringMessageParamsByKeyType
>(
key: Key,
substitutions: ICUStringMessageParamsByKeyType[Key]
) => {
const result = intl.formatMessage(
{ id: key },
normalizeSubstitutions(substitutions)
);
strictAssert(result !== key, `i18n: missing translation for "${key}"`);
return result;
}) as LocalizerType;
localizer.getIntl = () => {
return intl;
};
localizer.getLocale = () => locale;
localizer.getLocaleMessages = () => messages;
localizer.getLocaleDirection = () => {
return window.SignalContext.getResolvedMessagesLocaleDirection();
};
localizer.getHourCyclePreference = () => {
return window.SignalContext.getHourCyclePreference();
};
return localizer;
} }

145
ts/util/setupI18nMain.ts Normal file
View file

@ -0,0 +1,145 @@
// Copyright 2018 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { IntlShape } from 'react-intl';
import { createIntl, createIntlCache } from 'react-intl';
import type { LocaleMessageType, LocaleMessagesType } from '../types/I18N';
import type {
LocalizerType,
ICUStringMessageParamsByKeyType,
} from '../types/Util';
import { strictAssert } from './assert';
import * as log from '../logging/log';
import * as Errors from '../types/errors';
import { Environment, getEnvironment } from '../environment';
import { bidiIsolate } from './unicodeBidi';
export function isLocaleMessageType(
value: unknown
): value is LocaleMessageType {
return (
typeof value === 'object' &&
value != null &&
Object.hasOwn(value, 'messageformat')
);
}
export type SetupI18nOptionsType = Readonly<{
renderEmojify: (parts: ReadonlyArray<unknown>) => JSX.Element | void;
}>;
export function createCachedIntl(
locale: string,
icuMessages: Record<string, string>,
{ renderEmojify }: SetupI18nOptionsType
): IntlShape {
const intlCache = createIntlCache();
const intl = createIntl(
{
locale: locale.replace('_', '-'), // normalize supported locales to browser format
messages: icuMessages,
defaultRichTextElements: {
emojify: renderEmojify,
},
onError(error) {
log.error('intl.onError', Errors.toLogFormat(error));
},
onWarn(warning) {
if (
getEnvironment() === Environment.Test &&
warning.includes(
// This warning is very noisy during tests
'"defaultRichTextElements" was specified but "message" was not pre-compiled.'
)
) {
return;
}
log.warn('intl.onWarn', warning);
},
},
intlCache
);
return intl;
}
function normalizeSubstitutions<
Substitutions extends Record<string, string | number | Date> | undefined
>(substitutions?: Substitutions): Substitutions | undefined {
if (!substitutions) {
return;
}
const normalized: Record<string, string | number | Date> = {};
const entries = Object.entries(substitutions);
if (entries.length === 0) {
return;
}
for (const [key, value] of entries) {
if (typeof value === 'string') {
normalized[key] = bidiIsolate(value);
} else {
normalized[key] = value;
}
}
return normalized as Substitutions;
}
function filterLegacyMessages(
messages: LocaleMessagesType
): Record<string, string> {
const icuMessages: Record<string, string> = {};
for (const [key, value] of Object.entries(messages)) {
if (isLocaleMessageType(value) && value.messageformat != null) {
icuMessages[key] = value.messageformat;
}
}
return icuMessages;
}
export function setupI18n(
locale: string,
messages: LocaleMessagesType,
{ renderEmojify }: SetupI18nOptionsType
): LocalizerType {
if (!locale) {
throw new Error('i18n: locale parameter is required');
}
if (!messages) {
throw new Error('i18n: messages parameter is required');
}
const intl = createCachedIntl(locale, filterLegacyMessages(messages), {
renderEmojify,
});
const localizer: LocalizerType = (<
Key extends keyof ICUStringMessageParamsByKeyType
>(
key: Key,
substitutions: ICUStringMessageParamsByKeyType[Key]
) => {
const result = intl.formatMessage(
{ id: key },
normalizeSubstitutions(substitutions)
);
strictAssert(result !== key, `i18n: missing translation for "${key}"`);
return result;
}) as LocalizerType;
localizer.getIntl = () => {
return intl;
};
localizer.getLocale = () => locale;
localizer.getLocaleMessages = () => messages;
localizer.getLocaleDirection = () => {
return window.SignalContext.getResolvedMessagesLocaleDirection();
};
localizer.getHourCyclePreference = () => {
return window.SignalContext.getHourCyclePreference();
};
return localizer;
}