From e0fd2c00816c0f5f9b646cda4524567f26d054da Mon Sep 17 00:00:00 2001 From: oceans404 Date: Thu, 26 Feb 2026 14:43:18 -0800 Subject: [PATCH 1/9] Add Google Translate integration with language switcher --- README.md | 14 ++ src/app/(sidebar)/account/fund/page.tsx | 8 +- src/app/(sidebar)/account/saved/page.tsx | 5 +- .../components/SavedEndpointsPage.tsx | 6 +- .../(sidebar)/smart-contracts/saved/page.tsx | 5 +- src/app/(sidebar)/transaction/saved/page.tsx | 5 +- src/app/(sidebar)/xdr/diff/page.tsx | 2 +- src/app/layout.tsx | 44 ++++- src/components/CodeEditor/index.tsx | 2 +- src/components/FloatNotification/index.tsx | 2 +- src/components/GoogleTranslateMountPoint.tsx | 27 +++ src/components/Home/Networks.tsx | 12 +- src/components/LanguageSelector/index.tsx | 139 ++++++++++++++ src/components/LanguageSelector/styles.scss | 65 +++++++ src/components/NetworkIndicator/index.tsx | 2 +- src/components/NetworkName.tsx | 8 + src/components/NetworkSelector/index.tsx | 5 +- src/components/NoTranslate.tsx | 12 ++ src/components/PrettyJson/index.tsx | 5 +- src/components/StellarDataRenderer/index.tsx | 6 +- src/components/SwitchNetworkButtons.tsx | 3 +- src/components/TxHashLink.tsx | 3 +- src/components/TxResponse/index.tsx | 5 +- src/components/XdrLink.tsx | 3 +- src/components/XdrTypeSelect/index.tsx | 2 +- .../layout/LayoutSidebarContent.tsx | 2 + src/constants/networkLimits.ts | 180 +++++++++--------- src/helpers/translate.ts | 46 +++++ src/middleware.ts | 6 +- src/styles/globals.scss | 31 +++ tests/e2e/fundAccountPage.test.ts | 2 +- tests/e2e/languageSelector.test.ts | 93 +++++++++ tests/e2e/networkSelector.test.ts | 4 +- tests/e2e/savedRequests.test.ts | 2 +- 34 files changed, 629 insertions(+), 127 deletions(-) create mode 100644 src/components/GoogleTranslateMountPoint.tsx create mode 100644 src/components/LanguageSelector/index.tsx create mode 100644 src/components/LanguageSelector/styles.scss create mode 100644 src/components/NetworkName.tsx create mode 100644 src/components/NoTranslate.tsx create mode 100644 src/helpers/translate.ts create mode 100644 tests/e2e/languageSelector.test.ts diff --git a/README.md b/README.md index fbbe23022..77fbab6e7 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,20 @@ In the deployment location the following command will run the app: node server.js ``` +## Adding a language + +Add an entry to the `LANGUAGES` array in +`src/components/LanguageSelector/index.tsx`: + +```typescript +{ code: "it", label: "Italiano" } +``` + +`code` must be a +[BCP 47 tag supported by Google Translate](https://cloud.google.com/translate/docs/languages) +(e.g. `"zh-CN"`, `"pt"`, `"ar"`). `label` should be the language's own name, +not its English translation. + ## Tracking To improve Lab, we use [Amplitude](https://amplitude.com/) and diff --git a/src/app/(sidebar)/account/fund/page.tsx b/src/app/(sidebar)/account/fund/page.tsx index 01b91566c..4275e6fdb 100644 --- a/src/app/(sidebar)/account/fund/page.tsx +++ b/src/app/(sidebar)/account/fund/page.tsx @@ -17,6 +17,8 @@ import { useFriendBot } from "@/query/useFriendBot"; import { useAccountInfo } from "@/query/useAccountInfo"; import { useAddTrustline } from "@/query/useAddTrustline"; import { useSubmitHorizonTx } from "@/query/useSubmitHorizonTx"; +import { NetworkName } from "@/components/NetworkName"; + import { useStore } from "@/store/useStore"; import { EURC_TESTNET_ISSUER, USDC_TESTNET_ISSUER } from "@/constants/settings"; @@ -201,7 +203,7 @@ export default function FundAccount() { id: `fund-account-success-xlm`, type: "success", title: "XLM has been successfully funded!", - description: `10,000 XLM was funded to ${shortenStellarAddress(inputPublicKey)} on ${network.label}.`, + description: <>10,000 XLM was funded to {shortenStellarAddress(inputPublicKey)} on {network.label}., }); setActiveToken(""); @@ -245,7 +247,7 @@ export default function FundAccount() { id: `fund-account-success-${assetCode}`, type: "success", title: "Trustline added", - description: `${assetCode} trustline has been successfully added to ${shortenStellarAddress(inputPublicKey)} on ${network.label}.`, + description: <>{assetCode} trustline has been successfully added to {shortenStellarAddress(inputPublicKey)} on {network.label}., }); } }, [ @@ -306,7 +308,7 @@ export default function FundAccount() { return (
Friendbot: fund a {network.label} account or contract with XLM, USDC, and EURC} >
diff --git a/src/app/(sidebar)/account/saved/page.tsx b/src/app/(sidebar)/account/saved/page.tsx index e838184e2..522e996fb 100644 --- a/src/app/(sidebar)/account/saved/page.tsx +++ b/src/app/(sidebar)/account/saved/page.tsx @@ -24,6 +24,8 @@ import { arrayItem } from "@/helpers/arrayItem"; import { getNetworkHeaders } from "@/helpers/getNetworkHeaders"; import { getNetworkById } from "@/helpers/getNetworkById"; +import { NetworkName } from "@/components/NetworkName"; + import { useStore } from "@/store/useStore"; import { useIsTestingNetwork } from "@/hooks/useIsTestingNetwork"; import { useFriendBot } from "@/query/useFriendBot"; @@ -97,7 +99,8 @@ export default function SavedKeypairs() { <> {savedKeypairs.length === 0 - ? `There are no saved keypairs on ${network.label} network.` + ? <>There are no saved keypairs on {network.label} network. + : savedKeypairs.map((kp) => ( { const HorizonEndpoints = () => { if (savedEndpointsHorizon.length === 0) { - return `There are no saved Horizon Endpoints on ${network.label} network`; + return <>There are no saved Horizon Endpoints on {network.label} network; } return ( @@ -272,7 +274,7 @@ export const SavedEndpointsPage = () => { const RpcEndpoints = () => { if (savedRpcMethods.length === 0) { - return `There are no saved RPC Methods ${network.label} network`; + return <>There are no saved RPC Methods on {network.label} network; } return ( diff --git a/src/app/(sidebar)/smart-contracts/saved/page.tsx b/src/app/(sidebar)/smart-contracts/saved/page.tsx index b4ee53fc5..c1715a2d8 100644 --- a/src/app/(sidebar)/smart-contracts/saved/page.tsx +++ b/src/app/(sidebar)/smart-contracts/saved/page.tsx @@ -16,6 +16,8 @@ import { arrayItem } from "@/helpers/arrayItem"; import { delayedAction } from "@/helpers/delayedAction"; import { Routes } from "@/constants/routes"; +import { NetworkName } from "@/components/NetworkName"; + import { useStore } from "@/store/useStore"; import { trackEvent, TrackingEvent } from "@/metrics/tracking"; @@ -46,7 +48,8 @@ export default function SavedSmartContracts() { <> {savedContracts.length === 0 - ? `There are no saved smart contracts on ${network.label} network.` + ? <>There are no saved smart contracts on {network.label} network. + : savedContracts.map((c) => ( <> {savedTxns.length === 0 - ? `There are no saved transactions on ${network.label} network.` + ? <>There are no saved transactions on {network.label} network. + : savedTxns.map((t) => ( ))} diff --git a/src/app/(sidebar)/xdr/diff/page.tsx b/src/app/(sidebar)/xdr/diff/page.tsx index f7529957d..3b7fddc07 100644 --- a/src/app/(sidebar)/xdr/diff/page.tsx +++ b/src/app/(sidebar)/xdr/diff/page.tsx @@ -191,7 +191,7 @@ export default function DiffXdr() { ) : null}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx index df3448de0..f045fc66f 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,6 @@ import React, { Suspense } from "react"; import type { Metadata } from "next"; +import { headers } from "next/headers"; import { LayoutMain } from "@/components/layout/LayoutMain"; import { LayoutContextProvider } from "@/components/layout/LayoutContextProvider"; @@ -9,6 +10,7 @@ import { CustomAiButton } from "@/components/CustomAiButton"; import { QueryProvider } from "@/query/QueryProvider"; import { StoreProvider } from "@/store/StoreProvider"; import { GoogleAnalytics } from "@/metrics/GoogleAnalytics"; +import { GoogleTranslateMountPoint } from "@/components/GoogleTranslateMountPoint"; import "@stellar/design-system/build/styles.min.css"; import "@/styles/globals.scss"; @@ -22,14 +24,49 @@ export const metadata: Metadata = { // Automatically generates nonce for script and style tags export const dynamic = "force-dynamic"; -export default function RootLayout({ +export default async function RootLayout({ children, }: { children: React.ReactNode; }) { + const nonce = (await headers()).get("x-nonce") ?? ""; + return ( - - + + + {/* Google Translate init — must carry nonce to pass the app's CSP. + suppressHydrationWarning is required because browsers strip nonce + attributes from the DOM after load (to prevent CSS exfiltration), + so the client-side React tree sees nonce="" while the server-rendered + HTML has the real value. The nonce is still present in the raw HTML + that the browser parses for CSP enforcement before React runs. */} +