translation pass 2

This commit is contained in:
benalleng
2023-07-21 13:45:27 -04:00
committed by Paul Miller
parent 43647cb403
commit 7c9c992084
29 changed files with 573 additions and 228 deletions

View File

@@ -37,14 +37,14 @@
"workbox-window": "^6.6.0" "workbox-window": "^6.6.0"
}, },
"dependencies": { "dependencies": {
"@mutinywallet/barcode-scanner": "5.0.0-beta.3",
"@capacitor/android": "^5.2.1", "@capacitor/android": "^5.2.1",
"@capacitor/clipboard": "^5.0.6", "@capacitor/clipboard": "^5.0.6",
"@capacitor/core": "^5.2.1", "@capacitor/core": "^5.2.1",
"@kobalte/core": "^0.9.8", "@kobalte/core": "^0.9.8",
"@kobalte/tailwindcss": "^0.5.0", "@kobalte/tailwindcss": "^0.5.0",
"@mutinywallet/mutiny-wasm": "0.4.4",
"@modular-forms/solid": "^0.18.0", "@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", "@mutinywallet/waila-wasm": "^0.2.1",
"@solid-primitives/upload": "^0.0.111", "@solid-primitives/upload": "^0.0.111",
"@solidjs/meta": "^0.28.5", "@solidjs/meta": "^0.28.5",

View File

@@ -3,6 +3,7 @@ import { Card, VStack } from "~/components/layout";
import { useMegaStore } from "~/state/megaStore"; import { useMegaStore } from "~/state/megaStore";
import { satsToUsd } from "~/utils/conversions"; import { satsToUsd } from "~/utils/conversions";
import { AmountEditable } from "./AmountEditable"; import { AmountEditable } from "./AmountEditable";
import { useI18n } from "~/i18n/context";
const noop = () => { const noop = () => {
// do nothing // do nothing
@@ -25,6 +26,7 @@ export const InlineAmount: ParentComponent<{
sign?: string; sign?: string;
fiat?: boolean; fiat?: boolean;
}> = (props) => { }> = (props) => {
const i18n = useI18n();
const prettyPrint = createMemo(() => { const prettyPrint = createMemo(() => {
const parsed = Number(props.amount); const parsed = Number(props.amount);
if (isNaN(parsed)) { if (isNaN(parsed)) {
@@ -39,12 +41,15 @@ export const InlineAmount: ParentComponent<{
{props.sign ? `${props.sign} ` : ""} {props.sign ? `${props.sign} ` : ""}
{props.fiat ? "$" : ""} {props.fiat ? "$" : ""}
{prettyPrint()}{" "} {prettyPrint()}{" "}
<span class="text-sm">{props.fiat ? "USD" : "SATS"}</span> <span class="text-sm">
{props.fiat ? i18n.t("common.usd") : i18n.t("common.sats")}
</span>
</div> </div>
); );
}; };
function USDShower(props: { amountSats: string; fee?: string }) { function USDShower(props: { amountSats: string; fee?: string }) {
const i18n = useI18n();
const [state, _] = useMegaStore(); const [state, _] = useMegaStore();
const amountInUsd = () => const amountInUsd = () =>
satsToUsd(state.price, add(props.amountSats, props.fee), true); satsToUsd(state.price, add(props.amountSats, props.fee), true);
@@ -54,7 +59,7 @@ function USDShower(props: { amountSats: string; fee?: string }) {
<KeyValue gray key=""> <KeyValue gray key="">
<div class="self-end"> <div class="self-end">
~{amountInUsd()}&nbsp; ~{amountInUsd()}&nbsp;
<span class="text-sm">USD</span> <span class="text-sm">{i18n.t("common.usd")}</span>
</div> </div>
</KeyValue> </KeyValue>
</Show> </Show>

View File

@@ -69,13 +69,19 @@ function SingleDigitButton(props: {
onClear: () => void; onClear: () => void;
fiat: boolean; fiat: boolean;
}) { }) {
const i18n = useI18n();
let holdTimer: number; let holdTimer: number;
const holdThreshold = 500; const holdThreshold = 500;
function onHold() { function onHold() {
holdTimer = setTimeout(() => { if (
props.onClear(); props.character === "DEL" ||
}, holdThreshold); props.character === i18n.t("char.del")
) {
holdTimer = setTimeout(() => {
props.onClear();
}, holdThreshold);
}
} }
function endHold() { function endHold() {
@@ -130,7 +136,7 @@ function BigScalingText(props: { text: string; fiat: boolean }) {
> >
{props.text}&nbsp; {props.text}&nbsp;
<span class="text-xl"> <span class="text-xl">
{props.fiat ? "USD" : `${i18n.t("common.sats")}`} {props.fiat ? i18n.t("common.usd") : i18n.t("common.sats")}
</span> </span>
</h1> </h1>
); );
@@ -142,7 +148,7 @@ function SmallSubtleAmount(props: { text: string; fiat: boolean }) {
<h2 class="flex flex-row items-end text-xl font-light text-neutral-400"> <h2 class="flex flex-row items-end text-xl font-light text-neutral-400">
~{props.text}&nbsp; ~{props.text}&nbsp;
<span class="text-base"> <span class="text-base">
{props.fiat ? "USD" : `${i18n.t("common.sats")}`} {props.fiat ? i18n.t("common.usd") : `${i18n.t("common.sats")}`}
</span> </span>
<img <img
class={"pl-[4px] pb-[4px] hover:cursor-pointer"} class={"pl-[4px] pb-[4px] hover:cursor-pointer"}
@@ -214,7 +220,7 @@ export const AmountEditable: ParentComponent<{
"9", "9",
".", ".",
"0", "0",
`${i18n.t("char.del")}` i18n.t("char.del")
]; ];
const displaySats = () => toDisplayHandleNaN(localSats(), false); const displaySats = () => toDisplayHandleNaN(localSats(), false);
@@ -243,9 +249,13 @@ export const AmountEditable: ParentComponent<{
if ((state.balance?.lightning || 0n) === 0n) { if ((state.balance?.lightning || 0n) === 0n) {
const network = state.mutiny_wallet?.get_network() as Network; const network = state.mutiny_wallet?.get_network() as Network;
if (network === "bitcoin") { 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 { } 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)) { 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; return undefined;
@@ -269,10 +279,10 @@ export const AmountEditable: ParentComponent<{
if (parsed >= 2099999997690000) { if (parsed >= 2099999997690000) {
// If over 21 million bitcoin, warn that too much // 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) { } else if (parsed >= 4000000) {
// If over 4 million sats, warn that it's a beta bro // 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; let sane;
if (character === "DEL") { if (character === "DEL" || character === i18n.t("char.del")) {
if (localValue().length <= 1) { if (localValue().length <= 1) {
sane = "0"; sane = "0";
} else { } else {
@@ -424,7 +434,7 @@ export const AmountEditable: ParentComponent<{
when={localSats() !== "0"} when={localSats() !== "0"}
fallback={ fallback={
<div class="inline-block font-semibold"> <div class="inline-block font-semibold">
{i18n.t("set_amount")} {i18n.t("receive.amount_editable.set_amount")}
</div> </div>
} }
> >
@@ -540,7 +550,7 @@ export const AmountEditable: ParentComponent<{
}} }}
class="py-2 px-4 rounded-lg bg-white/10" class="py-2 px-4 rounded-lg bg-white/10"
> >
MAX {i18n.t("receive.amount_editable.max")}
</button> </button>
</Show> </Show>
</div> </div>
@@ -561,7 +571,7 @@ export const AmountEditable: ParentComponent<{
class="w-full flex-none" class="w-full flex-none"
onClick={handleSubmit} onClick={handleSubmit}
> >
{i18n.t("set_amount")} {i18n.t("receive.amount_editable.set_amount")}
</Button> </Button>
</div> </div>
</Dialog.Content> </Dialog.Content>

View File

@@ -3,10 +3,12 @@ import { createSignal } from "solid-js";
import { ConfirmDialog } from "~/components/Dialog"; import { ConfirmDialog } from "~/components/Dialog";
import { Button } from "~/components/layout"; import { Button } from "~/components/layout";
import { showToast } from "~/components/Toaster"; import { showToast } from "~/components/Toaster";
import { useI18n } from "~/i18n/context";
import { useMegaStore } from "~/state/megaStore"; import { useMegaStore } from "~/state/megaStore";
import eify from "~/utils/eify"; import eify from "~/utils/eify";
export function DeleteEverything(props: { emergency?: boolean }) { export function DeleteEverything(props: { emergency?: boolean }) {
const i18n = useI18n();
const [state, actions] = useMegaStore(); const [state, actions] = useMegaStore();
async function confirmReset() { async function confirmReset() {
@@ -34,7 +36,14 @@ export function DeleteEverything(props: { emergency?: boolean }) {
await MutinyWallet.import_json("{}"); 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(() => { setTimeout(() => {
window.location.href = "/"; window.location.href = "/";
@@ -50,14 +59,16 @@ export function DeleteEverything(props: { emergency?: boolean }) {
return ( return (
<> <>
<Button onClick={confirmReset}>Delete Everything</Button> <Button onClick={confirmReset}>
{i18n.t("settings.emergency_kit.delete_everything.delete")}
</Button>
<ConfirmDialog <ConfirmDialog
loading={confirmLoading()} loading={confirmLoading()}
open={confirmOpen()} open={confirmOpen()}
onConfirm={resetNode} onConfirm={resetNode}
onCancel={() => setConfirmOpen(false)} onCancel={() => setConfirmOpen(false)}
> >
This will delete your node's state. This can't be undone! {i18n.t("settings.emergency_kit.delete_everything.confirm")}
</ConfirmDialog> </ConfirmDialog>
</> </>
); );

View File

@@ -15,8 +15,10 @@ import { ConfirmDialog } from "./Dialog";
import initMutinyWallet, { MutinyWallet } from "@mutinywallet/mutiny-wasm"; import initMutinyWallet, { MutinyWallet } from "@mutinywallet/mutiny-wasm";
import { InfoBox } from "./InfoBox"; import { InfoBox } from "./InfoBox";
import { TextField } from "./layout/TextField"; import { TextField } from "./layout/TextField";
import { useI18n } from "~/i18n/context";
export function ImportExport(props: { emergency?: boolean }) { export function ImportExport(props: { emergency?: boolean }) {
const i18n = useI18n();
const [state, _] = useMegaStore(); const [state, _] = useMegaStore();
const [error, setError] = createSignal<Error>(); const [error, setError] = createSignal<Error>();
@@ -44,7 +46,11 @@ export function ImportExport(props: { emergency?: boolean }) {
try { try {
setError(undefined); setError(undefined);
if (!password()) { 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()); const json = await MutinyWallet.export_json(password());
downloadTextFile(json || "", "mutiny-state.json"); downloadTextFile(json || "", "mutiny-state.json");
@@ -77,11 +83,23 @@ export function ImportExport(props: { emergency?: boolean }) {
if (result) { if (result) {
resolve(result); resolve(result);
} else { } 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) => 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"); fileReader.readAsText(file, "UTF-8");
}); });
@@ -130,25 +148,35 @@ export function ImportExport(props: { emergency?: boolean }) {
return ( return (
<> <>
<InnerCard title="Export wallet state"> <InnerCard
title={i18n.t("settings.emergency_kit.import_export.title")}
>
<NiceP> <NiceP>
You can export your entire Mutiny Wallet state to a file and {i18n.t("settings.emergency_kit.import_export.tip")}
import it into a new browser. It usually works!
</NiceP> </NiceP>
<NiceP> <NiceP>
<strong>Important caveats:</strong> after exporting don't do <strong>
any operations in the original browser. If you do, you'll {i18n.t(
need to export again. After a successful import, a best "settings.emergency_kit.import_export.caveat_header"
practice is to clear the state of the original browser just )}
to make sure you don't create conflicts. </strong>{" "}
{i18n.t("settings.emergency_kit.import_export.caveat")}
</NiceP> </NiceP>
<div /> <div />
<Show when={error()}> <Show when={error()}>
<InfoBox accent="red">{error()?.message}</InfoBox> <InfoBox accent="red">{error()?.message}</InfoBox>
</Show> </Show>
<VStack> <VStack>
<Button onClick={handleSave}>Save State As File</Button> <Button onClick={handleSave}>
<Button onClick={uploadFile}>Import State From File</Button> {i18n.t(
"settings.emergency_kit.import_export.save_state"
)}
</Button>
<Button onClick={uploadFile}>
{i18n.t(
"settings.emergency_kit.import_export.import_state"
)}
</Button>
</VStack> </VStack>
</InnerCard> </InnerCard>
<ConfirmDialog <ConfirmDialog
@@ -157,11 +185,14 @@ export function ImportExport(props: { emergency?: boolean }) {
onConfirm={importJson} onConfirm={importJson}
onCancel={() => setConfirmOpen(false)} onCancel={() => 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}?
</ConfirmDialog> </ConfirmDialog>
{/* TODO: this is pretty redundant with the DecryptDialog, could make a shared component */} {/* TODO: this is pretty redundant with the DecryptDialog, could make a shared component */}
<SimpleDialog <SimpleDialog
title="Enter your password to decrypt" title={i18n.t(
"settings.emergency_kit.import_export.confirm_replace"
)}
open={exportDecrypt()} open={exportDecrypt()}
> >
<form onSubmit={savePassword}> <form onSubmit={savePassword}>
@@ -180,7 +211,9 @@ export function ImportExport(props: { emergency?: boolean }) {
<InfoBox accent="red">{error()?.message}</InfoBox> <InfoBox accent="red">{error()?.message}</InfoBox>
</Show> </Show>
<Button intent="blue" onClick={savePassword}> <Button intent="blue" onClick={savePassword}>
Decrypt Wallet {i18n.t(
"settings.emergency_kit.import_export.decrypt_wallet"
)}
</Button> </Button>
</div> </div>
</form> </form>

View File

@@ -1,8 +1,10 @@
import { MutinyWallet } from "@mutinywallet/mutiny-wasm"; import { MutinyWallet } from "@mutinywallet/mutiny-wasm";
import { Button, InnerCard, NiceP, VStack } from "~/components/layout"; import { Button, InnerCard, NiceP, VStack } from "~/components/layout";
import { useI18n } from "~/i18n/context";
import { downloadTextFile } from "~/utils/download"; import { downloadTextFile } from "~/utils/download";
export function Logs() { export function Logs() {
const i18n = useI18n();
async function handleSave() { async function handleSave() {
try { try {
const logs = await MutinyWallet.get_logs(); const logs = await MutinyWallet.get_logs();
@@ -18,11 +20,13 @@ export function Logs() {
} }
return ( return (
<InnerCard title="Download debug logs"> <InnerCard title={i18n.t("settings.emergency_kit.logs.title")}>
<VStack> <VStack>
<NiceP>Something screwy going on? Check out the logs!</NiceP> <NiceP>
{i18n.t("settings.emergency_kit.logs.something_screwy")}
</NiceP>
<Button intent="green" onClick={handleSave}> <Button intent="green" onClick={handleSave}>
Download Logs {i18n.t("settings.emergency_kit.logs.download_logs")}
</Button> </Button>
</VStack> </VStack>
</InnerCard> </InnerCard>

View File

@@ -15,7 +15,7 @@ export function FeesModal(props: { icon?: boolean }) {
props.icon ? ( props.icon ? (
<img src={help} alt="help" class="w-4 h-4 cursor-pointer" /> <img src={help} alt="help" class="w-4 h-4 cursor-pointer" />
) : ( ) : (
i18n.t("why?") i18n.t("why")
) )
} }
> >

View File

@@ -1,5 +1,12 @@
import { onCleanup, onMount } from "solid-js"; 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"; import QrScanner from "qr-scanner";
export default function Scanner(props: { onResult: (result: string) => void }) { 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 () => { const startScan = async () => {
// Check camera permission // Check camera permission
const permissions: PermissionStates = await BarcodeScanner.checkPermissions(); const permissions: PermissionStates =
await BarcodeScanner.checkPermissions();
if (permissions.camera === "granted") { if (permissions.camera === "granted") {
const callback = (result: ScanResult, err?: any) => { const callback = (result: ScanResult, err?: any) => {
if (err) { if (err) {
@@ -24,10 +32,14 @@ export default function Scanner(props: { onResult: (result: string) => void }) {
handleResult({ data: result.content }); // pass the raw scanned content 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") { } else if (permissions.camera === "prompt") {
// Request permission if it has not been asked before // 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 (requestedPermissions.camera === "granted") {
// If user grants permission, start the scan // If user grants permission, start the scan
await startScan(); await startScan();
@@ -35,7 +47,7 @@ export default function Scanner(props: { onResult: (result: string) => void }) {
} else if (permissions.camera === "denied") { } else if (permissions.camera === "denied") {
// Handle the scenario when user denies the permission // Handle the scenario when user denies the permission
// Maybe show a user friendly message here // Maybe show a user friendly message here
console.log('Camera permission was denied'); console.log("Camera permission was denied");
} }
}; };

View File

@@ -1,11 +1,13 @@
import { For, Match, Switch, createMemo, createSignal } from "solid-js"; import { For, Match, Switch, createMemo, createSignal } from "solid-js";
import { useCopy } from "~/utils/useCopy"; import { useCopy } from "~/utils/useCopy";
import copyIcon from "~/assets/icons/copy.svg"; import copyIcon from "~/assets/icons/copy.svg";
import { useI18n } from "~/i18n/context";
export function SeedWords(props: { export function SeedWords(props: {
words: string; words: string;
setHasSeen?: (hasSeen: boolean) => void; setHasSeen?: (hasSeen: boolean) => void;
}) { }) {
const i18n = useI18n();
const [shouldShow, setShouldShow] = createSignal(false); const [shouldShow, setShouldShow] = createSignal(false);
const [copy, copied] = useCopy({ copiedTimeout: 1000 }); const [copy, copied] = useCopy({ copiedTimeout: 1000 });
@@ -30,7 +32,9 @@ export function SeedWords(props: {
class="cursor-pointer flex w-full justify-center" class="cursor-pointer flex w-full justify-center"
onClick={toggleShow} onClick={toggleShow}
> >
<code class="text-red">TAP TO REVEAL SEED WORDS</code> <code class="text-red">
{i18n.t("settings.backup.seed_words.reveal")}
</code>
</div> </div>
</Match> </Match>
@@ -40,7 +44,9 @@ export function SeedWords(props: {
class="cursor-pointer flex w-full justify-center" class="cursor-pointer flex w-full justify-center"
onClick={toggleShow} onClick={toggleShow}
> >
<code class="text-red">HIDE</code> <code class="text-red">
{i18n.t("settings.backup.seed_words.hide")}
</code>
</div> </div>
<ol class="overflow-hidden columns-2 w-full list-decimal list-inside"> <ol class="overflow-hidden columns-2 w-full list-decimal list-inside">
<For each={splitWords()}> <For each={splitWords()}>
@@ -59,8 +65,12 @@ export function SeedWords(props: {
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span> <span>
{copied() {copied()
? "Copied!" ? i18n.t(
: "Dangerously Copy to Clipboard"} "settings.backup.seed_words.copied"
)
: i18n.t(
"settings.backup.seed_words.copy"
)}
</span> </span>
<img <img
src={copyIcon} src={copyIcon}

View File

@@ -1,10 +1,12 @@
import { Back } from "~/assets/svg/Back"; import { Back } from "~/assets/svg/Back";
import { useI18n } from "~/i18n/context";
export function BackButton(props: { export function BackButton(props: {
onClick: () => void; onClick: () => void;
title?: string; title?: string;
showOnDesktop?: boolean; showOnDesktop?: boolean;
}) { }) {
const i18n = useI18n();
return ( return (
<button <button
onClick={() => props.onClick()} onClick={() => props.onClick()}
@@ -12,7 +14,7 @@ export function BackButton(props: {
classList={{ "md:!flex": props.showOnDesktop }} classList={{ "md:!flex": props.showOnDesktop }}
> >
<Back /> <Back />
{props.title ? props.title : "Home"} {props.title ? props.title : i18n.t("common.home")}
</button> </button>
); );
} }

View File

@@ -1,14 +1,16 @@
import { A } from "solid-start"; import { A } from "solid-start";
import { Back } from "~/assets/svg/Back"; import { Back } from "~/assets/svg/Back";
import { useI18n } from "~/i18n/context";
export function BackLink(props: { href?: string; title?: string }) { export function BackLink(props: { href?: string; title?: string }) {
const i18n = useI18n();
return ( return (
<A <A
href={props.href ? props.href : "/"} href={props.href ? props.href : "/"}
class="text-m-red active:text-m-red/80 text-xl font-semibold no-underline md:hidden flex items-center" class="text-m-red active:text-m-red/80 text-xl font-semibold no-underline md:hidden flex items-center"
> >
<Back /> <Back />
{props.title ? props.title : "Home"} {props.title ? props.title : i18n.t("common.home")}
</A> </A>
); );
} }

View File

@@ -1,11 +1,13 @@
import { useLocation, useNavigate } from "solid-start"; import { useLocation, useNavigate } from "solid-start";
import { BackButton } from "./BackButton"; import { BackButton } from "./BackButton";
import { useI18n } from "~/i18n/context";
type StateWithPrevious = { type StateWithPrevious = {
previous?: string; previous?: string;
}; };
export function BackPop() { export function BackPop() {
const i18n = useI18n();
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
@@ -15,7 +17,7 @@ export function BackPop() {
return ( return (
<BackButton <BackButton
title="Back" title={i18n.t("common.back")}
onClick={() => navigate(backPath())} onClick={() => navigate(backPath())}
showOnDesktop showOnDesktop
/> />

View File

@@ -1,5 +1,6 @@
import { Progress } from "@kobalte/core"; import { Progress } from "@kobalte/core";
import { SmallHeader } from "."; import { SmallHeader } from ".";
import { useI18n } from "~/i18n/context";
export default function formatNumber(num: number) { export default function formatNumber(num: number) {
const map = [ const map = [
@@ -21,19 +22,24 @@ export default function formatNumber(num: number) {
} }
export function ProgressBar(props: { value: number; max: number }) { export function ProgressBar(props: { value: number; max: number }) {
const i18n = useI18n();
return ( return (
<Progress.Root <Progress.Root
value={props.value} value={props.value}
minValue={0} minValue={0}
maxValue={props.max} maxValue={props.max}
getValueLabel={({ value, max }) => getValueLabel={({ value, max }) =>
`${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" class="w-full flex flex-col gap-2"
> >
<div class="flex justify-between"> <div class="flex justify-between">
<Progress.Label> <Progress.Label>
<SmallHeader>Sending...</SmallHeader> <SmallHeader>{i18n.t("send.sending")}</SmallHeader>
</Progress.Label> </Progress.Label>
<Progress.ValueLabel class="text-sm font-semibold uppercase" /> <Progress.ValueLabel class="text-sm font-semibold uppercase" />
</div> </div>

View File

@@ -25,6 +25,7 @@ import { A } from "solid-start";
import down from "~/assets/icons/down.svg"; import down from "~/assets/icons/down.svg";
import { DecryptDialog } from "../DecryptDialog"; import { DecryptDialog } from "../DecryptDialog";
import { LoadingIndicator } from "~/components/LoadingIndicator"; import { LoadingIndicator } from "~/components/LoadingIndicator";
import { useI18n } from "~/i18n/context";
export { Button, ButtonLink, Linkify }; export { Button, ButtonLink, Linkify };
@@ -138,6 +139,7 @@ export const DefaultMain: ParentComponent = (props) => {
}; };
export const FullscreenLoader = () => { export const FullscreenLoader = () => {
const i18n = useI18n();
const [waitedTooLong, setWaitedTooLong] = createSignal(false); const [waitedTooLong, setWaitedTooLong] = createSignal(false);
setTimeout(() => { setTimeout(() => {
@@ -149,10 +151,9 @@ export const FullscreenLoader = () => {
<LoadingSpinner wide /> <LoadingSpinner wide />
<Show when={waitedTooLong()}> <Show when={waitedTooLong()}>
<p class="max-w-[20rem] text-neutral-400"> <p class="max-w-[20rem] text-neutral-400">
Stuck on this screen? Try reloading. If that doesn't work, {i18n.t("error.load_time.stuck")}{" "}
check out the{" "}
<A class="text-white" href="/emergencykit"> <A class="text-white" href="/emergencykit">
emergency kit. {i18n.t("error.load_time.emergency_link")}
</A> </A>
</p> </p>
</Show> </Show>

View File

@@ -5,27 +5,32 @@ import LanguageDetector from "i18next-browser-languagedetector";
import en from "~/i18n/en/translations"; import en from "~/i18n/en/translations";
import pt from "~/i18n/pt/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( const i18n = use(LanguageDetector).init(
{ {
returnNull: false,
fallbackLng: "en", fallbackLng: "en",
preload: ["en"], preload: ["en"],
load: "languageOnly", load: "languageOnly",
ns: ["translations"], ns: ["translations"],
defaultNS: "translations", defaultNS: defaultNS,
fallbackNS: false, fallbackNS: false,
debug: true, debug: true,
detection: { detection: {
order: ["querystring", "navigator", "htmlTag"], order: ["querystring", "navigator", "htmlTag"],
lookupQuerystring: "lang" lookupQuerystring: "lang"
}, },
resources: { resources: resources
en: {
translations: en
},
pt: {
translations: pt
}
}
// FIXME: this doesn't work when deployed // FIXME: this doesn't work when deployed
// backend: { // backend: {
// loadPath: 'src/i18n/{{lng}}/{{ns}}.json', // loadPath: 'src/i18n/{{lng}}/{{ns}}.json',

View File

@@ -7,7 +7,9 @@ export default {
usd: "USD", usd: "USD",
fee: "Fee", fee: "Fee",
send: "Send", send: "Send",
receive: "Receive" receive: "Receive",
dangit: "Dangit",
back: "Back"
}, },
char: { char: {
del: "DEL" del: "DEL"
@@ -19,13 +21,55 @@ export default {
choose_format: "Choose format", choose_format: "Choose format",
payment_received: "Payment Received", payment_received: "Payment Received",
payment_initiated: "Payment Initiated", 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: { send: {
sending: "Sending...", sending: "Sending...",
confirm_send: "Confirm Send", confirm_send: "Confirm Send",
contact_placeholder: "Add the receiver for your records", 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: { activity: {
view_all: "View all", view_all: "View all",
@@ -34,25 +78,159 @@ export default {
channel_close: "Channel Close", channel_close: "Channel Close",
unknown: "Unknown" 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", create_an_issue: "Create an issue",
feedback: "Bugs? Feedback?",
send_bitcoin: "Send Bitcoin", send_bitcoin: "Send Bitcoin",
view_transaction: "View Transaction", view_transaction: "View Transaction",
amount_editable_first_payment_10k_or_greater: why: "Why?",
"Your first lightning receive needs to be 10,000 sats or greater. A setup fee will be deducted from the requested amount.",
"why?": "Why?",
more_info_modal_p1: 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.", "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: 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.", "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", learn_more_about_liquidity: "Learn more about liquidity",
set_amount: "Set amount",
whats_with_the_fees: "What's with the fees?", whats_with_the_fees: "What's with the fees?",
private_tags: "Private tags", private_tags: "Private tags",
continue: "Continue", continue: "Continue",
keep_mutiny_open: "Keep Mutiny open to complete the payment.", 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.",
}; };

9
src/i18n/i18next.d.ts vendored Normal file
View File

@@ -0,0 +1,9 @@
import { resources, defaultNS } from "~/i18n/config";
declare module "i18next" {
interface CustomTypeOptions {
defaultNS: typeof defaultNS;
resources: (typeof resources)["en"];
returnNull: false;
}
}

View File

@@ -27,10 +27,12 @@ import feedback from "~/assets/icons/feedback.svg";
import { InfoBox } from "~/components/InfoBox"; import { InfoBox } from "~/components/InfoBox";
import eify from "~/utils/eify"; import eify from "~/utils/eify";
import { MegaCheck } from "~/components/successfail/MegaCheck"; import { MegaCheck } from "~/components/successfail/MegaCheck";
import { useI18n } from "~/i18n/context";
const FEEDBACK_API = import.meta.env.VITE_FEEDBACK; const FEEDBACK_API = import.meta.env.VITE_FEEDBACK;
export function FeedbackLink(props: { setupError?: boolean }) { export function FeedbackLink(props: { setupError?: boolean }) {
const i18n = useI18n();
const location = useLocation(); const location = useLocation();
return ( return (
<A <A
@@ -43,7 +45,7 @@ export function FeedbackLink(props: { setupError?: boolean }) {
}} }}
href="/feedback" href="/feedback"
> >
Feedback? {i18n.t("feedback.link")}
<img src={feedback} class="h-5 w-5" alt="Feedback" /> <img src={feedback} class="h-5 w-5" alt="Feedback" />
</A> </A>
); );
@@ -58,11 +60,6 @@ type FeedbackForm = {
images: File[]; 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) { async function formDataFromFeedbackForm(f: FeedbackForm) {
const formData = new FormData(); const formData = new FormData();
@@ -107,9 +104,23 @@ async function formDataFromFeedbackForm(f: FeedbackForm) {
} }
function FeedbackForm(props: { onSubmitted: () => void }) { function FeedbackForm(props: { onSubmitted: () => void }) {
const i18n = useI18n();
const [loading, setLoading] = createSignal(false); const [loading, setLoading] = createSignal(false);
const [error, setError] = createSignal<Error>(); const [error, setError] = createSignal<Error>();
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<FeedbackForm>({ const [feedbackForm, { Form, Field }] = createForm<FeedbackForm>({
initialValues: { initialValues: {
user_type: "nostr", user_type: "nostr",
@@ -133,7 +144,9 @@ function FeedbackForm(props: { onSubmitted: () => void }) {
}); });
if (!res.ok) { 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(); const json = await res.json();
@@ -142,7 +155,9 @@ function FeedbackForm(props: { onSubmitted: () => void }) {
props.onSubmitted(); props.onSubmitted();
} else { } else {
throw new Error( throw new Error(
"Error submitting feedback. Please try again later." `${i18n.t("feedback.error")}. ${i18n.t(
"feedback.try_again"
)}`
); );
} }
} catch (e) { } catch (e) {
@@ -158,7 +173,7 @@ function FeedbackForm(props: { onSubmitted: () => void }) {
<VStack> <VStack>
<Field <Field
name="feedback" name="feedback"
validate={[required("Please say something!")]} validate={[required(i18n.t("feedback.invalid_feedback"))]}
> >
{(field, props) => ( {(field, props) => (
<TextField <TextField
@@ -166,7 +181,9 @@ function FeedbackForm(props: { onSubmitted: () => void }) {
{...props} {...props}
value={field.value} value={field.value}
error={field.error} error={field.error}
placeholder="Bugs, feature requests, feedback, etc." placeholder={i18n.t(
"feedback.feedback_placeholder"
)}
/> />
)} )}
</Field> </Field>
@@ -174,8 +191,8 @@ function FeedbackForm(props: { onSubmitted: () => void }) {
{(field, _props) => ( {(field, _props) => (
<Checkbox <Checkbox
checked={field.value || false} checked={field.value || false}
label="Include contact info" label={i18n.t("feedback.info_label")}
caption="If you need us to follow-up on this issue" caption={i18n.t("feedback.info_caption")}
onChange={(c) => onChange={(c) =>
setValue(feedbackForm, "include_contact", c) setValue(feedbackForm, "include_contact", c)
} }
@@ -210,7 +227,7 @@ function FeedbackForm(props: { onSubmitted: () => void }) {
<Field <Field
name="id" name="id"
validate={[ validate={[
required("We need some way to contact you") required(i18n.t("feedback.need_contact"))
]} ]}
> >
{(field, props) => ( {(field, props) => (
@@ -218,7 +235,7 @@ function FeedbackForm(props: { onSubmitted: () => void }) {
{...props} {...props}
value={field.value} value={field.value}
error={field.error} error={field.error}
label="Nostr npub or NIP-05" label={i18n.t("feedback.nostr_label")}
placeholder="npub..." placeholder="npub..."
/> />
)} )}
@@ -234,10 +251,8 @@ function FeedbackForm(props: { onSubmitted: () => void }) {
<Field <Field
name="id" name="id"
validate={[ validate={[
required("We need some way to contact you"), required(i18n.t("feedback.need_contact")),
email( email(i18n.t("feedback.invalid_email"))
"That doesn't look like an email address to me"
)
]} ]}
> >
{(field, props) => ( {(field, props) => (
@@ -246,7 +261,7 @@ function FeedbackForm(props: { onSubmitted: () => void }) {
value={field.value} value={field.value}
error={field.error} error={field.error}
type="email" type="email"
label="Email" label={i18n.t("feedback.email")}
placeholder="email@nokycemail.com" placeholder="email@nokycemail.com"
/> />
)} )}
@@ -267,7 +282,7 @@ function FeedbackForm(props: { onSubmitted: () => void }) {
intent="blue" intent="blue"
type="submit" type="submit"
> >
Send Feedback {i18n.t("feedback.send_feedback")}
</Button> </Button>
</VStack> </VStack>
</Form> </Form>
@@ -275,6 +290,7 @@ function FeedbackForm(props: { onSubmitted: () => void }) {
} }
export default function Feedback() { export default function Feedback() {
const i18n = useI18n();
const [submitted, setSubmitted] = createSignal(false); const [submitted, setSubmitted] = createSignal(false);
const location = useLocation(); const location = useLocation();
@@ -292,35 +308,30 @@ export default function Feedback() {
<div class="flex flex-col gap-4 items-center h-full"> <div class="flex flex-col gap-4 items-center h-full">
<MegaCheck /> <MegaCheck />
<LargeHeader centered> <LargeHeader centered>
Feedback received! {i18n.t("feedback.received")}
</LargeHeader> </LargeHeader>
<NiceP> <NiceP>{i18n.t("feedback.thanks")}</NiceP>
Thank you for letting us know what's going on.
</NiceP>
<ButtonLink intent="blue" href="/" layout="full"> <ButtonLink intent="blue" href="/" layout="full">
Go Home {i18n.t("common.home")}
</ButtonLink> </ButtonLink>
<Button <Button
intent="text" intent="text"
layout="full" layout="full"
onClick={() => setSubmitted(false)} onClick={() => setSubmitted(false)}
> >
Got more to say? {i18n.t("feedback.more")}
</Button> </Button>
</div> </div>
</Match> </Match>
<Match when={true}> <Match when={true}>
<LargeHeader>Give us feedback!</LargeHeader> <LargeHeader>{i18n.t("feedback.header")}</LargeHeader>
<NiceP>{i18n.t("feedback.tracking")}</NiceP>
<NiceP> <NiceP>
Mutiny doesn't track or spy on your behavior, so {i18n.t("feedback.github_one")}{" "}
your feedback is incredibly helpful.
</NiceP>
<NiceP>
If you're comfortable with GitHub you can also{" "}
<ExternalLink href="https://github.com/MutinyWallet/mutiny-web/issues"> <ExternalLink href="https://github.com/MutinyWallet/mutiny-web/issues">
create an issue {i18n.t("feedback.create_issue")}
</ExternalLink> </ExternalLink>
. {i18n.t("feedback.github_two")}
</NiceP> </NiceP>
<FeedbackForm onSubmitted={() => setSubmitted(true)} /> <FeedbackForm onSubmitted={() => setSubmitted(true)} />
</Match> </Match>

View File

@@ -7,8 +7,10 @@ import { useMegaStore } from "~/state/megaStore";
import { toParsedParams } from "~/logic/waila"; import { toParsedParams } from "~/logic/waila";
import { Clipboard } from "@capacitor/clipboard"; import { Clipboard } from "@capacitor/clipboard";
import { Capacitor } from "@capacitor/core"; import { Capacitor } from "@capacitor/core";
import { useI18n } from "~/i18n/context";
export default function Scanner() { export default function Scanner() {
const i18n = useI18n();
const [state, actions] = useMegaStore(); const [state, actions] = useMegaStore();
const [scanResult, setScanResult] = createSignal<string>(); const [scanResult, setScanResult] = createSignal<string>();
const navigate = useNavigate(); const navigate = useNavigate();
@@ -70,9 +72,9 @@ export default function Scanner() {
<div class="w-full flex flex-col items-center fixed bottom-[2rem] gap-8 px-8"> <div class="w-full flex flex-col items-center fixed bottom-[2rem] gap-8 px-8">
<div class="w-full max-w-[800px] flex flex-col gap-2"> <div class="w-full max-w-[800px] flex flex-col gap-2">
<Button intent="blue" onClick={handlePaste}> <Button intent="blue" onClick={handlePaste}>
Paste Something {i18n.t("scanner.paste")}
</Button> </Button>
<Button onClick={exit}>Cancel</Button> <Button onClick={exit}>{i18n.t("scanner.cancel")}</Button>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -574,15 +574,15 @@ export default function Send() {
> >
<BackButton <BackButton
onClick={() => clearAll()} onClick={() => clearAll()}
title={`${i18n.t("send.start_over")}`} title={i18n.t("send.start_over")}
/> />
</Show> </Show>
<LargeHeader>{i18n.t("send_bitcoin")}</LargeHeader> <LargeHeader>{i18n.t("send_bitcoin")}</LargeHeader>
<SuccessModal <SuccessModal
confirmText={ confirmText={
sentDetails()?.amount sentDetails()?.amount
? `${i18n.t("send.nice")}` ? i18n.t("common.nice")
: `${i18n.t("send.home")}` : i18n.t("common.home")
} }
open={!!sentDetails()} open={!!sentDetails()}
setOpen={(open: boolean) => { setOpen={(open: boolean) => {

View File

@@ -113,7 +113,7 @@ export default function Swap() {
if (peer) { if (peer) {
setSelectedPeer(peer.pubkey); setSelectedPeer(peer.pubkey);
} else { } else {
showToast(new Error("Peer not found")); showToast(new Error(i18n.t("swap.peer_not_found")));
} }
} catch (e) { } catch (e) {
showToast(eify(e)); showToast(eify(e));
@@ -198,11 +198,11 @@ export default function Swap() {
const network = state.mutiny_wallet?.get_network() as Network; const network = state.mutiny_wallet?.get_network() as Network;
if (network === "bitcoin" && amountSats() < 50000n) { 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) { 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 ( if (
@@ -211,7 +211,7 @@ export default function Swap() {
(state.balance?.unconfirmed || 0n) || (state.balance?.unconfirmed || 0n) ||
!feeEstimate() !feeEstimate()
) { ) {
return "You don't have enough funds to make this channel"; return i18n.t("swap.insufficient_funds");
} }
return undefined; return undefined;
@@ -266,10 +266,12 @@ export default function Swap() {
<SafeArea> <SafeArea>
<DefaultMain> <DefaultMain>
<BackLink /> <BackLink />
<LargeHeader>Swap to Lightning</LargeHeader> <LargeHeader>{i18n.t("swap.header")}</LargeHeader>
<SuccessModal <SuccessModal
confirmText={ confirmText={
channelOpenResult()?.channel ? "Nice" : "Home" channelOpenResult()?.channel
? i18n.t("common.nice")
: i18n.t("common.home")
} }
open={!!channelOpenResult()} open={!!channelOpenResult()}
setOpen={(open: boolean) => { setOpen={(open: boolean) => {
@@ -295,14 +297,13 @@ export default function Swap() {
<MegaCheck /> <MegaCheck />
<div class="flex flex-col justify-center"> <div class="flex flex-col justify-center">
<h1 class="w-full mt-4 mb-2 justify-center text-2xl font-semibold text-center md:text-3xl"> <h1 class="w-full mt-4 mb-2 justify-center text-2xl font-semibold text-center md:text-3xl">
Swap Initiated {i18n.t("swap.initiated")}
</h1> </h1>
<p class="text-xl text-center"> <p class="text-xl text-center">
+ +
{channelOpenResult()?.channel?.balance.toLocaleString() ?? {channelOpenResult()?.channel?.balance.toLocaleString() ??
"0"}{" "} "0"}{" "}
sats will be added to your Lightning {i18n.t("swap.sats_added")}
balance
</p> </p>
<AmountFiat <AmountFiat
amountSats={ amountSats={
@@ -348,7 +349,7 @@ export default function Swap() {
for="peerselect" for="peerselect"
class="uppercase font-semibold text-sm" class="uppercase font-semibold text-sm"
> >
Use existing peer {i18n.t("swap.use_existing")}
</label> </label>
<select <select
name="peerselect" name="peerselect"
@@ -361,7 +362,7 @@ export default function Swap() {
class="" class=""
selected selected
> >
Choose a peer {i18n.t("swap.choose_peer")}
</option> </option>
<For each={peers()}> <For each={peers()}>
{(peer) => ( {(peer) => (
@@ -389,8 +390,12 @@ export default function Swap() {
{...props} {...props}
value={field.value} value={field.value}
error={field.error} error={field.error}
label="Connect to new peer" label={i18n.t(
placeholder="Peer connect string" "swap.peer_connect_label"
)}
placeholder={i18n.t(
"swap.peer_connect_placeholder"
)}
/> />
)} )}
</Field> </Field>
@@ -400,8 +405,12 @@ export default function Swap() {
disabled={isConnecting()} disabled={isConnecting()}
> >
{isConnecting() {isConnecting()
? "Connecting..." ? i18n.t(
: "Connect"} "swap.connecting"
)
: i18n.t(
"swap.connect"
)}
</Button> </Button>
</Form> </Form>
</Show> </Show>
@@ -429,7 +438,7 @@ export default function Swap() {
onClick={handleSwap} onClick={handleSwap}
loading={loading()} loading={loading()}
> >
{"Confirm Swap"} {i18n.t("swap.confirm_swap")}
</Button> </Button>
</DefaultMain> </DefaultMain>
<NavBar activeTab="none" /> <NavBar activeTab="none" />

View File

@@ -6,18 +6,20 @@ import {
LargeHeader, LargeHeader,
SafeArea SafeArea
} from "~/components/layout"; } from "~/components/layout";
import { useI18n } from "~/i18n/context";
export default function NotFound() { export default function NotFound() {
const i18n = useI18n();
return ( return (
<SafeArea> <SafeArea>
<Title>Not Found</Title> <Title>{i18n.t("not_found.title")}</Title>
<HttpStatusCode code={404} /> <HttpStatusCode code={404} />
<DefaultMain> <DefaultMain>
<LargeHeader>Not Found</LargeHeader> <LargeHeader>{i18n.t("not_found.title")}</LargeHeader>
<p>This is probably Paul's fault.</p> <p>{i18n.t("not_found.wtf_paul")}</p>
<div class="h-full" /> <div class="h-full" />
<ButtonLink href="/" intent="red"> <ButtonLink href="/" intent="red">
Dangit {i18n.t("common.dangit")}
</ButtonLink> </ButtonLink>
</DefaultMain> </DefaultMain>
</SafeArea> </SafeArea>

View File

@@ -11,26 +11,27 @@ import {
VStack VStack
} from "~/components/layout"; } from "~/components/layout";
import { BackLink } from "~/components/layout/BackLink"; import { BackLink } from "~/components/layout/BackLink";
import { useI18n } from "~/i18n/context";
export default function Admin() { export default function Admin() {
const i18n = useI18n();
return ( return (
<MutinyWalletGuard> <MutinyWalletGuard>
<SafeArea> <SafeArea>
<DefaultMain> <DefaultMain>
<BackLink href="/settings" title="Settings" /> <BackLink
<LargeHeader>Secret Debug Tools</LargeHeader> href="/settings"
title={i18n.t("settings.header")}
/>
<LargeHeader>{i18n.t("settings.admin.header")}</LargeHeader>
<VStack> <VStack>
<NiceP> <NiceP>{i18n.t("settings.admin.warning_one")}</NiceP>
If you know what you're doing you're in the right <NiceP>{i18n.t("settings.admin.warning_two")}</NiceP>
place.
</NiceP>
<NiceP>
These are internal tools we use to debug and test
the app. Please be careful!
</NiceP>
<KitchenSink /> <KitchenSink />
<div class="rounded-xl p-4 flex flex-col gap-2 bg-m-red overflow-x-hidden"> <div class="rounded-xl p-4 flex flex-col gap-2 bg-m-red overflow-x-hidden">
<SmallHeader>Danger zone</SmallHeader> <SmallHeader>
{i18n.t("settings.danger_zone")}
</SmallHeader>
<DeleteEverything /> <DeleteEverything />
</div> </div>
</VStack> </VStack>

View File

@@ -14,8 +14,10 @@ import { SeedWords } from "~/components/SeedWords";
import { useMegaStore } from "~/state/megaStore"; import { useMegaStore } from "~/state/megaStore";
import { Show, createEffect, createSignal } from "solid-js"; import { Show, createEffect, createSignal } from "solid-js";
import { BackLink } from "~/components/layout/BackLink"; import { BackLink } from "~/components/layout/BackLink";
import { useI18n } from "~/i18n/context";
function Quiz(props: { setHasCheckedAll: (hasChecked: boolean) => void }) { function Quiz(props: { setHasCheckedAll: (hasChecked: boolean) => void }) {
const i18n = useI18n();
const [one, setOne] = createSignal(false); const [one, setOne] = createSignal(false);
const [two, setTwo] = createSignal(false); const [two, setTwo] = createSignal(false);
const [three, setThree] = createSignal(false); const [three, setThree] = createSignal(false);
@@ -33,23 +35,24 @@ function Quiz(props: { setHasCheckedAll: (hasChecked: boolean) => void }) {
<Checkbox <Checkbox
checked={one()} checked={one()}
onChange={setOne} onChange={setOne}
label="I wrote down the words" label={i18n.t("settings.backup.confirm")}
/> />
<Checkbox <Checkbox
checked={two()} checked={two()}
onChange={setTwo} onChange={setTwo}
label="I understand that my funds are my responsibility" label={i18n.t("settings.backup.responsibility")}
/> />
<Checkbox <Checkbox
checked={three()} checked={three()}
onChange={setThree} onChange={setThree}
label="I'm not lying just to get this over with" label={i18n.t("settings.backup.liar")}
/> />
</VStack> </VStack>
); );
} }
export default function Backup() { export default function Backup() {
const i18n = useI18n();
const [store, actions] = useMegaStore(); const [store, actions] = useMegaStore();
const navigate = useNavigate(); const navigate = useNavigate();
@@ -69,23 +72,19 @@ export default function Backup() {
<MutinyWalletGuard> <MutinyWalletGuard>
<SafeArea> <SafeArea>
<DefaultMain> <DefaultMain>
<BackLink href="/settings" title="Settings" /> <BackLink
<LargeHeader>Backup</LargeHeader> href="/settings"
title={i18n.t("settings.header")}
/>
<LargeHeader>{i18n.t("settings.backup.title")}</LargeHeader>
<VStack> <VStack>
<NiceP>Let's get these funds secured.</NiceP> <NiceP>{i18n.t("settings.backup.secure_funds")}</NiceP>
<NiceP> <NiceP>
We'll show you 12 words. You write down the 12 {i18n.t("settings.backup.twelve_words_tip")}
words.
</NiceP>
<NiceP>
If you clear your browser history, or lose your
device, these 12 words are the only way you can
restore your wallet.
</NiceP>
<NiceP>
Mutiny is self-custodial. It's all up to you...
</NiceP> </NiceP>
<NiceP>{i18n.t("settings.backup.warning_one")}</NiceP>
<NiceP>{i18n.t("settings.backup.warning_two")}</NiceP>
<SeedWords <SeedWords
words={store.mutiny_wallet?.show_seed() || ""} words={store.mutiny_wallet?.show_seed() || ""}
setHasSeen={setHasSeenBackup} setHasSeen={setHasSeenBackup}
@@ -99,7 +98,7 @@ export default function Backup() {
onClick={wroteDownTheWords} onClick={wroteDownTheWords}
loading={loading()} loading={loading()}
> >
I wrote down the words {i18n.t("settings.backup.confirm")}
</Button> </Button>
</VStack> </VStack>
</DefaultMain> </DefaultMain>

View File

@@ -14,13 +14,17 @@ import {
import { AmountSmall } from "~/components/Amount"; import { AmountSmall } from "~/components/Amount";
import { BackLink } from "~/components/layout/BackLink"; import { BackLink } from "~/components/layout/BackLink";
import NavBar from "~/components/NavBar"; import NavBar from "~/components/NavBar";
import { useI18n } from "~/i18n/context";
function BalanceBar(props: { inbound: number; outbound: number }) { function BalanceBar(props: { inbound: number; outbound: number }) {
const i18n = useI18n();
return ( return (
<VStack smallgap> <VStack smallgap>
<div class="flex justify-between"> <div class="flex justify-between">
<SmallHeader>Outbound</SmallHeader> <SmallHeader>
<SmallHeader>Inbound</SmallHeader> {i18n.t("settings.channels.outbound")}
</SmallHeader>
<SmallHeader>{i18n.t("settings.channels.inbound")}</SmallHeader>
</div> </div>
<div class="flex gap-1 w-full"> <div class="flex gap-1 w-full">
<div <div
@@ -45,6 +49,7 @@ function BalanceBar(props: { inbound: number; outbound: number }) {
} }
export function LiquidityMonitor() { export function LiquidityMonitor() {
const i18n = useI18n();
const [state, _actions] = useMegaStore(); const [state, _actions] = useMegaStore();
const [channelInfo] = createResource(async () => { const [channelInfo] = createResource(async () => {
@@ -71,41 +76,41 @@ export function LiquidityMonitor() {
<Match when={channelInfo()?.channelCount}> <Match when={channelInfo()?.channelCount}>
<Card> <Card>
<NiceP> <NiceP>
You have {channelInfo()?.channelCount} lightning{" "} {i18n.t("settings.channels.have_channels")}{" "}
{channelInfo()?.channelCount}{" "}
{channelInfo()?.channelCount === 1 {channelInfo()?.channelCount === 1
? "channel" ? i18n.t("settings.channels.have_channels_one")
: "channels"} : i18n.t("settings.channels.have_channels_many")}
.
</NiceP>{" "} </NiceP>{" "}
<BalanceBar <BalanceBar
inbound={Number(channelInfo()?.inbound) || 0} inbound={Number(channelInfo()?.inbound) || 0}
outbound={Number(state.balance?.lightning) || 0} outbound={Number(state.balance?.lightning) || 0}
/> />
<TinyText> <TinyText>
Outbound is the amount of money you can spend on {i18n.t("settings.channels.inbound_outbound_tip")}
lightning. Inbound is the amount you can receive without
incurring a lightning service fee.
</TinyText> </TinyText>
</Card> </Card>
</Match> </Match>
<Match when={true}> <Match when={true}>
<NiceP> <NiceP>{i18n.t("settings.channels.no_channels")}</NiceP>
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!
</NiceP>
</Match> </Match>
</Switch> </Switch>
); );
} }
export default function Channels() { export default function Channels() {
const i18n = useI18n();
return ( return (
<MutinyWalletGuard> <MutinyWalletGuard>
<SafeArea> <SafeArea>
<DefaultMain> <DefaultMain>
<BackLink href="/settings" title="Settings" /> <BackLink
<LargeHeader>Lightning Channels</LargeHeader> href="/settings"
title={i18n.t("settings.header")}
/>
<LargeHeader>
{i18n.t("settings.channels.title")}
</LargeHeader>
<LiquidityMonitor /> <LiquidityMonitor />
</DefaultMain> </DefaultMain>
<NavBar activeTab="settings" /> <NavBar activeTab="settings" />

View File

@@ -21,8 +21,10 @@ import { BackLink } from "~/components/layout/BackLink";
import { TextField } from "~/components/layout/TextField"; import { TextField } from "~/components/layout/TextField";
import { useMegaStore } from "~/state/megaStore"; import { useMegaStore } from "~/state/megaStore";
import eify from "~/utils/eify"; import eify from "~/utils/eify";
import { useI18n } from "~/i18n/context";
function Nwc() { function Nwc() {
const i18n = useI18n();
const [state, _actions] = useMegaStore(); const [state, _actions] = useMegaStore();
const [nwcProfiles, { refetch }] = createResource(async () => { const [nwcProfiles, { refetch }] = createResource(async () => {
@@ -47,7 +49,7 @@ function Nwc() {
setCreateLoading(true); setCreateLoading(true);
if (formName() === "") { if (formName() === "") {
setError("Name cannot be empty"); setError(i18n.t("settings.connections.error_name"));
return; return;
} }
const profile = await state.mutiny_wallet?.create_nwc_profile( const profile = await state.mutiny_wallet?.create_nwc_profile(
@@ -56,7 +58,7 @@ function Nwc() {
); );
if (!profile) { if (!profile) {
setError("Failed to create Wallet Connection"); setError(i18n.t("settings.connections.error_connection"));
return; return;
} else { } else {
refetch(); refetch();
@@ -93,10 +95,12 @@ function Nwc() {
return ( return (
<VStack biggap> <VStack biggap>
<Button intent="blue" onClick={() => setDialogOpen(true)}> <Button intent="blue" onClick={() => setDialogOpen(true)}>
Add Connection {i18n.t("settings.connections.add_connection")}
</Button> </Button>
<Show when={nwcProfiles() && nwcProfiles()!.length > 0}> <Show when={nwcProfiles() && nwcProfiles()!.length > 0}>
<SettingsCard title="Manage Connections"> <SettingsCard
title={i18n.t("settings.connections.manage_connections")}
>
<For each={nwcProfiles()}> <For each={nwcProfiles()}>
{(profile) => ( {(profile) => (
<Collapser <Collapser
@@ -121,7 +125,13 @@ function Nwc() {
layout="small" layout="small"
onClick={() => toggleEnabled(profile)} onClick={() => toggleEnabled(profile)}
> >
{profile.enabled ? "Disable" : "Enable"} {profile.enabled
? i18n.t(
"settings.connections.disable_connection"
)
: i18n.t(
"settings.connections.enable_connection"
)}
</Button> </Button>
</VStack> </VStack>
</Collapser> </Collapser>
@@ -132,19 +142,23 @@ function Nwc() {
<SimpleDialog <SimpleDialog
open={dialogOpen()} open={dialogOpen()}
setOpen={setDialogOpen} setOpen={setDialogOpen}
title="New Connection" title={i18n.t("settings.connections.new_connection")}
> >
<div class="flex flex-col gap-4 py-4"> <div class="flex flex-col gap-4 py-4">
<TextField <TextField
name="name" name="name"
label="Name" label={i18n.t(
"settings.connections.new_connection_label"
)}
ref={noop} ref={noop}
value={formName()} value={formName()}
onInput={(e) => setFormName(e.currentTarget.value)} onInput={(e) => setFormName(e.currentTarget.value)}
error={""} error={""}
onBlur={noop} onBlur={noop}
onChange={noop} onChange={noop}
placeholder="My favorite nostr client..." placeholder={i18n.t(
"settings.connections.new_connection_placeholder"
)}
/> />
<Show when={error()}> <Show when={error()}>
<InfoBox accent="red">{error()}</InfoBox> <InfoBox accent="red">{error()}</InfoBox>
@@ -156,7 +170,7 @@ function Nwc() {
loading={createLoading()} loading={createLoading()}
onClick={createConnection} onClick={createConnection}
> >
Create Connection {i18n.t("settings.connections.create_connection")}
</Button> </Button>
</SimpleDialog> </SimpleDialog>
</VStack> </VStack>
@@ -164,16 +178,19 @@ function Nwc() {
} }
export default function Connections() { export default function Connections() {
const i18n = useI18n();
return ( return (
<MutinyWalletGuard> <MutinyWalletGuard>
<SafeArea> <SafeArea>
<DefaultMain> <DefaultMain>
<BackLink href="/settings" title="Settings" /> <BackLink
<LargeHeader>Wallet Connections</LargeHeader> href="/settings"
<NiceP> title={i18n.t("settings.header")}
Authorize external services to request payments from />
your wallet. Pairs great with Nostr clients. <LargeHeader>
</NiceP> {i18n.t("settings.connections.title")}
</LargeHeader>
<NiceP>{i18n.t("settings.connections.authorize")}</NiceP>
<Nwc /> <Nwc />
<div class="h-full" /> <div class="h-full" />
</DefaultMain> </DefaultMain>

View File

@@ -13,14 +13,16 @@ import {
} from "~/components/layout"; } from "~/components/layout";
import { BackLink } from "~/components/layout/BackLink"; import { BackLink } from "~/components/layout/BackLink";
import { ExternalLink } from "~/components/layout/ExternalLink"; import { ExternalLink } from "~/components/layout/ExternalLink";
import { useI18n } from "~/i18n/context";
function EmergencyStack() { function EmergencyStack() {
const i18n = useI18n();
return ( return (
<VStack> <VStack>
<ImportExport emergency /> <ImportExport emergency />
<Logs /> <Logs />
<div class="rounded-xl p-4 flex flex-col gap-2 bg-m-red overflow-x-hidden"> <div class="rounded-xl p-4 flex flex-col gap-2 bg-m-red overflow-x-hidden">
<SmallHeader>Danger zone</SmallHeader> <SmallHeader>{i18n.t("settings.danger_zone")}</SmallHeader>
<DeleteEverything emergency /> <DeleteEverything emergency />
</div> </div>
</VStack> </VStack>
@@ -28,22 +30,23 @@ function EmergencyStack() {
} }
export default function EmergencyKit() { export default function EmergencyKit() {
const i18n = useI18n();
return ( return (
<SafeArea> <SafeArea>
<DefaultMain> <DefaultMain>
<BackLink href="/settings" title="Settings" /> <BackLink href="/settings" title={i18n.t("settings.header")} />
<LargeHeader>Emergency Kit</LargeHeader> <LargeHeader>
{i18n.t("settings.emergency_kit.title")}
</LargeHeader>
<VStack> <VStack>
<LoadingIndicator /> <LoadingIndicator />
<NiceP> <NiceP>
If your wallet seems broken, here are some tools to try {i18n.t("settings.emergency_kit.emergency_tip")}
to debug and repair it.
</NiceP> </NiceP>
<NiceP> <NiceP>
If you have any questions on what these buttons do, {i18n.t("settings.emergency_kit.questions")}{" "}
please{" "}
<ExternalLink href="https://matrix.to/#/#mutiny-community:lightninghackers.com"> <ExternalLink href="https://matrix.to/#/#mutiny-community:lightninghackers.com">
reach out to us for support. {i18n.t("settings.emergency_kit.link")}
</ExternalLink> </ExternalLink>
</NiceP> </NiceP>
<EmergencyStack /> <EmergencyStack />

View File

@@ -17,6 +17,7 @@ import { TextField } from "~/components/layout/TextField";
import { timeout } from "~/utils/timeout"; import { timeout } from "~/utils/timeout";
import eify from "~/utils/eify"; import eify from "~/utils/eify";
import { InfoBox } from "~/components/InfoBox"; import { InfoBox } from "~/components/InfoBox";
import { useI18n } from "~/i18n/context";
type EncryptPasswordForm = { type EncryptPasswordForm = {
existingPassword: string; existingPassword: string;
@@ -25,6 +26,7 @@ type EncryptPasswordForm = {
}; };
export default function Encrypt() { export default function Encrypt() {
const i18n = useI18n();
const [store, _actions] = useMegaStore(); const [store, _actions] = useMegaStore();
const [error, setError] = createSignal<Error>(); const [error, setError] = createSignal<Error>();
const [loading, setLoading] = createSignal(false); const [loading, setLoading] = createSignal(false);
@@ -66,7 +68,10 @@ export default function Encrypt() {
<MutinyWalletGuard> <MutinyWalletGuard>
<SafeArea> <SafeArea>
<DefaultMain> <DefaultMain>
<BackLink href="/settings" title="Settings" /> <BackLink
href="/settings"
title={i18n.t("settings.header")}
/>
<LargeHeader> <LargeHeader>
Encrypt your seed words (optional) Encrypt your seed words (optional)
</LargeHeader> </LargeHeader>

View File

@@ -10,6 +10,7 @@ import NavBar from "~/components/NavBar";
import { A } from "solid-start"; import { A } from "solid-start";
import { For, Show } from "solid-js"; import { For, Show } from "solid-js";
import forward from "~/assets/icons/forward.svg"; import forward from "~/assets/icons/forward.svg";
import { useI18n } from "~/i18n/context";
function SettingsLinkList(props: { function SettingsLinkList(props: {
header: string; header: string;
@@ -57,36 +58,37 @@ function SettingsLinkList(props: {
} }
export default function Settings() { export default function Settings() {
const i18n = useI18n();
return ( return (
<SafeArea> <SafeArea>
<DefaultMain> <DefaultMain>
<BackLink /> <BackLink />
<LargeHeader>Settings</LargeHeader> <LargeHeader>{i18n.t("settings.header")}</LargeHeader>
<VStack biggap> <VStack biggap>
<SettingsLinkList <SettingsLinkList
header="Mutiny+" header={i18n.t("settings.mutiny_plus")}
links={[ links={[
{ {
href: "/settings/plus", href: "/settings/plus",
text: "Learn how to support Mutiny" text: i18n.t("settings.support")
} }
]} ]}
/> />
<SettingsLinkList <SettingsLinkList
header="General" header={i18n.t("settings.general")}
links={[ links={[
{ {
href: "/settings/channels", href: "/settings/channels",
text: "Lightning Channels" text: i18n.t("settings.channels.title")
}, },
{ {
href: "/settings/backup", href: "/settings/backup",
text: "Backup", text: i18n.t("settings.backup.title"),
accent: "green" accent: "green"
}, },
{ {
href: "/settings/restore", href: "/settings/restore",
text: "Restore", text: i18n.t("settings.restore.title"),
accent: "red" accent: "red"
}, },
// { // {
@@ -99,39 +101,38 @@ export default function Settings() {
// }, // },
{ {
href: "/settings/servers", href: "/settings/servers",
text: "Servers", text: i18n.t("settings.servers.title"),
caption: caption: i18n.t("settings.servers.caption")
"Don't trust us! Use your own servers to back Mutiny."
} }
]} ]}
/> />
<SettingsLinkList <SettingsLinkList
header="Beta Features" header={i18n.t("settings.beta_features")}
links={[ links={[
{ {
href: "/settings/connections", href: "/settings/connections",
text: "Wallet Connections" text: i18n.t("settings.connections.title")
}, },
{ {
href: "/settings/lnurlauth", href: "/settings/lnurlauth",
text: "LNURL Auth" text: i18n.t("settings.lnurl_auth.title")
} }
]} ]}
/> />
<SettingsLinkList <SettingsLinkList
header="Debug Tools" header={i18n.t("settings.debug_tools")}
links={[ links={[
{ {
href: "/settings/emergencykit", href: "/settings/emergencykit",
text: "Emergency Kit", text: i18n.t("settings.emergency_kit.title"),
caption: caption: i18n.t(
"Diagnose and solve problems with your wallet." "settings.emergency_kit.caption"
)
}, },
{ {
href: "/settings/admin", href: "/settings/admin",
text: "Admin Page", text: i18n.t("settings.admin.title"),
caption: caption: i18n.t("settings.admin.caption"),
"Our internal debug tools. Use wisely!",
accent: "red" accent: "red"
} }
]} ]}