Skip to content

Commit

Permalink
feat(i18n): enhance types (#18347)
Browse files Browse the repository at this point in the history
* feat(i18n): generate types

* fix: types

* chore: cleanup

* chore: remove test file strings

* fix: types

* fix: types

* fix: replace child_process with cross-spawn for type generation

* fix: refactor type generation script and improve output format

* chore: update yarn.lock

* chore: update yarn.lock
  • Loading branch information
olafsulich authored Nov 27, 2024
1 parent f2c0cf6 commit 084dbf2
Show file tree
Hide file tree
Showing 91 changed files with 2,167 additions and 301 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ module.exports = {
'src/worker/',
'src/script/components/Icon.tsx',
'*.js',
'src/types/i18n.d.ts',
],
parserOptions: {
project: ['./tsconfig.build.json', './server/tsconfig.json'],
Expand Down
2 changes: 1 addition & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ CHANGELOG.md
node_modules
npm-debug.log
yarn-error.log

src/types/i18n.d.ts
38 changes: 38 additions & 0 deletions bin/transalations_generate_types.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
const fs = require('fs');
const path = require('path');

const escapeString = string => {
return string
.replace(/\\/g, '\\\\') // Escape backslashes first
.replace(/'/g, "\\'") // Escape single quotes
.replace(/\n/g, '\\n') // Escape newlines
.replace(/\r/g, '\\r') // Escape carriage returns
.replace(/\t/g, '\\t'); // Escape tabs
};

const generateTypeDefinitions = (jsonPath, outputPath) => {
const json = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));

const header = `// This file is autogenerated by the script. DO NOT EDIT BY HAND.
// To update this file, run: yarn run translate:generate-types
`;

const body = Object.entries(json)
.map(([key, value]) => ` '${key}': \`${escapeString(value)}\`;`)
.join('\n');

const content = `${header}declare module 'I18n/en-US.json' {
const translations: {
${body}
};
export default translations;
}
`;

fs.writeFileSync(outputPath, content);
};

const ROOT_PATH = path.resolve(__dirname, '..');

generateTypeDefinitions(path.join(ROOT_PATH, 'src/i18n/en-US.json'), path.join(ROOT_PATH, 'src/types/i18n.d.ts'));
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@
"babel-loader": "9.2.1",
"babel-plugin-transform-import-meta": "2.2.1",
"cross-env": "7.0.3",
"cross-spawn": "^7.0.6",
"css-loader": "7.1.2",
"cssnano": "7.0.6",
"dexie": "4.0.7",
Expand Down Expand Up @@ -202,7 +203,9 @@
"test:server": "cd server && yarn test",
"test:types": "tsc --project tsconfig.build.json --noEmit && cd server && tsc --noEmit",
"translate:extract": "i18next-scanner 'src/{page,script}/**/*.{js,html,htm}'",
"translate:merge": "formatjs extract --format './bin/translations_extract_formatter.js' --out-file './src/i18n/en-US.json'"
"translate:merge": "formatjs extract --format './bin/translations_extract_formatter.js' --out-file './src/i18n/en-US.json'",
"translate:generate-types": "node ./bin/transalations_generate_types.js",
"typecheck": "tsc --noEmit"
},
"resolutions": {
"cross-spawn": "7.0.6",
Expand Down
2 changes: 1 addition & 1 deletion src/i18n/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -687,7 +687,7 @@
"featureConfigChangeModalAudioVideoDescriptionItemCameraEnabled": "Camera in calls is enabled",
"featureConfigChangeModalAudioVideoHeadline": "There has been a change in {brandName}",
"featureConfigChangeModalConferenceCallingEnabled": "Your team was upgraded to {brandName} Enterprise, which gives you access to features such as conference calls and more. [link]Learn more about {brandName} Enterprise[/link]",
"featureConfigChangeModalConferenceCallingTitle": "{brandName} Enterprise",
"featureConfigChangeModalConferenceCallingHeadline": "{brandName} Enterprise",
"featureConfigChangeModalConversationGuestLinksDescriptionItemConversationGuestLinksDisabled": "Generating guest links is now disabled for all group admins.",
"featureConfigChangeModalConversationGuestLinksDescriptionItemConversationGuestLinksEnabled": "Generating guest links is now enabled for all group admins.",
"featureConfigChangeModalConversationGuestLinksHeadline": "Team settings changed",
Expand Down
34 changes: 26 additions & 8 deletions src/script/E2EIdentity/Modals/Modals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,20 @@ export const getModalOptions = ({
</svg>
</div>
`;

const gracePeriodOverParagraph = t('acme.settingsChanged.gracePeriodOver.paragraph', undefined, {
br: '<br>',
...replaceLearnMore,
});

const settingsChangedParagraph = t('acme.settingsChanged.paragraph', undefined, {br: '<br>', ...replaceLearnMore});

switch (type) {
case ModalType.ENROLL:
options = {
text: {
closeBtnLabel: t('acme.settingsChanged.button.close'),
htmlMessage: extraParams?.isGracePeriodOver
? t('acme.settingsChanged.gracePeriodOver.paragraph', {}, {br: '<br>', ...replaceLearnMore})
: t('acme.settingsChanged.paragraph', {}, {br: '<br>', ...replaceLearnMore}),
htmlMessage: extraParams?.isGracePeriodOver ? gracePeriodOverParagraph : settingsChangedParagraph,
title: t('acme.settingsChanged.headline.alt'),
},
primaryAction: {
Expand All @@ -109,8 +115,14 @@ export const getModalOptions = ({
text: {
closeBtnLabel: t('acme.renewCertificate.button.close'),
htmlMessage: extraParams?.isGracePeriodOver
? t('acme.renewCertificate.gracePeriodOver.paragraph')
: t('acme.renewCertificate.paragraph'),
? // @ts-expect-error
// the "url" should be provided
// TODO: check it when changing this code
t('acme.renewCertificate.gracePeriodOver.paragraph')
: // @ts-expect-error
// the "url" should be provided
// TODO: check it when changing this code
t('acme.renewCertificate.paragraph'),
title: t('acme.renewCertificate.headline.alt'),
},
primaryAction: {
Expand Down Expand Up @@ -148,7 +160,10 @@ export const getModalOptions = ({
options = {
text: {
closeBtnLabel: t('acme.settingsChanged.button.close'),
htmlMessage: t('acme.remindLater.paragraph', extraParams?.delayTime),
// @ts-expect-error
// the "url" should be provided
// TODO: check it when changing this code
htmlMessage: t('acme.remindLater.paragraph', {delayTime: extraParams?.delayTime}),
title: t('acme.settingsChanged.headline.alt'),
},
primaryAction: {
Expand All @@ -166,8 +181,8 @@ export const getModalOptions = ({
text: {
closeBtnLabel: t('acme.error.button.close'),
htmlMessage: extraParams?.isGracePeriodOver
? t('acme.error.gracePeriod.paragraph', {}, {br: '<br>'})
: t('acme.error.paragraph', {}, {br: '<br>'}),
? t('acme.error.gracePeriod.paragraph', undefined, {br: '<br>'})
: t('acme.error.paragraph', undefined, {br: '<br>'}),
title: t('acme.error.headline'),
},
primaryAction: {
Expand Down Expand Up @@ -199,6 +214,9 @@ export const getModalOptions = ({
text: {
closeBtnLabel: t('acme.done.button.close'),
htmlMessage: `<div style="text-align: center">${svgHtml}${
// @ts-expect-error
// the "url" should be provided
// TODO: check it when changing this code
extraParams?.isRenewal ? t('acme.renewal.done.paragraph') : t('acme.done.paragraph')
}</div>`,
title: extraParams?.isRenewal ? t('acme.renewal.done.headline') : t('acme.done.headline'),
Expand Down
2 changes: 1 addition & 1 deletion src/script/auth/page/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ const LoginComponent = ({
<div>
<H2 center>{t('login.twoFactorLoginTitle')}</H2>
<Text data-uie-name="label-with-email">
{t('login.twoFactorLoginSubHead', {email: twoFactorLoginData.email})}
{t('login.twoFactorLoginSubHead', {email: twoFactorLoginData.email as string})}
</Text>
<Label markInvalid={!!twoFactorSubmitError}>
<CodeInput
Expand Down
24 changes: 14 additions & 10 deletions src/script/calling/CallingRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -631,7 +631,7 @@ export class CallingRepository {
text: t('callDegradationAction'),
},
text: {
message: t('callDegradationDescription', participant.user.name()),
message: t('callDegradationDescription', {username: participant.user.name()}),
title: t('callDegradationTitle'),
},
},
Expand Down Expand Up @@ -661,8 +661,8 @@ export class CallingRepository {
{
close: () => this.acceptVersionWarning(conversationId),
text: {
message: t('modalCallUpdateClientMessage', brandName),
title: t('modalCallUpdateClientHeadline', brandName),
message: t('modalCallUpdateClientMessage', {brandName}),
title: t('modalCallUpdateClientHeadline', {brandName}),
},
},
'update-client-warning',
Expand Down Expand Up @@ -2291,13 +2291,17 @@ export class CallingRepository {
const modalOptions = {
text: {
closeBtnLabel: t('modalNoCameraCloseBtn'),
htmlMessage: t('modalNoCameraMessage', Config.getConfig().BRAND_NAME, {
'/faqLink': '</a>',
br: '<br>',
faqLink: `<a href="${
Config.getConfig().URL.SUPPORT.CAMERA_ACCESS_DENIED
}" data-uie-name="go-no-camera-faq" target="_blank" rel="noopener noreferrer">`,
}),
htmlMessage: t(
'modalNoCameraMessage',
{brandName: Config.getConfig().BRAND_NAME},
{
'/faqLink': '</a>',
br: '<br>',
faqLink: `<a href="${
Config.getConfig().URL.SUPPORT.CAMERA_ACCESS_DENIED
}" data-uie-name="go-no-camera-faq" target="_blank" rel="noopener noreferrer">`,
},
),
title: t('modalNoCameraTitle'),
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ const MLSVerificationBadge = ({
};

return (
// @ts-expect-error: too broad `translationKeys` type, todo: narrow it down
<TooltipIcon {...mlsVerificationProps} body={t(translationKeys[context])}>
<MLSVerified />
</TooltipIcon>
Expand Down
6 changes: 3 additions & 3 deletions src/script/components/Conversation/Conversation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ export const Conversation = ({

return showWarningModal(
t(isGif ? 'modalGifTooLargeHeadline' : 'modalPictureTooLargeHeadline'),
t(isGif ? 'modalGifTooLargeMessage' : 'modalPictureTooLargeMessage', maxSize),
t(isGif ? 'modalGifTooLargeMessage' : 'modalPictureTooLargeMessage', {number: maxSize}),
);
}
}
Expand Down Expand Up @@ -189,7 +189,7 @@ export const Conversation = ({

if (isFileTooLarge) {
const fileSize = formatBytes(uploadLimit);
showWarningModal(t('modalAssetTooLargeHeadline'), t('modalAssetTooLargeMessage', fileSize));
showWarningModal(t('modalAssetTooLargeHeadline'), t('modalAssetTooLargeMessage', {number: fileSize}));

return;
}
Expand Down Expand Up @@ -294,7 +294,7 @@ export const Conversation = ({
text: t('modalOpenLinkAction'),
},
text: {
htmlMessage: t('modalOpenLinkMessage', href, {}, true),
htmlMessage: t('modalOpenLinkMessage', {link: href}, {}, true),
title: t('modalOpenLinkTitle'),
},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export const ReadOnlyConversationMessage: FC<ReadOnlyConversationMessageProps> =
return (
<ReadOnlyConversationMessageBase>
<span>
{replaceReactComponents(t('otherUserNotSupportMLSMsg'), [
{replaceReactComponents(t('otherUserNotSupportMLSMsg', {participantName: '{participantName}'}), [
{
exactMatch: '{participantName}',
render: () => <strong>{user.name()}</strong>,
Expand All @@ -79,7 +79,7 @@ export const ReadOnlyConversationMessage: FC<ReadOnlyConversationMessageProps> =
return (
<ReadOnlyConversationMessageBase>
<span>
{replaceReactComponents(t('selfNotSupportMLSMsgPart1'), [
{replaceReactComponents(t('selfNotSupportMLSMsgPart1', {selfUserName: '{selfUserName}'}), [
{
exactMatch: '{selfUserName}',
render: () => <strong>{user.name()}</strong>,
Expand All @@ -104,7 +104,7 @@ export const ReadOnlyConversationMessage: FC<ReadOnlyConversationMessageProps> =
return (
<ReadOnlyConversationMessageBase>
<span>
{replaceReactComponents(t('otherUserNoAvailableKeyPackages'), [
{replaceReactComponents(t('otherUserNoAvailableKeyPackages', {participantName: '{participantName}'}), [
{
exactMatch: '{participantName}',
render: () => <strong>{user.name()}</strong>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ export const ConversationListCell = ({
onKeyDown={handleDivKeyDown}
data-uie-name="go-open-conversation"
tabIndex={isFocused ? TabIndex.FOCUSABLE : TabIndex.UNFOCUSABLE}
aria-label={t('accessibility.openConversation', displayName)}
aria-label={t('accessibility.openConversation', {name: displayName})}
aria-describedby={contextMenuKeyboardShortcut}
>
<span id={contextMenuKeyboardShortcut} aria-label={t('accessibility.conversationOptionsMenuAccessKey')} />
Expand Down
2 changes: 1 addition & 1 deletion src/script/components/HistoryImport/HistoryImport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ const HistoryImport = ({user, backupRepository, file, switchContent}: HistoryImp
setErrorSecondary(t('backupImportAccountErrorSecondary'));
} else if (error instanceof IncompatibleBackupError) {
setErrorHeadline(t('backupImportVersionErrorHeadline'));
setErrorSecondary(t('backupImportVersionErrorSecondary', Config.getConfig().BRAND_NAME));
setErrorSecondary(t('backupImportVersionErrorSecondary', {brandName: Config.getConfig().BRAND_NAME}));
} else if (error instanceof IncompatibleBackupFormatError) {
setErrorHeadline(t('backupImportFormatErrorHeadline'));
setErrorSecondary(t('backupImportFormatErrorSecondary'));
Expand Down
4 changes: 2 additions & 2 deletions src/script/components/InputBar/InputBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ export const InputBar = ({
if (isMessageTextTooLong) {
showWarningModal(
t('modalConversationMessageTooLongHeadline'),
t('modalConversationMessageTooLongMessage', CONFIG.MAXIMUM_MESSAGE_LENGTH),
t('modalConversationMessageTooLongMessage', {number: CONFIG.MAXIMUM_MESSAGE_LENGTH}),
);

return;
Expand Down Expand Up @@ -413,7 +413,7 @@ export const InputBar = ({
const {lastModified} = pastedFile;

const date = formatLocale(lastModified || new Date(), 'PP, pp');
const fileName = `${t('conversationSendPastedFile', date)}.${getFileExtension(pastedFile.name)}`;
const fileName = `${t('conversationSendPastedFile', {date})}.${getFileExtension(pastedFile.name)}`;

const newFile = new File([pastedFile], fileName, {
type: pastedFile.type,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -257,8 +257,8 @@ const QuotedMessage: FC<QuotedMessageProps> = ({
tabIndex={messageFocusedTabIndex}
>
{isBeforeToday(timestamp)
? t('replyQuoteTimeStampDate', formatDateNumeral(timestamp))
: t('replyQuoteTimeStampTime', formatTimeShort(timestamp))}
? t('replyQuoteTimeStampDate', {date: formatDateNumeral(timestamp)})
: t('replyQuoteTimeStampTime', {time: formatTimeShort(timestamp)})}
</button>
</>
);
Expand Down
24 changes: 16 additions & 8 deletions src/script/components/MessagesList/Message/DecryptErrorMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,22 @@ const DecryptErrorMessage: React.FC<DecryptErrorMessageProps> = ({message, onCli

const link = Config.getConfig().URL.SUPPORT.DECRYPT_ERROR;
const caption = message.isIdentityChanged
? t('conversationUnableToDecrypt2', message.user().name(), {
'/highlight': '</span>',
highlight: '<span class="label-bold-xs">',
})
: t('conversationUnableToDecrypt1', message.user().name(), {
'/highlight': '</span>',
highlight: '<span class="label-bold-xs">',
});
? t(
'conversationUnableToDecrypt2',
{user: message.user().name()},
{
'/highlight': '</span>',
highlight: '<span class="label-bold-xs">',
},
)
: t(
'conversationUnableToDecrypt1',
{user: message.user().name()},
{
'/highlight': '</span>',
highlight: '<span class="label-bold-xs">',
},
);

return (
<div data-uie-name="element-message-decrypt-error">
Expand Down
7 changes: 3 additions & 4 deletions src/script/components/MessagesList/Message/DeleteMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,9 @@ export interface DeleteMessageProps {
const DeleteMessage: React.FC<DeleteMessageProps> = ({message, onClickAvatar = () => {}}) => {
const deletedTimeStamp = message.deleted_timestamp || 0;

const formattedDeletionTime = t(
'conversationDeleteTimestamp',
formatTimeShort(fromUnixTime(deletedTimeStamp / TIME_IN_MILLIS.SECOND)),
);
const formattedDeletionTime = t('conversationDeleteTimestamp', {
date: formatTimeShort(fromUnixTime(deletedTimeStamp / TIME_IN_MILLIS.SECOND)),
});

return (
<MessageHeader message={message} onClickAvatar={onClickAvatar} uieName="element-message-delete" noBadges noColor>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export const E2EIVerificationMessage = ({message, conversation}: E2EIVerificatio
{isVerified && (
<span
dangerouslySetInnerHTML={{
__html: t('conversation.AllE2EIDevicesVerified', {}, learnMoreReplacement),
__html: t('conversation.AllE2EIDevicesVerified', undefined, learnMoreReplacement),
}}
/>
)}
Expand Down Expand Up @@ -182,15 +182,15 @@ export const E2EIVerificationMessage = ({message, conversation}: E2EIVerificatio
) : (
<span
dangerouslySetInnerHTML={{
__html: t('conversation.E2EISelfUserCertificateRevoked', {}, learnMoreReplacement),
__html: t('conversation.E2EISelfUserCertificateRevoked', undefined, learnMoreReplacement),
}}
/>
))}

{isNoLongerVerified && (
<span
dangerouslySetInnerHTML={{
__html: t('conversation.E2EICertificateNoLongerVerifiedGeneric', {}, learnMoreReplacement),
__html: t('conversation.E2EICertificateNoLongerVerifiedGeneric', undefined, learnMoreReplacement),
}}
/>
)}
Expand Down
Loading

0 comments on commit 084dbf2

Please sign in to comment.