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/playwright.config.ts b/playwright.config.ts index 54107a8fa..61a17bb48 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -38,6 +38,21 @@ export default defineConfig({ video: "off", actionTimeout: 10 * 1000, navigationTimeout: 15 * 1000, + launchOptions: { + args: [ + // Block Google Translate CDN requests so they don't slow down page + // loads in test environments. The app loads the GT script on every + // page (src/app/layout.tsx), and in environments with restricted or + // slow access to Google's CDN the external requests can push page + // loads past navigationTimeout, causing flaky failures in unrelated + // tests. Mapping these hosts to 127.0.0.1 makes connections fail + // immediately ("connection refused") rather than hanging. The async + // GT script handles the failure gracefully without blocking page load. + // Language selector tests are unaffected — they only verify cookie + // behaviour and UI state, not that the page is actually translated. + "--host-rules=MAP translate.google.com 127.0.0.1, MAP translate.googleapis.com 127.0.0.1", + ], + }, }, /* Run your local dev server before starting the tests */ diff --git a/src/app/(sidebar)/account/fund/components/SwitchNetwork.tsx b/src/app/(sidebar)/account/fund/components/SwitchNetwork.tsx index 864794255..bbbdde637 100644 --- a/src/app/(sidebar)/account/fund/components/SwitchNetwork.tsx +++ b/src/app/(sidebar)/account/fund/components/SwitchNetwork.tsx @@ -2,6 +2,8 @@ import { Card, Text } from "@stellar/design-system"; import { SwitchNetworkButtons } from "@/components/SwitchNetworkButtons"; +import { AssetCode } from "@/components/AssetCode"; +import { NetworkName } from "@/components/NetworkName"; import "../../styles.scss"; @@ -11,15 +13,15 @@ export const SwitchNetwork = () => {
- Fund a Futurenet or Testnet network account or contract with XLM, - USDC, and EURC + Fund a{" "}Futurenet{" "}or{" "}Testnet{" "}network account or contract with XLM,{" "} + USDC, and EURC - You must switch your network to Testnet or Futurenet in order to + You must switch your network to{" "}Testnet{" "}or{" "}Futurenet{" "}in order to fund keypairs. Friendbot is a standalone service that funds your - account or contract with XLM. To fund assets such as USDC and EURC, - you’ll need to add a trustline manually before funding. + account or contract with XLM. Adding a trustline is required to + fund assets such as{" "}USDC{" "}and{" "}EURC.
diff --git a/src/app/(sidebar)/account/fund/page.tsx b/src/app/(sidebar)/account/fund/page.tsx index 01b91566c..c6d410a44 100644 --- a/src/app/(sidebar)/account/fund/page.tsx +++ b/src/app/(sidebar)/account/fund/page.tsx @@ -17,6 +17,9 @@ 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 { AssetCode } from "@/components/AssetCode"; + import { useStore } from "@/store/useStore"; import { EURC_TESTNET_ISSUER, USDC_TESTNET_ISSUER } from "@/constants/settings"; @@ -201,7 +204,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 +248,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,13 +309,13 @@ export default function FundAccount() { return (
Friendbot: fund a{" "}{network.label}{" "}account or contract with XLM,{" "}USDC, and{" "}EURC} >
Friendbot is a standalone service that funds your testnet account or - contract with XLM. To fund assets such as USDC and EURC, you’ll need - to add a trustline manually before funding. + contract with XLM. Adding a trustline is required to fund assets + such as{" "}USDC{" "}and{" "}EURC. {`${formatNumber(parseFloat(t.amount))} ${t.currency}`} + > + {formatNumber(parseFloat(t.amount))}{" "} + {t.currency} +
{t.id === "xlm" || hasTrustline ? ( diff --git a/src/app/(sidebar)/account/saved/page.tsx b/src/app/(sidebar)/account/saved/page.tsx index e838184e2..39e4ceb8e 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..7cdcb1199 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. */} +