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"
},
"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",

View File

@@ -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()}{" "}
<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>
);
};
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 }) {
<KeyValue gray key="">
<div class="self-end">
~{amountInUsd()}&nbsp;
<span class="text-sm">USD</span>
<span class="text-sm">{i18n.t("common.usd")}</span>
</div>
</KeyValue>
</Show>

View File

@@ -69,14 +69,20 @@ function SingleDigitButton(props: {
onClear: () => void;
fiat: boolean;
}) {
const i18n = useI18n();
let holdTimer: number;
const holdThreshold = 500;
function onHold() {
if (
props.character === "DEL" ||
props.character === i18n.t("char.del")
) {
holdTimer = setTimeout(() => {
props.onClear();
}, holdThreshold);
}
}
function endHold() {
clearTimeout(holdTimer);
@@ -130,7 +136,7 @@ function BigScalingText(props: { text: string; fiat: boolean }) {
>
{props.text}&nbsp;
<span class="text-xl">
{props.fiat ? "USD" : `${i18n.t("common.sats")}`}
{props.fiat ? i18n.t("common.usd") : i18n.t("common.sats")}
</span>
</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">
~{props.text}&nbsp;
<span class="text-base">
{props.fiat ? "USD" : `${i18n.t("common.sats")}`}
{props.fiat ? i18n.t("common.usd") : `${i18n.t("common.sats")}`}
</span>
<img
class={"pl-[4px] pb-[4px] hover:cursor-pointer"}
@@ -214,7 +220,7 @@ export const AmountEditable: ParentComponent<{
"9",
".",
"0",
`${i18n.t("char.del")}`
i18n.t("char.del")
];
const displaySats = () => 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={
<div class="inline-block font-semibold">
{i18n.t("set_amount")}
{i18n.t("receive.amount_editable.set_amount")}
</div>
}
>
@@ -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")}
</button>
</Show>
</div>
@@ -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")}
</Button>
</div>
</Dialog.Content>

View File

@@ -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 (
<>
<Button onClick={confirmReset}>Delete Everything</Button>
<Button onClick={confirmReset}>
{i18n.t("settings.emergency_kit.delete_everything.delete")}
</Button>
<ConfirmDialog
loading={confirmLoading()}
open={confirmOpen()}
onConfirm={resetNode}
onCancel={() => setConfirmOpen(false)}
>
This will delete your node's state. This can't be undone!
{i18n.t("settings.emergency_kit.delete_everything.confirm")}
</ConfirmDialog>
</>
);

View File

@@ -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<Error>();
@@ -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 (
<>
<InnerCard title="Export wallet state">
<InnerCard
title={i18n.t("settings.emergency_kit.import_export.title")}
>
<NiceP>
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")}
</NiceP>
<NiceP>
<strong>Important caveats:</strong> 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.
<strong>
{i18n.t(
"settings.emergency_kit.import_export.caveat_header"
)}
</strong>{" "}
{i18n.t("settings.emergency_kit.import_export.caveat")}
</NiceP>
<div />
<Show when={error()}>
<InfoBox accent="red">{error()?.message}</InfoBox>
</Show>
<VStack>
<Button onClick={handleSave}>Save State As File</Button>
<Button onClick={uploadFile}>Import State From File</Button>
<Button onClick={handleSave}>
{i18n.t(
"settings.emergency_kit.import_export.save_state"
)}
</Button>
<Button onClick={uploadFile}>
{i18n.t(
"settings.emergency_kit.import_export.import_state"
)}
</Button>
</VStack>
</InnerCard>
<ConfirmDialog
@@ -157,11 +185,14 @@ export function ImportExport(props: { emergency?: boolean }) {
onConfirm={importJson}
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>
{/* TODO: this is pretty redundant with the DecryptDialog, could make a shared component */}
<SimpleDialog
title="Enter your password to decrypt"
title={i18n.t(
"settings.emergency_kit.import_export.confirm_replace"
)}
open={exportDecrypt()}
>
<form onSubmit={savePassword}>
@@ -180,7 +211,9 @@ export function ImportExport(props: { emergency?: boolean }) {
<InfoBox accent="red">{error()?.message}</InfoBox>
</Show>
<Button intent="blue" onClick={savePassword}>
Decrypt Wallet
{i18n.t(
"settings.emergency_kit.import_export.decrypt_wallet"
)}
</Button>
</div>
</form>

View File

@@ -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 (
<InnerCard title="Download debug logs">
<InnerCard title={i18n.t("settings.emergency_kit.logs.title")}>
<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}>
Download Logs
{i18n.t("settings.emergency_kit.logs.download_logs")}
</Button>
</VStack>
</InnerCard>

View File

@@ -15,7 +15,7 @@ export function FeesModal(props: { icon?: boolean }) {
props.icon ? (
<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 { 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");
}
};

View File

@@ -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}
>
<code class="text-red">TAP TO REVEAL SEED WORDS</code>
<code class="text-red">
{i18n.t("settings.backup.seed_words.reveal")}
</code>
</div>
</Match>
@@ -40,7 +44,9 @@ export function SeedWords(props: {
class="cursor-pointer flex w-full justify-center"
onClick={toggleShow}
>
<code class="text-red">HIDE</code>
<code class="text-red">
{i18n.t("settings.backup.seed_words.hide")}
</code>
</div>
<ol class="overflow-hidden columns-2 w-full list-decimal list-inside">
<For each={splitWords()}>
@@ -59,8 +65,12 @@ export function SeedWords(props: {
<div class="flex items-center gap-2">
<span>
{copied()
? "Copied!"
: "Dangerously Copy to Clipboard"}
? i18n.t(
"settings.backup.seed_words.copied"
)
: i18n.t(
"settings.backup.seed_words.copy"
)}
</span>
<img
src={copyIcon}

View File

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

View File

@@ -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 (
<A
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"
>
<Back />
{props.title ? props.title : "Home"}
{props.title ? props.title : i18n.t("common.home")}
</A>
);
}

View File

@@ -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 (
<BackButton
title="Back"
title={i18n.t("common.back")}
onClick={() => navigate(backPath())}
showOnDesktop
/>

View File

@@ -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 (
<Progress.Root
value={props.value}
minValue={0}
maxValue={props.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"
>
<div class="flex justify-between">
<Progress.Label>
<SmallHeader>Sending...</SmallHeader>
<SmallHeader>{i18n.t("send.sending")}</SmallHeader>
</Progress.Label>
<Progress.ValueLabel class="text-sm font-semibold uppercase" />
</div>

View File

@@ -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 = () => {
<LoadingSpinner wide />
<Show when={waitedTooLong()}>
<p class="max-w-[20rem] text-neutral-400">
Stuck on this screen? Try reloading. If that doesn't work,
check out the{" "}
{i18n.t("error.load_time.stuck")}{" "}
<A class="text-white" href="/emergencykit">
emergency kit.
{i18n.t("error.load_time.emergency_link")}
</A>
</p>
</Show>

View File

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

View File

@@ -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."
};

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 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 (
<A
@@ -43,7 +45,7 @@ export function FeedbackLink(props: { setupError?: boolean }) {
}}
href="/feedback"
>
Feedback?
{i18n.t("feedback.link")}
<img src={feedback} class="h-5 w-5" alt="Feedback" />
</A>
);
@@ -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<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>({
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 }) {
<VStack>
<Field
name="feedback"
validate={[required("Please say something!")]}
validate={[required(i18n.t("feedback.invalid_feedback"))]}
>
{(field, props) => (
<TextField
@@ -166,7 +181,9 @@ function FeedbackForm(props: { onSubmitted: () => void }) {
{...props}
value={field.value}
error={field.error}
placeholder="Bugs, feature requests, feedback, etc."
placeholder={i18n.t(
"feedback.feedback_placeholder"
)}
/>
)}
</Field>
@@ -174,8 +191,8 @@ function FeedbackForm(props: { onSubmitted: () => void }) {
{(field, _props) => (
<Checkbox
checked={field.value || false}
label="Include contact info"
caption="If you need us to follow-up on this issue"
label={i18n.t("feedback.info_label")}
caption={i18n.t("feedback.info_caption")}
onChange={(c) =>
setValue(feedbackForm, "include_contact", c)
}
@@ -210,7 +227,7 @@ function FeedbackForm(props: { onSubmitted: () => void }) {
<Field
name="id"
validate={[
required("We need some way to contact you")
required(i18n.t("feedback.need_contact"))
]}
>
{(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
name="id"
validate={[
required("We need some way to contact you"),
email(
"That doesn't look like an email address to me"
)
required(i18n.t("feedback.need_contact")),
email(i18n.t("feedback.invalid_email"))
]}
>
{(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")}
</Button>
</VStack>
</Form>
@@ -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() {
<div class="flex flex-col gap-4 items-center h-full">
<MegaCheck />
<LargeHeader centered>
Feedback received!
{i18n.t("feedback.received")}
</LargeHeader>
<NiceP>
Thank you for letting us know what's going on.
</NiceP>
<NiceP>{i18n.t("feedback.thanks")}</NiceP>
<ButtonLink intent="blue" href="/" layout="full">
Go Home
{i18n.t("common.home")}
</ButtonLink>
<Button
intent="text"
layout="full"
onClick={() => setSubmitted(false)}
>
Got more to say?
{i18n.t("feedback.more")}
</Button>
</div>
</Match>
<Match when={true}>
<LargeHeader>Give us feedback!</LargeHeader>
<LargeHeader>{i18n.t("feedback.header")}</LargeHeader>
<NiceP>{i18n.t("feedback.tracking")}</NiceP>
<NiceP>
Mutiny doesn't track or spy on your behavior, so
your feedback is incredibly helpful.
</NiceP>
<NiceP>
If you're comfortable with GitHub you can also{" "}
{i18n.t("feedback.github_one")}{" "}
<ExternalLink href="https://github.com/MutinyWallet/mutiny-web/issues">
create an issue
{i18n.t("feedback.create_issue")}
</ExternalLink>
.
{i18n.t("feedback.github_two")}
</NiceP>
<FeedbackForm onSubmitted={() => setSubmitted(true)} />
</Match>

View File

@@ -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<string>();
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 max-w-[800px] flex flex-col gap-2">
<Button intent="blue" onClick={handlePaste}>
Paste Something
{i18n.t("scanner.paste")}
</Button>
<Button onClick={exit}>Cancel</Button>
<Button onClick={exit}>{i18n.t("scanner.cancel")}</Button>
</div>
</div>
</div>

View File

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

View File

@@ -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() {
<SafeArea>
<DefaultMain>
<BackLink />
<LargeHeader>Swap to Lightning</LargeHeader>
<LargeHeader>{i18n.t("swap.header")}</LargeHeader>
<SuccessModal
confirmText={
channelOpenResult()?.channel ? "Nice" : "Home"
channelOpenResult()?.channel
? i18n.t("common.nice")
: i18n.t("common.home")
}
open={!!channelOpenResult()}
setOpen={(open: boolean) => {
@@ -295,14 +297,13 @@ export default function Swap() {
<MegaCheck />
<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">
Swap Initiated
{i18n.t("swap.initiated")}
</h1>
<p class="text-xl text-center">
+
{channelOpenResult()?.channel?.balance.toLocaleString() ??
"0"}{" "}
sats will be added to your Lightning
balance
{i18n.t("swap.sats_added")}
</p>
<AmountFiat
amountSats={
@@ -348,7 +349,7 @@ export default function Swap() {
for="peerselect"
class="uppercase font-semibold text-sm"
>
Use existing peer
{i18n.t("swap.use_existing")}
</label>
<select
name="peerselect"
@@ -361,7 +362,7 @@ export default function Swap() {
class=""
selected
>
Choose a peer
{i18n.t("swap.choose_peer")}
</option>
<For each={peers()}>
{(peer) => (
@@ -389,8 +390,12 @@ export default function Swap() {
{...props}
value={field.value}
error={field.error}
label="Connect to new peer"
placeholder="Peer connect string"
label={i18n.t(
"swap.peer_connect_label"
)}
placeholder={i18n.t(
"swap.peer_connect_placeholder"
)}
/>
)}
</Field>
@@ -400,8 +405,12 @@ export default function Swap() {
disabled={isConnecting()}
>
{isConnecting()
? "Connecting..."
: "Connect"}
? i18n.t(
"swap.connecting"
)
: i18n.t(
"swap.connect"
)}
</Button>
</Form>
</Show>
@@ -429,7 +438,7 @@ export default function Swap() {
onClick={handleSwap}
loading={loading()}
>
{"Confirm Swap"}
{i18n.t("swap.confirm_swap")}
</Button>
</DefaultMain>
<NavBar activeTab="none" />

View File

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

View File

@@ -11,26 +11,27 @@ import {
VStack
} from "~/components/layout";
import { BackLink } from "~/components/layout/BackLink";
import { useI18n } from "~/i18n/context";
export default function Admin() {
const i18n = useI18n();
return (
<MutinyWalletGuard>
<SafeArea>
<DefaultMain>
<BackLink href="/settings" title="Settings" />
<LargeHeader>Secret Debug Tools</LargeHeader>
<BackLink
href="/settings"
title={i18n.t("settings.header")}
/>
<LargeHeader>{i18n.t("settings.admin.header")}</LargeHeader>
<VStack>
<NiceP>
If you know what you're doing you're in the right
place.
</NiceP>
<NiceP>
These are internal tools we use to debug and test
the app. Please be careful!
</NiceP>
<NiceP>{i18n.t("settings.admin.warning_one")}</NiceP>
<NiceP>{i18n.t("settings.admin.warning_two")}</NiceP>
<KitchenSink />
<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 />
</div>
</VStack>

View File

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

View File

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

View File

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

View File

@@ -13,14 +13,16 @@ import {
} from "~/components/layout";
import { BackLink } from "~/components/layout/BackLink";
import { ExternalLink } from "~/components/layout/ExternalLink";
import { useI18n } from "~/i18n/context";
function EmergencyStack() {
const i18n = useI18n();
return (
<VStack>
<ImportExport emergency />
<Logs />
<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 />
</div>
</VStack>
@@ -28,22 +30,23 @@ function EmergencyStack() {
}
export default function EmergencyKit() {
const i18n = useI18n();
return (
<SafeArea>
<DefaultMain>
<BackLink href="/settings" title="Settings" />
<LargeHeader>Emergency Kit</LargeHeader>
<BackLink href="/settings" title={i18n.t("settings.header")} />
<LargeHeader>
{i18n.t("settings.emergency_kit.title")}
</LargeHeader>
<VStack>
<LoadingIndicator />
<NiceP>
If your wallet seems broken, here are some tools to try
to debug and repair it.
{i18n.t("settings.emergency_kit.emergency_tip")}
</NiceP>
<NiceP>
If you have any questions on what these buttons do,
please{" "}
{i18n.t("settings.emergency_kit.questions")}{" "}
<ExternalLink href="https://matrix.to/#/#mutiny-community:lightninghackers.com">
reach out to us for support.
{i18n.t("settings.emergency_kit.link")}
</ExternalLink>
</NiceP>
<EmergencyStack />

View File

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

View File

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