Skip to content

Commit

Permalink
refactor(localization): Consolidate mappings to one config object (#1350
Browse files Browse the repository at this point in the history
)
  • Loading branch information
jaredvu authored Dec 3, 2024
1 parent 0167e76 commit e6b0bd8
Show file tree
Hide file tree
Showing 10 changed files with 124 additions and 67 deletions.
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ VITE_METADATA_SERVICE_URI=

VITE_FUNKIT_API_KEY=

# Restricted Locales that should not be supported on app deployment. ex: VITE_APP_RESTRICTED_LOCALES=zh-CN,ru,en
VITE_APP_RESTRICTED_LOCALES=

# URL for the qrcode that is generated within the modal share pnl analytics
VITE_SHARE_PNL_ANALYTICS_URL=

Expand Down
104 changes: 74 additions & 30 deletions src/constants/localization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
LOCALE_DATA,
NOTIFICATIONS,
NOTIFICATIONS_STRING_KEYS,
SupportedLocale,
TOOLTIPS,
WARNINGS_STRING_KEYS,
} from '@dydxprotocol/v4-localization';
Expand All @@ -15,6 +14,7 @@ import { StatsigConfigType } from '@/constants/statsig';
import { type LinksConfigs } from '@/hooks/useURLConfigs';

import formatString from '@/lib/formatString';
import { objectFromEntries } from '@/lib/objectHelpers';

export { TOOLTIP_STRING_KEYS } from '@dydxprotocol/v4-localization';

Expand Down Expand Up @@ -74,41 +74,85 @@ export type StringGetterFunction = <T extends StringGetterParams>(
? string
: ReturnType<typeof formatString>;

export const SUPPORTED_LOCALE_STRING_LABELS: { [key in SupportedLocales]: string } = {
[SupportedLocales.EN]: 'English',
[SupportedLocales.ZH_CN]: '中文',
[SupportedLocales.JA]: '日本語',
[SupportedLocales.KO]: '한국어',
[SupportedLocales.RU]: 'русский',
[SupportedLocales.TR]: 'Türkçe',
[SupportedLocales.FR]: 'Français',
[SupportedLocales.PT]: 'Português',
[SupportedLocales.ES]: 'Español',
[SupportedLocales.DE]: 'Deutsch',
};

export const SUPPORTED_LOCALE_BASE_TAGS = {
[SupportedLocales.EN]: 'en',
[SupportedLocales.ZH_CN]: 'zh',
[SupportedLocales.JA]: 'ja',
[SupportedLocales.KO]: 'ko',
[SupportedLocales.RU]: 'ru',
[SupportedLocales.TR]: 'tr',
[SupportedLocales.FR]: 'fr',
[SupportedLocales.PT]: 'pt',
[SupportedLocales.ES]: 'es',
[SupportedLocales.DE]: 'de',
};

export const EU_LOCALES: SupportedLocale[] = [
export const EU_LOCALES: SupportedLocales[] = [
SupportedLocales.DE,
SupportedLocales.PT,
SupportedLocales.ES,
SupportedLocales.FR,
];

export const SUPPORTED_BASE_TAGS_LOCALE_MAPPING = Object.fromEntries(
Object.entries(SUPPORTED_LOCALE_BASE_TAGS).map(([locale, baseTag]) => [baseTag, locale])
const DEPLOYER_RESTRICTED_LOCALES: SupportedLocales[] = import.meta.env.VITE_APP_RESTRICTED_LOCALES
? import.meta.env.VITE_APP_RESTRICTED_LOCALES?.split(',')
: [];

export const SUPPORTED_LOCALES = [
{
locale: SupportedLocales.EN,
baseTag: 'en',
label: 'English',
browserLanguage: 'en-US',
},
{
locale: SupportedLocales.ZH_CN,
baseTag: 'zh',
label: '中文',
browserLanguage: 'zh-CN',
},
{
locale: SupportedLocales.JA,
baseTag: 'ja',
label: '日本語',
browserLanguage: 'ja-JP',
},
{
locale: SupportedLocales.KO,
baseTag: 'ko',
label: '한국어',
browserLanguage: 'ko-KR',
},
{
locale: SupportedLocales.RU,
baseTag: 'ru',
label: 'русский',
browserLanguage: 'ru-RU',
},
{
locale: SupportedLocales.TR,
baseTag: 'tr',
label: 'Türkçe',
browserLanguage: 'tr-TR',
},
{
locale: SupportedLocales.FR,
baseTag: 'fr',
label: 'Français',
browserLanguage: 'fr-FR',
},
{
locale: SupportedLocales.PT,
baseTag: 'pt',
label: 'Português',
browserLanguage: 'pt-PT',
},
{
locale: SupportedLocales.ES,
baseTag: 'es',
label: 'Español',
browserLanguage: 'es-ES',
},
{
locale: SupportedLocales.DE,
baseTag: 'de',
label: 'Deutsch',
browserLanguage: 'de-DE',
},
].filter(({ locale }) =>
DEPLOYER_RESTRICTED_LOCALES.length ? !DEPLOYER_RESTRICTED_LOCALES.includes(locale) : true
);

// Map with locale as key and locale object as value
export const SUPPORTED_LOCALE_MAP = objectFromEntries(
SUPPORTED_LOCALES.map((locale) => [locale.locale, locale])
);

export type TooltipStrings = {
Expand Down
5 changes: 3 additions & 2 deletions src/hooks/tradingView/useTradingView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {

import { DEFAULT_RESOLUTION } from '@/constants/candles';
import { TOGGLE_ACTIVE_CLASS_NAME } from '@/constants/charts';
import { STRING_KEYS, SUPPORTED_LOCALE_BASE_TAGS } from '@/constants/localization';
import { STRING_KEYS, SUPPORTED_LOCALE_MAP } from '@/constants/localization';
import { StatsigFlags } from '@/constants/statsig';
import { tooltipStrings } from '@/constants/tooltips';
import type { TvWidget } from '@/constants/tvchart';
Expand Down Expand Up @@ -146,6 +146,7 @@ export const useTradingView = ({
if (marketId && tickSizeDecimals !== undefined && !tvWidget) {
const widgetOptions = getWidgetOptions();
const widgetOverrides = getWidgetOverrides({ appTheme, appColorMode });
const languageCode = SUPPORTED_LOCALE_MAP[selectedLocale].baseTag;

const initialPriceScale = BigNumber(10).exponentiatedBy(tickSizeDecimals).toNumber();
const options: TradingTerminalWidgetOptions = {
Expand All @@ -161,7 +162,7 @@ export const useTradingView = ({
stringGetter
),
interval: (savedResolution ?? DEFAULT_RESOLUTION) as ResolutionString,
locale: SUPPORTED_LOCALE_BASE_TAGS[selectedLocale] as LanguageCode,
locale: languageCode as LanguageCode,
symbol: marketId,
saved_data: !isEmpty(savedTvChartConfig) ? savedTvChartConfig : undefined,
auto_save_delay: 1,
Expand Down
5 changes: 3 additions & 2 deletions src/hooks/tradingView/useTradingViewLaunchable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
import { useDispatch } from 'react-redux';

import { DEFAULT_RESOLUTION } from '@/constants/candles';
import { SUPPORTED_LOCALE_BASE_TAGS } from '@/constants/localization';
import { SUPPORTED_LOCALE_MAP } from '@/constants/localization';
import type { TvWidget } from '@/constants/tvchart';

import { useAppSelector } from '@/state/appTypes';
Expand Down Expand Up @@ -52,13 +52,14 @@ export const useTradingViewLaunchable = ({
if (marketId && !isDataLoading && !tvWidget) {
const widgetOptions = getWidgetOptions(true);
const widgetOverrides = getWidgetOverrides({ appTheme, appColorMode });
const languageCode = SUPPORTED_LOCALE_MAP[selectedLocale].baseTag;

const options: TradingTerminalWidgetOptions = {
...widgetOptions,
...widgetOverrides,
datafeed: getLaunchableMarketDatafeed(metadataServiceData),
interval: (savedResolution ?? DEFAULT_RESOLUTION) as ResolutionString,
locale: SUPPORTED_LOCALE_BASE_TAGS[selectedLocale] as LanguageCode,
locale: languageCode as LanguageCode,
symbol: marketId,
saved_data: !isEmpty(savedTvChartConfig) ? savedTvChartConfig : undefined,
auto_save_delay: 1,
Expand Down
16 changes: 7 additions & 9 deletions src/hooks/useLocaleSeparators.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createContext, useContext, useEffect, useMemo, useState } from 'react';

import { SUPPORTED_BASE_TAGS_LOCALE_MAPPING } from '@/constants/localization';
import { SUPPORTED_LOCALES } from '@/constants/localization';

import { useAppSelector } from '@/state/appTypes';
import { getSelectedLocale } from '@/state/localizationSelectors';
Expand All @@ -27,14 +27,12 @@ const useLocaleContext = () => {
}, []);

useEffect(() => {
if (selectedLocale) {
const updatedBrowserLanguage = Object.entries(SUPPORTED_BASE_TAGS_LOCALE_MAPPING).find(
([, value]) => value === selectedLocale
);

if (updatedBrowserLanguage) {
setBrowserLanguage(selectedLocale);
}
const updatedBrowserLanguage = SUPPORTED_LOCALES.find(
({ locale }) => locale === selectedLocale
)?.browserLanguage;

if (updatedBrowserLanguage) {
setBrowserLanguage(updatedBrowserLanguage);
}
}, [selectedLocale]);

Expand Down
18 changes: 11 additions & 7 deletions src/lib/consistentAssetSize.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
import { mapValues, range, zipObject } from 'lodash';

import { SUPPORTED_LOCALE_STRING_LABELS, SupportedLocales } from '@/constants/localization';
import { SUPPORTED_LOCALES, SupportedLocales } from '@/constants/localization';

import { formatNumberOutput, OutputType } from '@/components/Output';

// for each locale, an array of the correct compact number suffix to use for 10^{index}
// e.g. for "en" we have ['', '', '', 'k', 'k', 'k', 'm', 'm', 'm', 'b', 'b', 'b', 't', 't', 't']
const supportedLocaleToCompactSuffixByPowerOfTen = mapValues(
SUPPORTED_LOCALE_STRING_LABELS,
(name, lang) =>
const supportedLocaleToCompactSuffixByPowerOfTen = Object.fromEntries(
SUPPORTED_LOCALES.map(({ locale }) => [
locale,
range(15)
.map((a) =>
Intl.NumberFormat(lang, {
Intl.NumberFormat(locale, {
style: 'decimal',
notation: 'compact',
maximumSignificantDigits: 6,
}).format(Math.abs(10 ** a))
)
// first capture group grabs all the numbers with normal separator, then we grab any groups of whitespace+numbers
// this is so we know which languages keep whitespace before the suffix
.map((b) => b.replace(/(^[\d,.]+){1}(\s\d+)*/, ''))
.map((b) => b.replace(/(^[\d,.]+){1}(\s\d+)*/, '')),
])
);

const zipObjectFn = <T extends string, K>(arr: T[], valueGenerator: (val: T) => K) =>
Expand Down Expand Up @@ -56,7 +57,10 @@ export const getConsistentAssetSizeString = (
return { displayDivisor: 1, displaySuffix: '' };
}
const unitToUse =
supportedLocaleToCompactSuffixByPowerOfTen[selectedLocale][Math.floor(Math.log10(stepSize))];
supportedLocaleToCompactSuffixByPowerOfTen[selectedLocale]?.[
Math.floor(Math.log10(stepSize))
];

if (unitToUse == null) {
return { displayDivisor: 1, displaySuffix: '' };
}
Expand Down
7 changes: 7 additions & 0 deletions src/lib/objectHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@
export const objectEntries = <T extends object>(t: T) =>
Object.entries(t) as { [K in keyof T]: [K, T[K]] }[keyof T][];

/** Object.fromEntries() with key types preserved */
export const objectFromEntries = <const T extends ReadonlyArray<readonly [PropertyKey, unknown]>>(
entries: T
): { [K in T[number] as K[0]]: K[1] } => {
return Object.fromEntries(entries) as { [K in T[number] as K[0]]: K[1] };
};

// Object.keys() with key types preserved - NOT SAFE for mutable variables, only readonly/consts that are never modified
// since typescript is structurally typed and objects can contain extra keys and still be valid objects of type T
export const objectKeys = <T extends object>(t: T) => Object.keys(t) as Array<keyof T>;
Expand Down
12 changes: 4 additions & 8 deletions src/pages/settings/Settings.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { Navigate, Route, Routes, useLocation } from 'react-router-dom';

import {
STRING_KEYS,
SUPPORTED_LOCALE_STRING_LABELS,
SupportedLocales,
} from '@/constants/localization';
import { STRING_KEYS, SUPPORTED_LOCALES, SupportedLocales } from '@/constants/localization';
import type { MenuItem } from '@/constants/menus';
import { DydxNetwork } from '@/constants/networks';
import { AppRoute, MobileSettingsRoute } from '@/constants/routes';
Expand Down Expand Up @@ -41,7 +37,7 @@ const SettingsPage = () => {
type: PageMenuItemType.Navigation,
href: `${AppRoute.Settings}/${MobileSettingsRoute.Language}`,
label: stringGetter({ key: STRING_KEYS.LANGUAGE }),
labelRight: SUPPORTED_LOCALE_STRING_LABELS[selectedLocale],
labelRight: SUPPORTED_LOCALES.find(({ locale }) => locale === selectedLocale)?.label,
},
{
value: 'network-nav-item',
Expand Down Expand Up @@ -75,9 +71,9 @@ const SettingsPage = () => {
onSelect: (locale: string) => {
dispatch(setSelectedLocale({ locale: locale as SupportedLocales }));
},
subitems: Object.values(SupportedLocales).map((locale) => ({
subitems: SUPPORTED_LOCALES.map(({ locale, label }) => ({
value: locale as string,
label: SUPPORTED_LOCALE_STRING_LABELS[locale],
label,
})),
};

Expand Down
15 changes: 9 additions & 6 deletions src/state/localizationMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import type { PayloadAction } from '@reduxjs/toolkit';
import { LocalStorageKey } from '@/constants/localStorage';
import {
EU_LOCALES,
SUPPORTED_BASE_TAGS_LOCALE_MAPPING,
SUPPORTED_LOCALE_MAP,
SUPPORTED_LOCALES,
SupportedLocales,
type LocaleData,
} from '@/constants/localization';
Expand All @@ -19,6 +20,7 @@ import { setLocaleData, setLocaleLoaded, setSelectedLocale } from '@/state/local

import { getBrowserLanguage } from '@/lib/language';
import { getLocalStorage, setLocalStorage } from '@/lib/localStorage';
import { objectKeys } from '@/lib/objectHelpers';

const getNewLocaleData = ({
store,
Expand Down Expand Up @@ -50,18 +52,19 @@ export default (store: any) => (next: any) => async (action: PayloadAction<any>)

switch (type) {
case initializeLocalization().type: {
const localStorageLocale = getLocalStorage({
const localStorageLocale = getLocalStorage<SupportedLocales | undefined>({
key: LocalStorageKey.SelectedLocale,
}) as SupportedLocales;
});

if (localStorageLocale) {
if (localStorageLocale && objectKeys(SUPPORTED_LOCALE_MAP).includes(localStorageLocale)) {
store.dispatch(setSelectedLocale({ locale: localStorageLocale }));
} else {
const browserLanguage = getBrowserLanguage();
const browserLanguageBaseTag = browserLanguage.split('-')[0]!.toLowerCase();

let locale = (SUPPORTED_BASE_TAGS_LOCALE_MAPPING[browserLanguageBaseTag] ??
SupportedLocales.EN) as SupportedLocales;
let locale =
SUPPORTED_LOCALES.find(({ baseTag }) => baseTag === browserLanguageBaseTag)?.locale ??
SupportedLocales.EN;

// regulatory: do not default to browser language if it's an EU language, default to English instead
if (EU_LOCALES.includes(locale)) {
Expand Down
6 changes: 3 additions & 3 deletions src/views/menus/LanguageSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SUPPORTED_LOCALE_STRING_LABELS, SupportedLocales } from '@/constants/localization';
import { SUPPORTED_LOCALES, SupportedLocales } from '@/constants/localization';

import { DropdownSelectMenu } from '@/components/DropdownSelectMenu';

Expand All @@ -16,9 +16,9 @@ type StyleProps = {
className?: string;
};

const localizationItems = Object.values(SupportedLocales).map((locale) => ({
const localizationItems = SUPPORTED_LOCALES.map(({ locale, label }) => ({
value: locale,
label: SUPPORTED_LOCALE_STRING_LABELS[locale],
label,
}));

export const LanguageSelector = ({
Expand Down

0 comments on commit e6b0bd8

Please sign in to comment.