From 7c9c99208464aa923bc765295bfec8240aade8ab Mon Sep 17 00:00:00 2001 From: benalleng Date: Fri, 21 Jul 2023 13:45:27 -0400 Subject: [PATCH] translation pass 2 --- package.json | 4 +- src/components/AmountCard.tsx | 9 +- src/components/AmountEditable.tsx | 40 +++-- src/components/DeleteEverything.tsx | 17 ++- src/components/ImportExport.tsx | 65 ++++++-- src/components/Logs.tsx | 10 +- src/components/MoreInfoModal.tsx | 2 +- src/components/Reader.tsx | 22 ++- src/components/SeedWords.tsx | 18 ++- src/components/layout/BackButton.tsx | 4 +- src/components/layout/BackLink.tsx | 4 +- src/components/layout/BackPop.tsx | 4 +- src/components/layout/ProgressBar.tsx | 10 +- src/components/layout/index.tsx | 7 +- src/i18n/config.ts | 23 +-- src/i18n/en/translations.ts | 204 ++++++++++++++++++++++++-- src/i18n/i18next.d.ts | 9 ++ src/routes/Feedback.tsx | 79 +++++----- src/routes/Scanner.tsx | 6 +- src/routes/Send.tsx | 6 +- src/routes/Swap.tsx | 41 ++++-- src/routes/[...404].tsx | 10 +- src/routes/settings/Admin.tsx | 23 +-- src/routes/settings/Backup.tsx | 33 ++--- src/routes/settings/Channels.tsx | 37 +++-- src/routes/settings/Connections.tsx | 47 ++++-- src/routes/settings/EmergencyKit.tsx | 19 ++- src/routes/settings/Encrypt.tsx | 7 +- src/routes/settings/index.tsx | 41 +++--- 29 files changed, 573 insertions(+), 228 deletions(-) create mode 100644 src/i18n/i18next.d.ts diff --git a/package.json b/package.json index 24fdd11..2890bc7 100644 --- a/package.json +++ b/package.json @@ -37,14 +37,14 @@ "workbox-window": "^6.6.0" }, "dependencies": { - "@mutinywallet/barcode-scanner": "5.0.0-beta.3", "@capacitor/android": "^5.2.1", "@capacitor/clipboard": "^5.0.6", "@capacitor/core": "^5.2.1", "@kobalte/core": "^0.9.8", "@kobalte/tailwindcss": "^0.5.0", - "@mutinywallet/mutiny-wasm": "0.4.4", "@modular-forms/solid": "^0.18.0", + "@mutinywallet/barcode-scanner": "5.0.0-beta.3", + "@mutinywallet/mutiny-wasm": "0.4.4", "@mutinywallet/waila-wasm": "^0.2.1", "@solid-primitives/upload": "^0.0.111", "@solidjs/meta": "^0.28.5", diff --git a/src/components/AmountCard.tsx b/src/components/AmountCard.tsx index 059a2dc..ee60816 100644 --- a/src/components/AmountCard.tsx +++ b/src/components/AmountCard.tsx @@ -3,6 +3,7 @@ import { Card, VStack } from "~/components/layout"; import { useMegaStore } from "~/state/megaStore"; import { satsToUsd } from "~/utils/conversions"; import { AmountEditable } from "./AmountEditable"; +import { useI18n } from "~/i18n/context"; const noop = () => { // do nothing @@ -25,6 +26,7 @@ export const InlineAmount: ParentComponent<{ sign?: string; fiat?: boolean; }> = (props) => { + const i18n = useI18n(); const prettyPrint = createMemo(() => { const parsed = Number(props.amount); if (isNaN(parsed)) { @@ -39,12 +41,15 @@ export const InlineAmount: ParentComponent<{ {props.sign ? `${props.sign} ` : ""} {props.fiat ? "$" : ""} {prettyPrint()}{" "} - {props.fiat ? "USD" : "SATS"} + + {props.fiat ? i18n.t("common.usd") : i18n.t("common.sats")} + ); }; function USDShower(props: { amountSats: string; fee?: string }) { + const i18n = useI18n(); const [state, _] = useMegaStore(); const amountInUsd = () => satsToUsd(state.price, add(props.amountSats, props.fee), true); @@ -54,7 +59,7 @@ function USDShower(props: { amountSats: string; fee?: string }) {
~{amountInUsd()}  - USD + {i18n.t("common.usd")}
diff --git a/src/components/AmountEditable.tsx b/src/components/AmountEditable.tsx index 7cc006b..eed5984 100644 --- a/src/components/AmountEditable.tsx +++ b/src/components/AmountEditable.tsx @@ -69,13 +69,19 @@ function SingleDigitButton(props: { onClear: () => void; fiat: boolean; }) { + const i18n = useI18n(); let holdTimer: number; const holdThreshold = 500; function onHold() { - holdTimer = setTimeout(() => { - props.onClear(); - }, holdThreshold); + if ( + props.character === "DEL" || + props.character === i18n.t("char.del") + ) { + holdTimer = setTimeout(() => { + props.onClear(); + }, holdThreshold); + } } function endHold() { @@ -130,7 +136,7 @@ function BigScalingText(props: { text: string; fiat: boolean }) { > {props.text}  - {props.fiat ? "USD" : `${i18n.t("common.sats")}`} + {props.fiat ? i18n.t("common.usd") : i18n.t("common.sats")} ); @@ -142,7 +148,7 @@ function SmallSubtleAmount(props: { text: string; fiat: boolean }) {

~{props.text}  - {props.fiat ? "USD" : `${i18n.t("common.sats")}`} + {props.fiat ? i18n.t("common.usd") : `${i18n.t("common.sats")}`} toDisplayHandleNaN(localSats(), false); @@ -243,9 +249,13 @@ export const AmountEditable: ParentComponent<{ if ((state.balance?.lightning || 0n) === 0n) { const network = state.mutiny_wallet?.get_network() as Network; if (network === "bitcoin") { - return "Your first lightning receive needs to be 50,000 sats or greater. A setup fee will be deducted from the requested amount."; + return i18n.t("receive.amount_editable.receive_too_small", { + amount: "50,000" + }); } else { - return i18n.t("amount_editable_first_payment_10k_or_greater"); + return i18n.t("receive.amount_editable.receive_too_small", { + amount: "10,000" + }); } } @@ -255,7 +265,7 @@ export const AmountEditable: ParentComponent<{ } if (parsed > (inboundCapacity() || 0)) { - return "A lightning setup fee will be charged if paid over lightning."; + return i18n.t("receive.amount_editable.setup_fee_lightning"); } return undefined; @@ -269,10 +279,10 @@ export const AmountEditable: ParentComponent<{ if (parsed >= 2099999997690000) { // If over 21 million bitcoin, warn that too much - return i18n.t("more_than_21m"); + return i18n.t("receive.amount_editable.more_than_21m"); } else if (parsed >= 4000000) { // If over 4 million sats, warn that it's a beta bro - return i18n.t("too_big_for_beta"); + return i18n.t("receive.amount_editable.too_big_for_beta"); } }; @@ -285,7 +295,7 @@ export const AmountEditable: ParentComponent<{ let sane; - if (character === "DEL") { + if (character === "DEL" || character === i18n.t("char.del")) { if (localValue().length <= 1) { sane = "0"; } else { @@ -424,7 +434,7 @@ export const AmountEditable: ParentComponent<{ when={localSats() !== "0"} fallback={
- {i18n.t("set_amount")} + {i18n.t("receive.amount_editable.set_amount")}
} > @@ -540,7 +550,7 @@ export const AmountEditable: ParentComponent<{ }} class="py-2 px-4 rounded-lg bg-white/10" > - MAX + {i18n.t("receive.amount_editable.max")} @@ -561,7 +571,7 @@ export const AmountEditable: ParentComponent<{ class="w-full flex-none" onClick={handleSubmit} > - {i18n.t("set_amount")} + {i18n.t("receive.amount_editable.set_amount")} diff --git a/src/components/DeleteEverything.tsx b/src/components/DeleteEverything.tsx index 2a90794..fbdfb97 100644 --- a/src/components/DeleteEverything.tsx +++ b/src/components/DeleteEverything.tsx @@ -3,10 +3,12 @@ import { createSignal } from "solid-js"; import { ConfirmDialog } from "~/components/Dialog"; import { Button } from "~/components/layout"; import { showToast } from "~/components/Toaster"; +import { useI18n } from "~/i18n/context"; import { useMegaStore } from "~/state/megaStore"; import eify from "~/utils/eify"; export function DeleteEverything(props: { emergency?: boolean }) { + const i18n = useI18n(); const [state, actions] = useMegaStore(); async function confirmReset() { @@ -34,7 +36,14 @@ export function DeleteEverything(props: { emergency?: boolean }) { await MutinyWallet.import_json("{}"); } - showToast({ title: "Deleted", description: `Deleted all data` }); + showToast({ + title: i18n.t( + "settings.emergency_kit.delete_everything.deleted" + ), + description: i18n.t( + "settings.emergency_kit.delete_everything.deleted_description" + ) + }); setTimeout(() => { window.location.href = "/"; @@ -50,14 +59,16 @@ export function DeleteEverything(props: { emergency?: boolean }) { return ( <> - + setConfirmOpen(false)} > - This will delete your node's state. This can't be undone! + {i18n.t("settings.emergency_kit.delete_everything.confirm")} ); diff --git a/src/components/ImportExport.tsx b/src/components/ImportExport.tsx index 73a1531..ff25b86 100644 --- a/src/components/ImportExport.tsx +++ b/src/components/ImportExport.tsx @@ -15,8 +15,10 @@ import { ConfirmDialog } from "./Dialog"; import initMutinyWallet, { MutinyWallet } from "@mutinywallet/mutiny-wasm"; import { InfoBox } from "./InfoBox"; import { TextField } from "./layout/TextField"; +import { useI18n } from "~/i18n/context"; export function ImportExport(props: { emergency?: boolean }) { + const i18n = useI18n(); const [state, _] = useMegaStore(); const [error, setError] = createSignal(); @@ -44,7 +46,11 @@ export function ImportExport(props: { emergency?: boolean }) { try { setError(undefined); if (!password()) { - throw new Error("Password is required"); + throw new Error( + i18n.t( + "settings.emergency_kit.import_export.error_password" + ) + ); } const json = await MutinyWallet.export_json(password()); downloadTextFile(json || "", "mutiny-state.json"); @@ -77,11 +83,23 @@ export function ImportExport(props: { emergency?: boolean }) { if (result) { resolve(result); } else { - reject(new Error("No text found in file")); + reject( + new Error( + i18n.t( + "settings.emergency_kit.import_export.error_no_text" + ) + ) + ); } }; fileReader.onerror = (_e) => - reject(new Error("File read error")); + reject( + new Error( + i18n.t( + "settings.emergency_kit.import_export.error_read_file" + ) + ) + ); fileReader.readAsText(file, "UTF-8"); }); @@ -130,25 +148,35 @@ export function ImportExport(props: { emergency?: boolean }) { return ( <> - + - You can export your entire Mutiny Wallet state to a file and - import it into a new browser. It usually works! + {i18n.t("settings.emergency_kit.import_export.tip")} - Important caveats: after exporting don't do - any operations in the original browser. If you do, you'll - need to export again. After a successful import, a best - practice is to clear the state of the original browser just - to make sure you don't create conflicts. + + {i18n.t( + "settings.emergency_kit.import_export.caveat_header" + )} + {" "} + {i18n.t("settings.emergency_kit.import_export.caveat")}
{error()?.message} - - + + setConfirmOpen(false)} > - Do you want to replace your state with {files()[0].name}? + {i18n.t("settings.emergency_kit.import_export.confirm_replace")}{" "} + {files()[0].name}? {/* TODO: this is pretty redundant with the DecryptDialog, could make a shared component */}
@@ -180,7 +211,9 @@ export function ImportExport(props: { emergency?: boolean }) { {error()?.message}
diff --git a/src/components/Logs.tsx b/src/components/Logs.tsx index 0eb0eff..d871677 100644 --- a/src/components/Logs.tsx +++ b/src/components/Logs.tsx @@ -1,8 +1,10 @@ import { MutinyWallet } from "@mutinywallet/mutiny-wasm"; import { Button, InnerCard, NiceP, VStack } from "~/components/layout"; +import { useI18n } from "~/i18n/context"; import { downloadTextFile } from "~/utils/download"; export function Logs() { + const i18n = useI18n(); async function handleSave() { try { const logs = await MutinyWallet.get_logs(); @@ -18,11 +20,13 @@ export function Logs() { } return ( - + - Something screwy going on? Check out the logs! + + {i18n.t("settings.emergency_kit.logs.something_screwy")} + diff --git a/src/components/MoreInfoModal.tsx b/src/components/MoreInfoModal.tsx index afcbaaf..eb83204 100644 --- a/src/components/MoreInfoModal.tsx +++ b/src/components/MoreInfoModal.tsx @@ -15,7 +15,7 @@ export function FeesModal(props: { icon?: boolean }) { props.icon ? ( help ) : ( - i18n.t("why?") + i18n.t("why") ) } > diff --git a/src/components/Reader.tsx b/src/components/Reader.tsx index 69462d5..4057044 100644 --- a/src/components/Reader.tsx +++ b/src/components/Reader.tsx @@ -1,5 +1,12 @@ import { onCleanup, onMount } from "solid-js"; -import { BarcodeScanner, BarcodeFormat, CameraPermissionState, CameraPermissionType, CameraPluginPermissions, PermissionStates } from '@mutinywallet/barcode-scanner'; +import { + BarcodeScanner, + BarcodeFormat, + CameraPermissionState, + CameraPermissionType, + CameraPluginPermissions, + PermissionStates +} from "@mutinywallet/barcode-scanner"; import QrScanner from "qr-scanner"; export default function Scanner(props: { onResult: (result: string) => void }) { @@ -12,7 +19,8 @@ export default function Scanner(props: { onResult: (result: string) => void }) { const startScan = async () => { // Check camera permission - const permissions: PermissionStates = await BarcodeScanner.checkPermissions(); + const permissions: PermissionStates = + await BarcodeScanner.checkPermissions(); if (permissions.camera === "granted") { const callback = (result: ScanResult, err?: any) => { if (err) { @@ -24,10 +32,14 @@ export default function Scanner(props: { onResult: (result: string) => void }) { handleResult({ data: result.content }); // pass the raw scanned content } }; - await BarcodeScanner.start({ targetedFormats: [BarcodeFormat.QR_CODE] }, callback); + await BarcodeScanner.start( + { targetedFormats: [BarcodeFormat.QR_CODE] }, + callback + ); } else if (permissions.camera === "prompt") { // Request permission if it has not been asked before - const requestedPermissions: PermissionStates = await BarcodeScanner.requestPermissions(); + const requestedPermissions: PermissionStates = + await BarcodeScanner.requestPermissions(); if (requestedPermissions.camera === "granted") { // If user grants permission, start the scan await startScan(); @@ -35,7 +47,7 @@ export default function Scanner(props: { onResult: (result: string) => void }) { } else if (permissions.camera === "denied") { // Handle the scenario when user denies the permission // Maybe show a user friendly message here - console.log('Camera permission was denied'); + console.log("Camera permission was denied"); } }; diff --git a/src/components/SeedWords.tsx b/src/components/SeedWords.tsx index 67b536f..c289ed7 100644 --- a/src/components/SeedWords.tsx +++ b/src/components/SeedWords.tsx @@ -1,11 +1,13 @@ import { For, Match, Switch, createMemo, createSignal } from "solid-js"; import { useCopy } from "~/utils/useCopy"; import copyIcon from "~/assets/icons/copy.svg"; +import { useI18n } from "~/i18n/context"; export function SeedWords(props: { words: string; setHasSeen?: (hasSeen: boolean) => void; }) { + const i18n = useI18n(); const [shouldShow, setShouldShow] = createSignal(false); const [copy, copied] = useCopy({ copiedTimeout: 1000 }); @@ -30,7 +32,9 @@ export function SeedWords(props: { class="cursor-pointer flex w-full justify-center" onClick={toggleShow} > - TAP TO REVEAL SEED WORDS + + {i18n.t("settings.backup.seed_words.reveal")} + @@ -40,7 +44,9 @@ export function SeedWords(props: { class="cursor-pointer flex w-full justify-center" onClick={toggleShow} > - HIDE + + {i18n.t("settings.backup.seed_words.hide")} +
    @@ -59,8 +65,12 @@ export function SeedWords(props: {
    {copied() - ? "Copied!" - : "Dangerously Copy to Clipboard"} + ? i18n.t( + "settings.backup.seed_words.copied" + ) + : i18n.t( + "settings.backup.seed_words.copy" + )} void; title?: string; showOnDesktop?: boolean; }) { + const i18n = useI18n(); return ( ); } diff --git a/src/components/layout/BackLink.tsx b/src/components/layout/BackLink.tsx index b31fcbe..c66f5f5 100644 --- a/src/components/layout/BackLink.tsx +++ b/src/components/layout/BackLink.tsx @@ -1,14 +1,16 @@ import { A } from "solid-start"; import { Back } from "~/assets/svg/Back"; +import { useI18n } from "~/i18n/context"; export function BackLink(props: { href?: string; title?: string }) { + const i18n = useI18n(); return ( - {props.title ? props.title : "Home"} + {props.title ? props.title : i18n.t("common.home")} ); } diff --git a/src/components/layout/BackPop.tsx b/src/components/layout/BackPop.tsx index 006481d..d553cfc 100644 --- a/src/components/layout/BackPop.tsx +++ b/src/components/layout/BackPop.tsx @@ -1,11 +1,13 @@ import { useLocation, useNavigate } from "solid-start"; import { BackButton } from "./BackButton"; +import { useI18n } from "~/i18n/context"; type StateWithPrevious = { previous?: string; }; export function BackPop() { + const i18n = useI18n(); const navigate = useNavigate(); const location = useLocation(); @@ -15,7 +17,7 @@ export function BackPop() { return ( navigate(backPath())} showOnDesktop /> diff --git a/src/components/layout/ProgressBar.tsx b/src/components/layout/ProgressBar.tsx index 64b945f..62ede29 100644 --- a/src/components/layout/ProgressBar.tsx +++ b/src/components/layout/ProgressBar.tsx @@ -1,5 +1,6 @@ import { Progress } from "@kobalte/core"; import { SmallHeader } from "."; +import { useI18n } from "~/i18n/context"; export default function formatNumber(num: number) { const map = [ @@ -21,19 +22,24 @@ export default function formatNumber(num: number) { } export function ProgressBar(props: { value: number; max: number }) { + const i18n = useI18n(); return ( - `${formatNumber(value)} of ${formatNumber(max)} sats sent` + `${formatNumber(value)} ${i18n.t( + "send.progress_bar.of" + )} ${formatNumber(max)} ${i18n.t( + "send.progress_bar.sats_sent" + )}` } class="w-full flex flex-col gap-2" >
    - Sending... + {i18n.t("send.sending")}
    diff --git a/src/components/layout/index.tsx b/src/components/layout/index.tsx index e744eed..cd8f66c 100644 --- a/src/components/layout/index.tsx +++ b/src/components/layout/index.tsx @@ -25,6 +25,7 @@ import { A } from "solid-start"; import down from "~/assets/icons/down.svg"; import { DecryptDialog } from "../DecryptDialog"; import { LoadingIndicator } from "~/components/LoadingIndicator"; +import { useI18n } from "~/i18n/context"; export { Button, ButtonLink, Linkify }; @@ -138,6 +139,7 @@ export const DefaultMain: ParentComponent = (props) => { }; export const FullscreenLoader = () => { + const i18n = useI18n(); const [waitedTooLong, setWaitedTooLong] = createSignal(false); setTimeout(() => { @@ -149,10 +151,9 @@ export const FullscreenLoader = () => {

    - Stuck on this screen? Try reloading. If that doesn't work, - check out the{" "} + {i18n.t("error.load_time.stuck")}{" "} - emergency kit. + {i18n.t("error.load_time.emergency_link")}

    diff --git a/src/i18n/config.ts b/src/i18n/config.ts index 98e4664..60b4504 100644 --- a/src/i18n/config.ts +++ b/src/i18n/config.ts @@ -5,27 +5,32 @@ import LanguageDetector from "i18next-browser-languagedetector"; import en from "~/i18n/en/translations"; import pt from "~/i18n/pt/translations"; +export const resources = { + en: { + translations: en + }, + pt: { + translations: pt + } +}; + +export const defaultNS = "translations"; + const i18n = use(LanguageDetector).init( { + returnNull: false, fallbackLng: "en", preload: ["en"], load: "languageOnly", ns: ["translations"], - defaultNS: "translations", + defaultNS: defaultNS, fallbackNS: false, debug: true, detection: { order: ["querystring", "navigator", "htmlTag"], lookupQuerystring: "lang" }, - resources: { - en: { - translations: en - }, - pt: { - translations: pt - } - } + resources: resources // FIXME: this doesn't work when deployed // backend: { // loadPath: 'src/i18n/{{lng}}/{{ns}}.json', diff --git a/src/i18n/en/translations.ts b/src/i18n/en/translations.ts index 8e06eea..9709eae 100644 --- a/src/i18n/en/translations.ts +++ b/src/i18n/en/translations.ts @@ -7,7 +7,9 @@ export default { usd: "USD", fee: "Fee", send: "Send", - receive: "Receive" + receive: "Receive", + dangit: "Dangit", + back: "Back" }, char: { del: "DEL" @@ -19,13 +21,55 @@ export default { choose_format: "Choose format", payment_received: "Payment Received", payment_initiated: "Payment Initiated", - receive_add_the_sender: "Add the sender for your records" + receive_add_the_sender: "Add the sender for your records", + amount_editable: { + receive_too_small: + "Your first lightning receive needs to be {{amount}} sats or greater. A setup fee will be deducted from the requested amount.", + setup_fee_lightning: + "A lightning setup fee will be charged if paid over lightning.", + too_big_for_beta: + "That's a lot of sats. You do know Mutiny Wallet is still in beta, yeah?", + more_than_21m: + "There are only 21 million bitcoin.", + set_amount: "Set amount", + max: "MAX" + } }, send: { sending: "Sending...", confirm_send: "Confirm Send", contact_placeholder: "Add the receiver for your records", - start_over: "Start Over" + start_over: "Start Over", + progress_bar: { + of: "of", + sats_sent: "sats sent" + } + }, + feedback: { + header: "Give us feedback!", + received: "Feedback received!", + thanks: "Thank you for letting us know what's going on.", + more: "Got more to say?", + tracking: + "Mutiny doesn't track or spy on your behavior, so your feedback is incredibly helpful.", + github_one: "If you're comfortable with GitHub you can also", + github_two: ".", + create_issue: "create an issue", + link: "Feedback?", + feedback_placeholder: "Bugs, feature requests, feedback, etc.", + info_label: "Include contact info", + info_caption: "If you need us to follow-up on this issue", + email: "Email", + email_caption: "Burners welcome", + nostr: "Nostr", + nostr_caption: "Your freshest npub", + nostr_label: "Nostr npub or NIP-05", + send_feedback: "Send Feedback", + invalid_feedback: "Please say something!", + need_contact: "We need some way to contact you", + invalid_email: "That doesn't look like an email address to me", + error: "Error submitting feedback", + try_again: "Please try again later." }, activity: { view_all: "View all", @@ -34,25 +78,159 @@ export default { channel_close: "Channel Close", unknown: "Unknown" }, + redshift: {}, + scanner: { + paste: "Paste Something", + cancel: "Cancel" + }, + settings: { + header: "Settings", + mutiny_plus: "MUTINY+", + support: "Learn how to support Mutiny", + general: "GENERAL", + beta_features: "BETA FEATURES", + debug_tools: "DEBUG TOOLS", + danger_zone: "Danger zone", + admin: { + title: "Admin Page", + caption: "Our internal debug tools. Use wisely!", + header: "Secret Debug Tools", + warning_one: + "If you know what you're doing you're in the right place.", + warning_two: + "These are internal tools we use to debug and test the app. Please be careful!" + }, + backup: { + title: "Backup", + secure_funds: "Let's get these funds secured.", + twelve_words_tip: + "We'll show you 12 words. You write down the 12 words.", + warning_one: + "If you clear your browser history, or lose your device, these 12 words are the only way you can restore your wallet.", + warning_two: "Mutiny is self-custodial. It's all up to you...", + confirm: "I wrote down the words", + responsibility: "I understand that my funds are my responsibility", + liar: "I'm not lying just to get this over with", + seed_words: { + reveal: "TAP TO REVEAL SEED WORDS", + hide: "HIDE", + copy: "Dangerously Copy to Clipboard", + copied: "Copied!" + } + }, + channels: { + title: "Lightning Channels", + outbound: "Outbound", + inbound: "Inbound", + have_channels: "You have", + have_channels_one: "lightning channel.", + have_channels_many: "lightning channels.", + inbound_outbound_tip: + "Outbound is the amount of money you can spend on lightning. Inbound is the amount you can receive without incurring a lightning service fee.", + no_channels: + "It looks like you don't have any channels yet. To get started, receive some sats over lightning, or swap some on-chain funds into a channel. Get your hands dirty!" + }, + connections: { + title: "Wallet Connections", + error_name: "Name cannot be empty", + error_connection: "Failed to create Wallet Connection", + add_connection: "Add Connection", + manage_connections: "Manage Connections", + disable_connection: "Disable", + enable_connection: "Enable", + new_connection: "New Connection", + new_connection_label: "Name", + new_connection_placeholder: "My favorite nostr client...", + create_connection: "Create Connection", + authorize: + "Authorize external services to request payments from your wallet. Pairs great with Nostr clients." + }, + emergency_kit: { + title: "Emergency Kit", + caption: "Diagnose and solve problems with your wallet.", + emergency_tip: + "If your wallet seems broken, here are some tools to try to debug and repair it.", + questions: + "If you have any questions on what these buttons do, please", + link: "reach out to us for support.", + import_export: { + title: "Export wallet state", + error_password: "Password is required", + error_read_file: "File read error", + error_no_text: "No text found in file", + tip: "You can export your entire Mutiny Wallet state to a file and import it into a new browser. It usually works!", + caveat_header: "Important caveats:", + caveat: "after exporting don't do any operations in the original browser. If you do, you'll need to export again. After a successful import, a best practice is to clear the state of the original browser just to make sure you don't create conflicts.", + save_state: "Save State As File", + import_state: "Import State From File", + confirm_replace: "Do you want to replace your state with", + password: "Enter your password to decrypt", + decrypt_wallet: "Decrypt Wallet" + }, + logs: { + title: "Download debug logs", + something_screwy: + "Something screwy going on? Check out the logs!", + download_logs: "Download Logs" + }, + delete_everything: { + delete: "Delete Everything", + confirm: + "This will delete your node's state. This can't be undone!", + deleted: "Deleted", + deleted_description: "Deleted all data" + } + }, + encrypt: {}, + lnurl_auth: { + title: "LNURL Auth" + }, + plus: {}, + restore: { + title: "Restore" + }, + servers: { + title: "Servers", + caption: "Don't trust us! Use your own servers to back Mutiny." + } + }, + swap: { + peer_not_found: "Peer not found", + channel_too_small: + "It's just silly to make a channel smaller than {{amount}} sats", + insufficient_funds: "You don't have enough funds to make this channel", + header: "Swap to Lightning", + initiated: "Swap Initiated", + sats_added: "sats will be added to your Lightning balance", + use_existing: "Use existing peer", + choose_peer: "Choose a peer", + peer_connect_label: "Connect to new peer", + peer_connect_placeholder: "Peer connect string", + connect: "Connect", + connecting: "Connecting...", + confirm_swap: "Confirm Swap" + }, + error: { + load_time: { + stuck: "Stuck on this screen? Try reloading. If that doesn't work, check out the", + emergency_link: "emergency kit." + }, + not_found: { + title: "Not Found", + wtf_paul: "This is probably Paul's fault." + } + }, create_an_issue: "Create an issue", - feedback: "Bugs? Feedback?", send_bitcoin: "Send Bitcoin", view_transaction: "View Transaction", - amount_editable_first_payment_10k_or_greater: - "Your first lightning receive needs to be 10,000 sats or greater. A setup fee will be deducted from the requested amount.", - "why?": "Why?", + why: "Why?", more_info_modal_p1: "Mutiny is a self-custodial wallet. To initiate a lightning payment we must open a lightning channel, which requires a minimum amount and a setup fee.", more_info_modal_p2: "Future payments, both send and recieve, will only incur normal network fees and a nominal service fee unless your channel runs out of inbound capacity.", learn_more_about_liquidity: "Learn more about liquidity", - set_amount: "Set amount", whats_with_the_fees: "What's with the fees?", private_tags: "Private tags", continue: "Continue", - keep_mutiny_open: "Keep Mutiny open to complete the payment.", - too_big_for_beta: - "That's a lot of sats. You do know Mutiny Wallet is still in beta, yeah?", - more_than_21m: - "There are only 21 million bitcoin.", + keep_mutiny_open: "Keep Mutiny open to complete the payment." }; diff --git a/src/i18n/i18next.d.ts b/src/i18n/i18next.d.ts new file mode 100644 index 0000000..9579b5b --- /dev/null +++ b/src/i18n/i18next.d.ts @@ -0,0 +1,9 @@ +import { resources, defaultNS } from "~/i18n/config"; + +declare module "i18next" { + interface CustomTypeOptions { + defaultNS: typeof defaultNS; + resources: (typeof resources)["en"]; + returnNull: false; + } +} diff --git a/src/routes/Feedback.tsx b/src/routes/Feedback.tsx index 197dcd3..c195ad0 100644 --- a/src/routes/Feedback.tsx +++ b/src/routes/Feedback.tsx @@ -27,10 +27,12 @@ import feedback from "~/assets/icons/feedback.svg"; import { InfoBox } from "~/components/InfoBox"; import eify from "~/utils/eify"; import { MegaCheck } from "~/components/successfail/MegaCheck"; +import { useI18n } from "~/i18n/context"; const FEEDBACK_API = import.meta.env.VITE_FEEDBACK; export function FeedbackLink(props: { setupError?: boolean }) { + const i18n = useI18n(); const location = useLocation(); return ( - Feedback? + {i18n.t("feedback.link")} Feedback ); @@ -58,11 +60,6 @@ type FeedbackForm = { images: File[]; }; -const COMMUNICATION_METHODS = [ - { value: "nostr", label: "Nostr", caption: "Your freshest npub" }, - { value: "email", label: "Email", caption: "Burners welcome" } -]; - async function formDataFromFeedbackForm(f: FeedbackForm) { const formData = new FormData(); @@ -107,9 +104,23 @@ async function formDataFromFeedbackForm(f: FeedbackForm) { } function FeedbackForm(props: { onSubmitted: () => void }) { + const i18n = useI18n(); const [loading, setLoading] = createSignal(false); const [error, setError] = createSignal(); + const COMMUNICATION_METHODS = [ + { + value: "nostr", + label: i18n.t("feedback.nostr"), + caption: i18n.t("feedback.nostr_caption") + }, + { + value: "email", + label: i18n.t("feedback.email"), + caption: i18n.t("feedback.email_caption") + } + ]; + const [feedbackForm, { Form, Field }] = createForm({ initialValues: { user_type: "nostr", @@ -133,7 +144,9 @@ function FeedbackForm(props: { onSubmitted: () => void }) { }); if (!res.ok) { - throw new Error(`Error submitting feedback: ${res.statusText}`); + throw new Error( + `${i18n.t("feedback.error")}: ${res.statusText}` + ); } const json = await res.json(); @@ -142,7 +155,9 @@ function FeedbackForm(props: { onSubmitted: () => void }) { props.onSubmitted(); } else { throw new Error( - "Error submitting feedback. Please try again later." + `${i18n.t("feedback.error")}. ${i18n.t( + "feedback.try_again" + )}` ); } } catch (e) { @@ -158,7 +173,7 @@ function FeedbackForm(props: { onSubmitted: () => void }) { {(field, props) => ( void }) { {...props} value={field.value} error={field.error} - placeholder="Bugs, feature requests, feedback, etc." + placeholder={i18n.t( + "feedback.feedback_placeholder" + )} /> )} @@ -174,8 +191,8 @@ function FeedbackForm(props: { onSubmitted: () => void }) { {(field, _props) => ( setValue(feedbackForm, "include_contact", c) } @@ -210,7 +227,7 @@ function FeedbackForm(props: { onSubmitted: () => void }) { {(field, props) => ( @@ -218,7 +235,7 @@ function FeedbackForm(props: { onSubmitted: () => void }) { {...props} value={field.value} error={field.error} - label="Nostr npub or NIP-05" + label={i18n.t("feedback.nostr_label")} placeholder="npub..." /> )} @@ -234,10 +251,8 @@ function FeedbackForm(props: { onSubmitted: () => void }) { {(field, props) => ( @@ -246,7 +261,7 @@ function FeedbackForm(props: { onSubmitted: () => void }) { value={field.value} error={field.error} type="email" - label="Email" + label={i18n.t("feedback.email")} placeholder="email@nokycemail.com" /> )} @@ -267,7 +282,7 @@ function FeedbackForm(props: { onSubmitted: () => void }) { intent="blue" type="submit" > - Send Feedback + {i18n.t("feedback.send_feedback")} @@ -275,6 +290,7 @@ function FeedbackForm(props: { onSubmitted: () => void }) { } export default function Feedback() { + const i18n = useI18n(); const [submitted, setSubmitted] = createSignal(false); const location = useLocation(); @@ -292,35 +308,30 @@ export default function Feedback() {
    - Feedback received! + {i18n.t("feedback.received")} - - Thank you for letting us know what's going on. - + {i18n.t("feedback.thanks")} - Go Home + {i18n.t("common.home")}
    - Give us feedback! + {i18n.t("feedback.header")} + {i18n.t("feedback.tracking")} - Mutiny doesn't track or spy on your behavior, so - your feedback is incredibly helpful. - - - If you're comfortable with GitHub you can also{" "} + {i18n.t("feedback.github_one")}{" "} - create an issue + {i18n.t("feedback.create_issue")} - . + {i18n.t("feedback.github_two")} setSubmitted(true)} /> diff --git a/src/routes/Scanner.tsx b/src/routes/Scanner.tsx index 00c56ba..b8bc1bb 100644 --- a/src/routes/Scanner.tsx +++ b/src/routes/Scanner.tsx @@ -7,8 +7,10 @@ import { useMegaStore } from "~/state/megaStore"; import { toParsedParams } from "~/logic/waila"; import { Clipboard } from "@capacitor/clipboard"; import { Capacitor } from "@capacitor/core"; +import { useI18n } from "~/i18n/context"; export default function Scanner() { + const i18n = useI18n(); const [state, actions] = useMegaStore(); const [scanResult, setScanResult] = createSignal(); const navigate = useNavigate(); @@ -70,9 +72,9 @@ export default function Scanner() {
    - +
    diff --git a/src/routes/Send.tsx b/src/routes/Send.tsx index 7a41d8b..f44b619 100644 --- a/src/routes/Send.tsx +++ b/src/routes/Send.tsx @@ -574,15 +574,15 @@ export default function Send() { > clearAll()} - title={`${i18n.t("send.start_over")}`} + title={i18n.t("send.start_over")} /> {i18n.t("send_bitcoin")} { diff --git a/src/routes/Swap.tsx b/src/routes/Swap.tsx index 9aca99d..6c57323 100644 --- a/src/routes/Swap.tsx +++ b/src/routes/Swap.tsx @@ -113,7 +113,7 @@ export default function Swap() { if (peer) { setSelectedPeer(peer.pubkey); } else { - showToast(new Error("Peer not found")); + showToast(new Error(i18n.t("swap.peer_not_found"))); } } catch (e) { showToast(eify(e)); @@ -198,11 +198,11 @@ export default function Swap() { const network = state.mutiny_wallet?.get_network() as Network; if (network === "bitcoin" && amountSats() < 50000n) { - return "It's just silly to make a channel smaller than 50,000 sats"; + return i18n.t("swap.channel_too_small", { amount: "50,000" }); } if (amountSats() < 10000n) { - return "It's just silly to make a channel smaller than 10,000 sats"; + return i18n.t("swap.channel_too_small", { amount: "10,000" }); } if ( @@ -211,7 +211,7 @@ export default function Swap() { (state.balance?.unconfirmed || 0n) || !feeEstimate() ) { - return "You don't have enough funds to make this channel"; + return i18n.t("swap.insufficient_funds"); } return undefined; @@ -266,10 +266,12 @@ export default function Swap() { - Swap to Lightning + {i18n.t("swap.header")} { @@ -295,14 +297,13 @@ export default function Swap() {

    - Swap Initiated + {i18n.t("swap.initiated")}

    + {channelOpenResult()?.channel?.balance.toLocaleString() ?? "0"}{" "} - sats will be added to your Lightning - balance + {i18n.t("swap.sats_added")}

    - Use existing peer + {i18n.t("swap.use_existing")}