Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PoC - Typed Translations #24250

Draft
wants to merge 3 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5,990 changes: 5,990 additions & 0 deletions app/_locales/en/messages.json.d.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,14 @@ export default function HoldToRevealModal({
: 'holdToRevealContentPrivateKey';

// If this is done inline, verify-locales will output `Forbidden use of template strings in 't' function`
const holdToRevealContent1 = `${holdToRevealContent}1`;
const holdToRevealContent2 = `${holdToRevealContent}2`;
const holdToRevealContent1 = `${holdToRevealContent}1` as const;
const holdToRevealContent2 = `${holdToRevealContent}2` as const;

// This is here to stop yarn verify-locales from removing these strings
t('holdToRevealContentPrivateKey1');
t('holdToRevealContentPrivateKey2');
t('holdToRevealContent1');
t('holdToRevealContent2');
// t('holdToRevealContentPrivateKey1');
// t('holdToRevealContentPrivateKey2');
// t('holdToRevealContent1');
// t('holdToRevealContent2');
Comment on lines +55 to +58
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The yarn verify-locales still works if we just add these as comments - don't need to actually call the translations.


const MainContent = () => {
return (
Expand Down
4 changes: 3 additions & 1 deletion ui/components/app/name/name-details/name-details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,9 @@ function getProviderLabel(
if (LOCALIZED_PROVIDERS.includes(sourceId)) {
// Use intermediate variable to avoid "Forbidden use of template strings
// in 't' function" error.
const messageKey = `nameProvider_${sourceId}`;
const messageKey = `nameProvider_${
sourceId as 'ens' | 'etherscan' | 'lens' | 'token'
}` as const;
return t(messageKey);
Comment on lines +99 to 102
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a quirk due to the translations being a bit more strict - it doesn't accept any arbitrary string.
Meh we can loosen it or have a separate type definition.

In fact could slowly migrate from a loose function definition to a strict function definition if we wanted to.

}
return nameSources[sourceId]?.label ?? sourceId;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ export function AssetBalance({ asset, error }: AssetBalanceProps) {
<AssetBalanceText asset={asset} balanceColor={balanceColor} />
{error ? (
<Text variant={TextVariant.bodySm} color={TextColor.errorDefault}>
. {t(error)}
.{' '}
{
// @ts-expect-error purposely widened string
t(error)
}
</Text>
) : null}
</Box>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,9 @@ export const Connections = () => {
menuType={AccountListItemMenuTypes.Connection}
currentTabOrigin={activeTabOrigin}
isActive={
mergedAccountsProps.isAccountActive ? t('active') : null
mergedAccountsProps.isAccountActive
? Boolean(t('active'))
: Boolean(null)
Comment on lines +268 to +270
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lol, this isActive expects a boolean, but we were passing it a string | null

}
onActionClick={setShowAccountDisconnectedToast}
/>
Expand Down
36 changes: 36 additions & 0 deletions ui/contexts/i18n.types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type TranslationsJson from '../../app/_locales/en/messages.json.d.ts';

type TJson = typeof TranslationsJson;
type Keys = keyof TJson;

// Gets a message from a given key
type GetMessage<K extends Keys> = (typeof TranslationsJson)[K]['message'];

// A message might have $1, etc. This will aggregate all these into a args tuple
// Useful so we know how many args a given translation needs!
// Can be used to find outdated translations that forgot to remove args, or translations that forgot args.
type GetArgTuple<
Message extends string,
Acc extends unknown[] = [],
> = Message extends ''
? Acc
: Message extends `${string}$${number}${infer Tail}`
? GetArgTuple<Tail, [...Acc, unknown]>
: Acc;

// Yay JS. The first func sig are for cases where we have multiple params...
type FuncSig1 = <K extends Keys>(
key: K,
...args: GetArgTuple<GetMessage<K>>
) => string;

// ...This second func sig is where developers use an array for multiple args.
type FuncSig2 = <K extends Keys>(
key: K,
args: GetArgTuple<GetMessage<K>>,
) => string;

// A bit cheeky here but it works.
// We declare that the translation can be either these 2 signatures
// NOTE - using intersection instead of unions by how functions are contravariant (or one of those terms).
export type TranslationFn = FuncSig1 & FuncSig2;
2 changes: 1 addition & 1 deletion ui/hooks/useI18nContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { I18nContext } from '../contexts/i18n';
* A time saving shortcut to using useContext + I18ncontext in many
* different places.
*
* @returns {Function} I18n function from contexts/I18n.js
* @returns {import('../contexts/i18n.types').TranslationFn} I18n function from contexts/I18n.js
*/
export function useI18nContext() {
return useContext(I18nContext);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ const getDisplayValues = ({
} else if (isSmartTransactionCancelled) {
return {
title: t('smartTransactionCancelled'),
description: t('smartTransactionCancelledDescription', [countdown]),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfect example! This translation doesn't need an argument!

Thankfully from the files I could test (only typescript files), there weren't any issues where we were using translations but forgetting to pass args.

description: t('smartTransactionCancelledDescription'),
iconName: IconName.Danger,
iconColor: IconColor.errorDefault,
};
Expand Down