Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
9058a79
feat: add initial japanese translations
mchappell Sep 10, 2025
799700c
feat: add initI18n to use selected language with fallback
mchappell Sep 10, 2025
17b4d35
feat: add analytics types for language selection
mchappell Sep 10, 2025
cb33b26
feat: add background services language change function
mchappell Sep 10, 2025
da0e0c8
feat: add language selection in app
mchappell Sep 10, 2025
b4e7871
fix: remove hardcoded search placeholder
mchappell Sep 25, 2025
d12f1f5
feat: add storage listener for language change
mchappell Sep 25, 2025
9445e09
feat: add missing translation keys
mchappell Sep 25, 2025
db51067
fix: add missing bitcoin-mode translation keys
mchappell Sep 26, 2025
608b7c1
feat: replace AI translations with translated copy
mchappell Sep 26, 2025
cdbc211
fix: use globalThis instead of window object
mchappell Sep 28, 2025
6c3d605
fix: add missing translations
mchappell Oct 6, 2025
0c554e6
fix: remove state flicker from translation preference loading
mchappell Oct 6, 2025
a9d4069
feat: add spanish switch
mchappell Oct 7, 2025
6d2c716
fix: add misspelt english translation for stake certificate
mchappell Oct 7, 2025
28d7d69
feat: add spanish translations
mchappell Oct 8, 2025
054bb63
fix: missing translations
mchappell Oct 8, 2025
7c9677d
Merge branch 'main' into feat/add-language-selection-es
ljagiela Oct 9, 2025
da0b328
Merge branch 'main' into feat/add-language-selection-es
mchappell Oct 9, 2025
8455060
Merge branch 'feat/add-language-selection-es' of https://github.com/i…
mchappell Oct 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,17 @@ import { useTranslation } from 'react-i18next';
import styles from './MainLoader.module.scss';

export interface MainLoaderProps {
text?: string;
overlay?: boolean;
}

export const MainLoader = ({ text, overlay = false }: MainLoaderProps): React.ReactElement => {
export const MainLoader = ({ overlay = false }: MainLoaderProps): React.ReactElement => {
const { t } = useTranslation();

return (
<div className={classNames([styles.loaderContainer, { [styles.overlay]: overlay }])} data-testid="main-loader">
<Loader className={styles.loader} data-testid="main-loader-image" />
<p className={styles.loaderText} data-testid="main-loader-text">
{text ?? t('general.loading')}
{t('general.loading')}
</p>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
Links,
LockWallet,
NetworkChoise,
LanguageChoice,
LanguageInfo,
RenameWalletDrawer,
Separator,
SettingsLink,
Expand Down Expand Up @@ -98,6 +100,11 @@ export const DropdownMenuOverlay: VFC<Props> = ({
sendAnalyticsEvent(PostHogAction.UserWalletProfileNetworkClick);
};

const handleLanguageChoice = () => {
setCurrentSection(Sections.Language);
sendAnalyticsEvent(PostHogAction.UserWalletProfileLanguageClick);
};

const goBackToMainSection = useCallback(() => setCurrentSection(Sections.Main), []);

useEffect(() => {
Expand Down Expand Up @@ -209,6 +216,7 @@ export const DropdownMenuOverlay: VFC<Props> = ({
/>
</div>
)}
<LanguageChoice onClick={handleLanguageChoice} />
<NetworkChoise onClick={handleNetworkChoise} />
{lockWalletButton && (
<>
Expand All @@ -219,6 +227,7 @@ export const DropdownMenuOverlay: VFC<Props> = ({
</div>
)}
{currentSection === Sections.NetworkInfo && <NetworkInfo onBack={goBackToMainSection} />}
{currentSection === Sections.Language && <LanguageInfo onBack={goBackToMainSection} />}
{currentSection === Sections.WalletAccounts && <WalletAccounts onBack={goBackToMainSection} isPopup={isPopup} />}
{isRenamingWallet && (
<RenameWalletDrawer open={isRenamingWallet} popupView={isPopup} onClose={() => setIsRenamingWallet(false)} />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import cn from 'classnames';
import styles from '../DropdownMenuOverlay.module.scss';
import { useBackgroundServiceAPIContext } from '@providers';
import { storage as webStorage } from 'webextension-polyfill';

type LanguageChoiceProps = {
onClick: () => void;
};

export const LanguageChoice = ({ onClick }: LanguageChoiceProps): React.ReactElement => {
const { t } = useTranslation();
const { getBackgroundStorage } = useBackgroundServiceAPIContext();
const [language, setLanguage] = useState<string>('en');
const [loadingLanguage, setLoadingLanguage] = useState(true);

useEffect(() => {
const getLanguage = async () => {
const { languageChoice } = await getBackgroundStorage();
if (languageChoice) setLanguage(languageChoice);
setLoadingLanguage(false);
};
getLanguage();
webStorage.onChanged.addListener(getLanguage);
}, [getBackgroundStorage, setLoadingLanguage]);

return (
<div
data-testid="header-menu-language-choice-container"
className={cn(styles.menuItem, styles.cta)}
onClick={() => onClick()}
>
<div className={styles.networkChoise}>
<span data-testid="header-menu-language-choice-label">{t('browserView.topNavigationBar.links.language')}</span>
{!loadingLanguage && (
<span data-testid="header-menu-language-choice-value" className={styles.value}>
{language || 'en'}
</span>
)}
</div>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/* eslint-disable unicorn/no-null */
import React, { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { NavigationButton, PostHogAction } from '@lace/common';
import styles from './NetworkInfo.module.scss';
import { useBackgroundServiceAPIContext } from '@providers';
import { Radio, RadioChangeEvent } from 'antd';
import { usePostHogClientContext } from '@providers/PostHogClientProvider';
import { storage as webStorage } from 'webextension-polyfill';
import type { Language } from '@lace/translation';

type LanguageChoiceProps = {
onBack: () => void;
};

const LANG_CHOICES = ['en', 'es']; // hardcoding for v1 only

export const LanguageInfo = ({ onBack }: LanguageChoiceProps): React.ReactElement => {
const { t } = useTranslation();
const [language, setLanguage] = useState<Language | null>(null);
const posthog = usePostHogClientContext();

const { getBackgroundStorage, handleChangeLanguage } = useBackgroundServiceAPIContext();

useEffect(() => {
const getLanguage = async () => {
const { languageChoice } = await getBackgroundStorage();
if (languageChoice) setLanguage(languageChoice);
};
webStorage.onChanged.addListener(getLanguage);
getLanguage();
}, [getBackgroundStorage]);

const handleLanguageChangeRequest = useCallback(
(e: RadioChangeEvent) => {
handleChangeLanguage(e.target.value);
posthog.sendEvent(PostHogAction.UserWalletProfileLanguageSelectClick, { language: e.target.value });
},
[handleChangeLanguage, posthog]
);

return (
<div data-testid="user-dropdown-language-info-section" className={styles.container}>
<div className={styles.navigation} data-testid="drawer-navigation">
<NavigationButton iconClassName={styles.iconClassName} icon="arrow" onClick={onBack} />
</div>
<div className={styles.titleSection}>
<div data-testid="user-dropdown-language-title" className={styles.title}>
{t('browserView.settings.wallet.language.title')}
</div>
<div data-testid="user-dropdown-language-description" className={styles.subTitle}>
{t('browserView.settings.wallet.language.drawerDescription')}
</div>
</div>
<div className={styles.content} data-testid="user-dropdown-language-choice">
<Radio.Group
className={styles.radioGroup}
onChange={handleLanguageChangeRequest}
value={language}
data-testid={'language-choice-radio-group'}
>
{LANG_CHOICES.map((choice) => (
<Radio key={`language-choice-${choice}`} value={choice} className={styles.radioLabel}>
{choice}
</Radio>
))}
</Radio.Group>
</div>
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,10 @@
font-size: 14px !important;
}
}

.radioGroup {
color: var(--text-color-primary) !important;
label {
color: var(--text-color-primary) !important;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ export * from './NetworkInfo';
export * from './AddNewWalletLink';
export * from './AddNewBitcoinWalletLink';
export * from './RenameWalletDrawer';
export * from './LanguageInfo';
export * from './LanguageChoice';
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export enum Sections {
Main = 'main',
NetworkInfo = 'network_info',
WalletAccounts = 'wallet_accounts'
WalletAccounts = 'wallet_accounts',
Language = 'language'
}
25 changes: 25 additions & 0 deletions apps/browser-extension-wallet/src/hooks/useAppInit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import { setBackgroundStorage } from '@lib/scripts/background/storage';
import { useCustomSubmitApi } from '@hooks/useCustomSubmitApi';
import { bitcoinWalletManager } from '@lib/wallet-api-ui';
import { useCurrentBlockchain } from '@src/multichain';
import { useBackgroundServiceAPIContext } from '@providers';
import { initI18n } from '@lace/translation';
import { Message, MessageTypes } from '@lib/scripts/types';

export const useAppInit = (): void => {
const {
Expand All @@ -26,6 +29,28 @@ export const useAppInit = (): void => {
const walletState = useWalletState();
const { environmentName, currentChain } = useWalletStore();
const { getCustomSubmitApiForNetwork } = useCustomSubmitApi();
const backgroundServices = useBackgroundServiceAPIContext();

useEffect(() => {
const subscription = backgroundServices.requestMessage$?.subscribe(({ type, data }: Message): void => {
if (type === MessageTypes.CHANGE_LANGUAGE) {
initI18n(data);
backgroundServices.setBackgroundStorage({ languageChoice: data });
}
});

backgroundServices
.getBackgroundStorage()
.then((bs) => {
initI18n(bs.languageChoice ?? globalThis.navigator.language ?? 'en');
})
.catch((error) => {
// eslint-disable-next-line no-console
console.log(error);
});

return () => subscription.unsubscribe();
}, [backgroundServices]);

useEffect(() => {
setWalletState(walletState);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const backgroundServiceProperties: RemoteApiProperties<BackgroundService>
handleOpenNamiBrowser: RemoteApiPropertyType.MethodReturningPromise,
closeAllTabsAndOpenPopup: RemoteApiPropertyType.MethodReturningPromise,
handleChangeTheme: RemoteApiPropertyType.MethodReturningPromise,
handleChangeLanguage: RemoteApiPropertyType.MethodReturningPromise,
handleChangeMode: RemoteApiPropertyType.MethodReturningPromise,
clearBackgroundStorage: RemoteApiPropertyType.MethodReturningPromise,
getBackgroundStorage: RemoteApiPropertyType.MethodReturningPromise,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { POPUP_WINDOW_NAMI_TITLE } from '@utils/constants';
import { catchAndBrandExtensionApiError } from '@utils/catch-and-brand-extension-api-error';
import { initCardanoTokenPrices } from './cardanoTokenPrices';
import { pollController$ } from '../session/poll-controller';
import { Language } from '@lace/translation';

export const requestMessage$ = new Subject<Message>();
export const backendFailures$ = new BehaviorSubject(0);
Expand Down Expand Up @@ -181,6 +182,8 @@ const closeAllTabsAndOpenPopup = async () => {

const handleChangeTheme = (data: ChangeThemeData) => requestMessage$.next({ type: MessageTypes.CHANGE_THEME, data });

const handleChangeLanguage = (data: Language) => requestMessage$.next({ type: MessageTypes.CHANGE_LANGUAGE, data });

const handleChangeMode = (data: ChangeModeData) => requestMessage$.next({ type: MessageTypes.CHANGE_MODE, data });

const { ADA_PRICE_CHECK_INTERVAL, SAVED_PRICE_DURATION } = config();
Expand Down Expand Up @@ -352,6 +355,7 @@ export const exposeBackgroundService = (wallet$: Observable<ActiveWallet>): void
migrationState$,
coinPrices,
handleChangeTheme,
handleChangeLanguage,
handleChangeMode,
clearBackgroundStorage,
getBackgroundStorage,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { themes } from '@providers/ThemeProvider';
import { BackgroundStorage, MigrationState } from './storage';
import { CoinPrices } from './prices';
import type { clearBackgroundStorage } from '../background/storage';
import { Language } from '@lace/translation';

export enum BaseChannels {
BACKGROUND_ACTIONS = 'background-actions'
Expand Down Expand Up @@ -31,7 +32,8 @@ export enum MessageTypes {
HTTP_CONNECTION = 'http-connnection',
WS_CONNECTION = 'ws-connnection',
OPEN_COLLATERAL_SETTINGS = 'open-collateral-settings',
CHANGE_MODE = 'change-mode'
CHANGE_MODE = 'change-mode',
CHANGE_LANGUAGE = 'change-language'
}

export enum BrowserViewSections {
Expand Down Expand Up @@ -84,12 +86,22 @@ interface ChangeMode {
type: MessageTypes.CHANGE_MODE;
data: ChangeModeData;
}
interface ChangeThemeMessage {
type: MessageTypes.CHANGE_THEME;
data: ChangeThemeData;
}

interface ChangeLanguageMessage {
type: MessageTypes.CHANGE_LANGUAGE;
data: Language;
}
export type Message =
| ChangeThemeMessage
| HTTPConnectionMessage
| WSConnectionMessage
| OpenBrowserMessage
| ChangeMode;
| ChangeMode
| ChangeLanguageMessage;

export type UnhandledError = {
type: 'error' | 'unhandledrejection';
Expand All @@ -104,6 +116,7 @@ export type BackgroundService = {
migrationState$: BehaviorSubject<MigrationState | undefined>;
coinPrices: CoinPrices;
handleChangeTheme: (data: ChangeThemeData) => void;
handleChangeLanguage: (data: Language) => void;
handleChangeMode: (data: ChangeModeData) => void;
setBackgroundStorage: (data: BackgroundStorage) => Promise<void>;
getBackgroundStorage: () => Promise<BackgroundStorage>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
RawFeatureFlagPayloads
} from '@lib/scripts/types/feature-flags';
import { LogLevelString } from '@lace/common';
import { Language } from '@lace/translation';

export interface PendingMigrationState {
from: string;
Expand Down Expand Up @@ -49,6 +50,7 @@ export interface BackgroundStorage {
dappInjectCompatibilityMode?: boolean;
optedInBeta?: boolean;
logLevel?: LogLevelString;
languageChoice?: Language;
}

export type BackgroundStorageKeys = keyof BackgroundStorage;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ export const Activity = (): React.ReactElement => {
id: transaction.transactionHash,
formattedTimestamp:
transaction.status === Bitcoin.TransactionStatus.Pending
? 'PENDING'
? t('browserView.activity.entry.name.pending')
: formattedTimestamp(new Date(transaction.timestamp * 1000)),
amount: `${new BigNumber(net.toString()).dividedBy(100_000_000).toFixed(8, BigNumber.ROUND_HALF_UP)} BTC`,
fiatAmount: `${computeBalance(Number(net) / SATS_IN_BTC, fiatCurrency.code, bitcoinPrice)} ${
Expand All @@ -194,7 +194,7 @@ export const Activity = (): React.ReactElement => {
items
};
});
}, [addresses, recentTransactions, bitcoinPrice, explorerBaseUrl, pendingTransaction, fiatCurrency]);
}, [addresses, recentTransactions, bitcoinPrice, explorerBaseUrl, pendingTransaction, fiatCurrency, t]);

const isLoading =
addresses.length === 0 || explorerBaseUrl.length === 0 || currentCursor === null || !activityFetched;
Expand Down
Loading
Loading