From 47a3c5f8bc9ee79bdd30bc7445dff5dc461d9832 Mon Sep 17 00:00:00 2001 From: smgv Date: Wed, 18 Dec 2024 01:00:52 +0530 Subject: [PATCH] i18n implemented --- .vscode/settings.json | 2 +- .../modal-ui/src/components/Body/Body.tsx | 15 +-- .../src/components/Body/ConnectWallet.tsx | 18 +-- .../modal-ui/src/components/Body/Login.tsx | 25 ++-- .../modal-ui/src/components/Footer/Footer.tsx | 3 +- .../modal-ui/src/components/Loader/Loader.tsx | 9 +- .../SocialLoginList/SocialLoginList.tsx | 10 +- .../components/WalletButton/WalletButton.tsx | 6 +- packages/modal-ui/src/localeImport.ts | 113 +++++++++++------- packages/modal-ui/src/loginModal.tsx | 103 +--------------- 10 files changed, 117 insertions(+), 187 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index fbf5c6634..0246c035d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -13,5 +13,5 @@ "changeProcessCWD": true } ], - "cSpell.words": ["bowser", "clsx", "JRPC", "PASSWORDLESS", "Ronin", "Solana", "SQAURE", "WEBAUTHN"] + "cSpell.words": ["bowser", "clsx", "dont", "JRPC", "PASSWORDLESS", "Ronin", "Solana", "SQAURE", "WEBAUTHN"] } diff --git a/packages/modal-ui/src/components/Body/Body.tsx b/packages/modal-ui/src/components/Body/Body.tsx index 3fc86277b..85d5002a4 100644 --- a/packages/modal-ui/src/components/Body/Body.tsx +++ b/packages/modal-ui/src/components/Body/Body.tsx @@ -1,7 +1,7 @@ import { type SafeEventEmitter } from "@web3auth/auth"; import { ChainNamespaceType, WalletRegistry } from "@web3auth/base/src"; import Bowser from "bowser"; -import { createContext, createEffect, createMemo, Show } from "solid-js"; +import { createContext, createMemo, Show } from "solid-js"; import { createStore } from "solid-js/store"; import { PAGES } from "../../constants"; @@ -17,6 +17,7 @@ import { SocialLoginsConfig, StateEmitterEvents, } from "../../interfaces"; +import { t } from "../../localeImport"; import { getBrowserExtensionUrl, getBrowserName, getMobileInstallLink, getOsName } from "../../utils/common"; import Footer from "../Footer/Footer"; import { Image } from "../Image"; @@ -68,10 +69,10 @@ const Body = (props: BodyProps) => { walletDetails: null, }); - createEffect(() => { - // eslint-disable-next-line no-console - console.log(props.socialLoginsConfig, "socialLoginsConfig", props.modalState, "modalState"); - }); + // createEffect(() => { + // // eslint-disable-next-line no-console + // console.log(props.socialLoginsConfig, "socialLoginsConfig", props.modalState, "modalState"); + // }); const handleExternalWalletBtnClick = (flag: boolean) => { props.setModalState({ @@ -122,7 +123,7 @@ const Body = (props: BodyProps) => { width="28" isButton /> - {`modal.external.install-mobile-app, ${getOsName(os as mobileOs)}`} + {t("modal.external.install-mobile-app", { os: getOsName(os as mobileOs) })} @@ -155,7 +156,7 @@ const Body = (props: BodyProps) => { isButton /> - {`modal.external.install-browser-extension, ${getBrowserName(deviceDetails().browser)}`} + {t("modal.external.install-browser-extension", { browser: getBrowserName(deviceDetails().browser) })} diff --git a/packages/modal-ui/src/components/Body/ConnectWallet.tsx b/packages/modal-ui/src/components/Body/ConnectWallet.tsx index f2882a32a..ede9c695f 100644 --- a/packages/modal-ui/src/components/Body/ConnectWallet.tsx +++ b/packages/modal-ui/src/components/Body/ConnectWallet.tsx @@ -5,6 +5,7 @@ import { MaskType, QRCodeCanvas } from "solid-qr-code"; import { CONNECT_WALLET_PAGES } from "../../constants"; import { browser, ExternalButton, ModalStatusType, os, platform } from "../../interfaces"; +import { t } from "../../localeImport"; import { WalletButton } from "../WalletButton"; import { BodyContext } from "./Body"; export interface ConnectWalletProps { @@ -28,9 +29,6 @@ const ConnectWallet = (props: ConnectWalletProps) => { const [selectedButton, setSelectedButton] = createSignal(null); const [walletSearch, setWalletSearch] = createSignal(""); - // eslint-disable-next-line no-console, solid/reactivity - console.log(selectedButton(), "SELECTED BUTTON"); - const handleBack = () => { if (!selectedWallet() && currentPage() === CONNECT_WALLET_PAGES.CONNECT_WALLET && props.onBackClick) { props.onBackClick(false); @@ -249,9 +247,9 @@ const ConnectWallet = (props: ConnectWalletProps) => { e.target.placeholder = ""; }} onBlur={(e) => { - e.target.placeholder = `modal.external.search-wallet, ${totalExternalWallets()}`; + e.target.placeholder = t("modal.external.search-wallet", { count: `${totalExternalWallets()}` }); }} - placeholder={`Search through wallets... ${totalExternalWallets()}`} + placeholder={t("modal.external.search-wallet", { count: `${totalExternalWallets()}` })} class="w3a--appearance-none w3a--px-4 w3a--py-2.5 w3a--border w3a--text-app-gray-900 w3a--border-app-gray-300 w3a--bg-app-gray-50 dark:w3a--bg-app-gray-700 dark:w3a--border-app-gray-600 dark:w3a--text-app-white placeholder:w3a--text-app-gray-500 dark:placeholder:w3a--text-app-gray-400 placeholder:w3a--text-sm placeholder:w3a--font-normal w3a--rounded-full w3a--outline-none focus:w3a--outline-none active:w3a--outline-none" /> @@ -260,7 +258,7 @@ const ConnectWallet = (props: ConnectWalletProps) => { when={externalButtons().length !== 0} fallback={
- {"modal.external.no-wallets-found"} + {t("modal.external.no-wallets-found")}
} > @@ -298,13 +296,15 @@ const ConnectWallet = (props: ConnectWalletProps) => { />

- Scan with a WalletConnect-supported wallet or click the QR code to copy to your clipboard. + {t("modal.external.walletconnect-copy")}

-

Don't have Trust Wallet?

+

+ {t("modal.external.dont-have")} {selectedButton()?.displayName}? +

diff --git a/packages/modal-ui/src/components/Body/Login.tsx b/packages/modal-ui/src/components/Body/Login.tsx index e0ff561d3..b387a4543 100644 --- a/packages/modal-ui/src/components/Body/Login.tsx +++ b/packages/modal-ui/src/components/Body/Login.tsx @@ -2,6 +2,7 @@ import { LOGIN_PROVIDER } from "@web3auth/auth"; import { createEffect, createMemo, createSignal, mergeProps, Show } from "solid-js"; import { DEFAULT_LOGO_DARK, DEFAULT_LOGO_LIGHT, SocialLoginsConfig } from "../../interfaces"; +import { t } from "../../localeImport"; import { cn } from "../../utils/common"; import { validatePhoneNumber } from "../../utils/modal"; import SocialLoginList from "../SocialLoginList"; @@ -72,9 +73,9 @@ const Login = (props: LoginProps) => { }; const title = createMemo(() => { - if (mergedProps.isEmailPasswordLessLoginVisible && mergedProps.isSmsPasswordLessLoginVisible) return "modal.social.passwordless-title"; - if (mergedProps.isEmailPasswordLessLoginVisible) return "modal.social.email"; - return "modal.social.phone"; + if (mergedProps.isEmailPasswordLessLoginVisible && mergedProps.isSmsPasswordLessLoginVisible) return t("modal.social.passwordless-title"); + if (mergedProps.isEmailPasswordLessLoginVisible) return t("modal.social.email"); + return t("modal.social.phone"); }); const placeholder = createMemo(() => { @@ -84,9 +85,9 @@ const Login = (props: LoginProps) => { }); const invalidInputErrorMessage = createMemo(() => { - if (mergedProps.isEmailPasswordLessLoginVisible && mergedProps.isSmsPasswordLessLoginVisible) return "modal.errors-invalid-number-email"; - if (mergedProps.isEmailPasswordLessLoginVisible) return "modal.errors-invalid-email"; - return "modal.errors-invalid-number"; + if (mergedProps.isEmailPasswordLessLoginVisible && mergedProps.isSmsPasswordLessLoginVisible) return t("modal.errors-invalid-number-email"); + if (mergedProps.isEmailPasswordLessLoginVisible) return t("modal.errors-invalid-email"); + return t("modal.errors-invalid-number"); }); const handleConnectWallet = (e: MouseEvent) => { @@ -97,7 +98,7 @@ const Login = (props: LoginProps) => { const headerLogo = createMemo(() => ([DEFAULT_LOGO_DARK, DEFAULT_LOGO_LIGHT].includes(mergedProps.appLogo) ? "" : mergedProps.appLogo)); const subtitle = createMemo(() => { - return `modal.header-subtitle-name, ${mergedProps.appName}`; + return t("modal.header-subtitle-name", { appName: mergedProps.appName }); }); return ( @@ -108,7 +109,7 @@ const Login = (props: LoginProps) => { Logo -

{"modal.header-title"}

+

{t("modal.header-title")}

{subtitle()}

@@ -136,7 +137,7 @@ const Login = (props: LoginProps) => { /> -
+
{invalidInputErrorMessage()}
@@ -147,13 +148,13 @@ const Login = (props: LoginProps) => { })} onClick={handleFormSubmit} > - {"modal.social.passwordless-cta"} + {t("modal.social.passwordless-cta")}
diff --git a/packages/modal-ui/src/components/Footer/Footer.tsx b/packages/modal-ui/src/components/Footer/Footer.tsx index fa36e0858..32fde8c6f 100644 --- a/packages/modal-ui/src/components/Footer/Footer.tsx +++ b/packages/modal-ui/src/components/Footer/Footer.tsx @@ -1,7 +1,8 @@ +import { t } from "../../localeImport"; const Footer = () => { return (
-
{"modal.footer.message-new"}
+
{t("modal.footer.message-new")}
{ {providerIcon()}
-
{`modal.adapter-loader.message1 ${props.adapterName}`}
-
{`modal.adapter-loader.message2 ${props.adapterName}`}
+
+ {t("modal.adapter-loader.message1", { adapter: props.adapterName })} +
+
+ {t("modal.adapter-loader.message2", { adapter: props.adapterName })} +
diff --git a/packages/modal-ui/src/components/SocialLoginList/SocialLoginList.tsx b/packages/modal-ui/src/components/SocialLoginList/SocialLoginList.tsx index fb4af959a..4bf7e8b22 100644 --- a/packages/modal-ui/src/components/SocialLoginList/SocialLoginList.tsx +++ b/packages/modal-ui/src/components/SocialLoginList/SocialLoginList.tsx @@ -4,6 +4,7 @@ import { createEffect, createSignal, For, Show, useContext } from "solid-js"; import { capitalizeFirstLetter } from "../../config"; import { ThemedContext } from "../../context/ThemeContext"; import { SocialLoginsConfig } from "../../interfaces"; +import { t } from "../../localeImport"; import { cn } from "../../utils/common"; import { SocialLoginButton } from "../SocialLoginButton"; @@ -50,9 +51,6 @@ const SocialLoginList = (props: SocialLoginListProps) => { return props.socialLoginsConfig.loginMethods[loginMethodKey].showOnModal; }); - // eslint-disable-next-line no-console - console.log(props.socialLoginsConfig.loginMethods, "loginMethods"); - const visibleRows: rowType[] = []; const otherRows: rowType[] = []; @@ -128,7 +126,7 @@ const SocialLoginList = (props: SocialLoginListProps) => { } showText={true} showIcon={true} - text={`modal.social.continueCustom ${mainOption().name}`} + text={t("modal.social.continueCustom", { adapter: mainOption().name })} /> @@ -186,7 +184,7 @@ const SocialLoginList = (props: SocialLoginListProps) => { -

{"modal.social.policy"}

+

{t("modal.social.policy")}

diff --git a/packages/modal-ui/src/components/WalletButton/WalletButton.tsx b/packages/modal-ui/src/components/WalletButton/WalletButton.tsx index 20dae49c7..b14e287c1 100644 --- a/packages/modal-ui/src/components/WalletButton/WalletButton.tsx +++ b/packages/modal-ui/src/components/WalletButton/WalletButton.tsx @@ -1,6 +1,7 @@ import { createMemo, Show } from "solid-js"; import { ExternalButton } from "../../interfaces"; +import { t } from "../../localeImport"; import { Image } from "../Image"; type os = "iOS" | "Android"; @@ -38,9 +39,6 @@ const WalletButton = (props: WalletButtonProps) => { } }; - // eslint-disable-next-line no-console, solid/reactivity - console.log(props.button); - return ( diff --git a/packages/modal-ui/src/localeImport.ts b/packages/modal-ui/src/localeImport.ts index 87239e63e..81d05bbb6 100644 --- a/packages/modal-ui/src/localeImport.ts +++ b/packages/modal-ui/src/localeImport.ts @@ -1,49 +1,74 @@ -// import i18n from "i18next"; -// import { initReactI18next } from "react-i18next"; - -// import { en } from "./i18n"; - -// const i18nInstance = i18n.createInstance() as typeof i18n; -// i18nInstance.use(initReactI18next).init({ -// resources: { -// en: { translation: en }, -// }, -// lng: "en", -// fallbackLng: "en", -// interpolation: { escapeValue: false }, -// debug: false, -// react: { -// useSuspense: true, -// }, -// }); - -// export default i18nInstance; - -import * as i18n from "@solid-primitives/i18n"; - -import { en } from "./i18n"; - -export type RawDictionary = typeof en; -export type Dictionary = i18n.Flatten; - -const localeMap = { - en: "english", - fr: "french", - es: "spanish", - de: "german", - ja: "japanese", - ko: "korean", - zh: "mandarin", - pt: "portuguese", - tr: "turkish", - nl: "dutch", +import { createEffect, createSignal } from "solid-js"; + +// Update type definitions +type TranslationType = Record>; +const languageMap: Record Promise<{ default: TranslationType }>> = { + en: () => import("./i18n/english.json"), + fr: () => import("./i18n/french.json"), + de: () => import("./i18n/german.json"), + es: () => import("./i18n/spanish.json"), + pt: () => import("./i18n/portuguese.json"), + tr: () => import("./i18n/turkish.json"), + zh: () => import("./i18n/mandarin.json"), + ko: () => import("./i18n/korean.json"), + nl: () => import("./i18n/dutch.json"), + ja: () => import("./i18n/japanese.json"), }; -export type Locale = "en" | "fr" | "es" | "de" | "ja" | "ko" | "zh" | "pt" | "tr" | "nl"; +// Function to get the locale from localStorage or default to 'en' +const storedLocale = localStorage.getItem("app-locale") || "en"; + +// Signals to manage locale and translations +const [locale, setLocale] = createSignal(storedLocale); +const [translations, setTranslations] = createSignal({}); + +// Function to load JSON dynamically and set translations +async function loadTranslations(newLocale: string) { + const loader = languageMap[newLocale]; + if (!loader) { + // eslint-disable-next-line no-console + console.error(`Locale '${newLocale}' is not supported.`); + return; + } -async function fetchDictionary(locale: Locale): Promise { - const { default: dict } = await import(`./i18n/${localeMap[locale]}.json`); - return i18n.flatten(dict) as Dictionary; // flatten the dictionary to make all nested keys available top-level + const { default: data } = await loader(); + setTranslations(data as TranslationType); } -export default fetchDictionary; +// Function to change locale and persist it to localStorage +async function changeLocale(newLocale: string) { + await loadTranslations(newLocale); + setLocale(newLocale); + localStorage.setItem("app-locale", newLocale); // Persist the new locale +} + +// Recursive t function to handle nested keys and placeholders +function t(key: string, placeholders: Record = {}): string { + const keys = key.split("."); + let translation: unknown = translations(); + + // Traverse the nested keys + for (const k of keys) { + if (translation && typeof translation === "object") { + translation = (translation as Record)[k]; + } else if (!translation && translations()[keys[0]][key.split(".").slice(1).join(".")]) { + translation = translations()[keys[0]][key.split(".").slice(1).join(".")]; + } else { + return key; + } + } + + // Handle placeholders if it's a string + if (typeof translation === "string") { + return Object.entries(placeholders).reduce((str, [placeholder, value]) => str.replace(new RegExp(`{{${placeholder}}}`, "g"), value), translation); + } + + return key; +} + +// Replace direct call with createEffect +createEffect(() => { + loadTranslations(locale()); +}); + +export { changeLocale, locale, t }; diff --git a/packages/modal-ui/src/loginModal.tsx b/packages/modal-ui/src/loginModal.tsx index 53a396fd3..5fc470ece 100644 --- a/packages/modal-ui/src/loginModal.tsx +++ b/packages/modal-ui/src/loginModal.tsx @@ -1,6 +1,5 @@ import "./index.css"; -// import * as i18n from "@solid-primitives/i18n"; import { applyWhiteLabelTheme, SafeEventEmitter } from "@web3auth/auth"; import { ADAPTER_EVENTS, @@ -17,12 +16,10 @@ import { Web3AuthError, Web3AuthNoModalEvents, } from "@web3auth/base"; -// import { createResource } from "solid-js"; import { render } from "solid-js/web"; import { LoginModal as Modal } from "./components/LoginModal"; import { ThemedContext } from "./context/ThemeContext"; -// import { en } from "./i18n"; import { DEFAULT_LOGO_DARK, DEFAULT_LOGO_LIGHT, @@ -35,7 +32,7 @@ import { StateEmitterEvents, UIConfig, } from "./interfaces"; -// import fetchDictionary from "./localeImport"; +import { changeLocale } from "./localeImport"; // import i18n from "./localeImport"; import { getUserLanguage } from "./utils/modal"; @@ -80,110 +77,14 @@ export class LoginModal extends SafeEventEmitter { } get isDark(): boolean { - // eslint-disable-next-line no-console - console.log(this.uiConfig, "this.uiConfig"); return this.uiConfig.mode === "dark" || (this.uiConfig.mode === "auto" && window.matchMedia("(prefers-color-scheme: dark)").matches); } initModal = async (): Promise => { - // eslint-disable-next-line no-console - console.log(this.isDark, "this.isDark"); const darkState = { isDark: this.isDark }; - // const useLang = this.uiConfig.defaultLanguage || LANGUAGES.en; - - // const [dict] = createResource(useLang, fetchDictionary, { - // initialValue: i18n.flatten(en), - // }); - // dict(); - - // const t = i18n.translator(dict); // Load new language resource - - // if (useLang === LANGUAGES.de) { - // createRes - // } else if (useLang === LANGUAGES.ja) { - // import(`./i18n/japanese.json`) - // .then((messages) => { - // i18n.addResourceBundle(useLang as string, "translation", messages.default); - // return i18n.changeLanguage(useLang); - // }) - // .catch((error) => { - // log.error(error); - // }); - // } else if (useLang === LANGUAGES.ko) { - // import(`./i18n/korean.json`) - // .then((messages) => { - // i18n.addResourceBundle(useLang as string, "translation", messages.default); - // return i18n.changeLanguage(useLang); - // }) - // .catch((error) => { - // log.error(error); - // }); - // } else if (useLang === LANGUAGES.zh) { - // import(`./i18n/mandarin.json`) - // .then((messages) => { - // i18n.addResourceBundle(useLang as string, "translation", messages.default); - // return i18n.changeLanguage(useLang); - // }) - // .catch((error) => { - // log.error(error); - // }); - // } else if (useLang === LANGUAGES.es) { - // import(`./i18n/spanish.json`) - // .then((messages) => { - // i18n.addResourceBundle(useLang as string, "translation", messages.default); - // return i18n.changeLanguage(useLang); - // }) - // .catch((error) => { - // log.error(error); - // }); - // } else if (useLang === LANGUAGES.fr) { - // import(`./i18n/french.json`) - // .then((messages) => { - // i18n.addResourceBundle(useLang as string, "translation", messages.default); - // return i18n.changeLanguage(useLang); - // }) - // .catch((error) => { - // log.error(error); - // }); - // } else if (useLang === LANGUAGES.pt) { - // import(`./i18n/portuguese.json`) - // .then((messages) => { - // i18n.addResourceBundle(useLang as string, "translation", messages.default); - // return i18n.changeLanguage(useLang); - // }) - // .catch((error) => { - // log.error(error); - // }); - // } else if (useLang === LANGUAGES.nl) { - // import(`./i18n/dutch.json`) - // .then((messages) => { - // i18n.addResourceBundle(useLang as string, "translation", messages.default); - // return i18n.changeLanguage(useLang); - // }) - // .catch((error) => { - // log.error(error); - // }); - // } else if (useLang === LANGUAGES.tr) { - // import(`./i18n/turkish.json`) - // .then((messages) => { - // i18n.addResourceBundle(useLang as string, "translation", messages.default); - // return i18n.changeLanguage(useLang); - // }) - // .catch((error) => { - // log.error(error); - // }); - // } else if (useLang === LANGUAGES.en) { - // import(`./i18n/english.json`) - // .then((messages) => { - // i18n.addResourceBundle(useLang as string, "translation", messages.default); - // return i18n.changeLanguage(useLang); - // }) - // .catch((error) => { - // log.error(error); - // }); - // } + changeLocale(this.uiConfig.defaultLanguage || "en"); return new Promise((resolve) => { this.stateEmitter.once("MOUNTED", () => {