feat: third pass at translations

This commit is contained in:
benalleng
2023-07-28 17:42:59 -04:00
committed by Paul Miller
parent 11ce48414f
commit 12a650b63e
43 changed files with 1568 additions and 448 deletions

View File

@@ -177,7 +177,9 @@ export function ActivityItem(props: {
</Switch> </Switch>
<Switch> <Switch>
<Match when={props.date && props.date > 2147483647}> <Match when={props.date && props.date > 2147483647}>
<time class="text-sm text-neutral-500">Pending</time> <time class="text-sm text-neutral-500">
{i18n.t("common.pending")}
</time>
</Match> </Match>
<Match when={true}> <Match when={true}>
<time class="text-sm text-neutral-500"> <time class="text-sm text-neutral-500">

View File

@@ -81,6 +81,7 @@ export function AmountCard(props: {
exitRoute?: string; exitRoute?: string;
maxAmountSats?: bigint; maxAmountSats?: bigint;
}) { }) {
const i18n = useI18n();
// Normally we want to add the fee to the amount, but for max amount we just show the max // Normally we want to add the fee to the amount, but for max amount we just show the max
const totalOrTotalLessFee = () => { const totalOrTotalLessFee = () => {
if ( if (
@@ -99,7 +100,7 @@ export function AmountCard(props: {
<Switch> <Switch>
<Match when={props.fee}> <Match when={props.fee}>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<KeyValue key="Amount"> <KeyValue key={i18n.t("receive.amount")}>
<Show <Show
when={props.isAmountEditable} when={props.isAmountEditable}
fallback={ fallback={
@@ -123,13 +124,13 @@ export function AmountCard(props: {
/> />
</Show> </Show>
</KeyValue> </KeyValue>
<KeyValue gray key="+ Fee"> <KeyValue gray key={i18n.t("receive.fee")}>
<InlineAmount amount={props.fee || "0"} /> <InlineAmount amount={props.fee || "0"} />
</KeyValue> </KeyValue>
</div> </div>
<hr class="border-white/20" /> <hr class="border-white/20" />
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<KeyValue key="Total"> <KeyValue key={i18n.t("receive.total")}>
<InlineAmount amount={totalOrTotalLessFee()} /> <InlineAmount amount={totalOrTotalLessFee()} />
</KeyValue> </KeyValue>
<USDShower <USDShower
@@ -140,7 +141,7 @@ export function AmountCard(props: {
</Match> </Match>
<Match when={props.reserve}> <Match when={props.reserve}>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<KeyValue key="Channel size"> <KeyValue key={i18n.t("receive.channel_size")}>
<InlineAmount <InlineAmount
amount={add( amount={add(
props.amountSats, props.amountSats,
@@ -148,13 +149,16 @@ export function AmountCard(props: {
).toString()} ).toString()}
/> />
</KeyValue> </KeyValue>
<KeyValue gray key="- Channel Reserve"> <KeyValue
gray
key={i18n.t("receive.channel_reserve")}
>
<InlineAmount amount={props.reserve || "0"} /> <InlineAmount amount={props.reserve || "0"} />
</KeyValue> </KeyValue>
</div> </div>
<hr class="border-white/20" /> <hr class="border-white/20" />
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<KeyValue key="Spendable"> <KeyValue key={i18n.t("receive.spendable")}>
<InlineAmount amount={props.amountSats} /> <InlineAmount amount={props.amountSats} />
</KeyValue> </KeyValue>
<USDShower <USDShower
@@ -165,7 +169,7 @@ export function AmountCard(props: {
</Match> </Match>
<Match when={!props.fee && !props.reserve}> <Match when={!props.fee && !props.reserve}>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<KeyValue key="Amount"> <KeyValue key={i18n.t("receive.amount")}>
<Show <Show
when={props.isAmountEditable} when={props.isAmountEditable}
fallback={ fallback={

View File

@@ -24,18 +24,6 @@ import { FeesModal } from "./MoreInfoModal";
import { useI18n } from "~/i18n/context"; import { useI18n } from "~/i18n/context";
import { useNavigate } from "solid-start"; import { useNavigate } from "solid-start";
const FIXED_AMOUNTS_SATS = [
{ label: "10k", amount: "10000" },
{ label: "100k", amount: "100000" },
{ label: "1m", amount: "1000000" }
];
const FIXED_AMOUNTS_USD = [
{ label: "$1", amount: "1" },
{ label: "$10", amount: "10" },
{ label: "$100", amount: "100" }
];
function fiatInputSanitizer(input: string): string { function fiatInputSanitizer(input: string): string {
// Make sure only numbers and a single decimal point are allowed // Make sure only numbers and a single decimal point are allowed
const numeric = input.replace(/[^0-9.]/g, "").replace(/(\..*)\./g, "$1"); const numeric = input.replace(/[^0-9.]/g, "").replace(/(\..*)\./g, "$1");
@@ -76,7 +64,7 @@ function SingleDigitButton(props: {
function onHold() { function onHold() {
if ( if (
props.character === "DEL" || props.character === "DEL" ||
props.character === i18n.t("char.del") props.character === i18n.t("receive.amount_editable.del")
) { ) {
holdTimer = setTimeout(() => { holdTimer = setTimeout(() => {
props.onClear(); props.onClear();
@@ -148,7 +136,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 ? i18n.t("common.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"}
@@ -208,6 +196,28 @@ export const AmountEditable: ParentComponent<{
false false
) )
); );
const FIXED_AMOUNTS_SATS = [
{
label: i18n.t("receive.amount_editable.fix_amounts.ten_k"),
amount: "10000"
},
{
label: i18n.t("receive.amount_editable.fix_amounts.one_hundred_k"),
amount: "100000"
},
{
label: i18n.t("receive.amount_editable.fix_amounts.one_million"),
amount: "1000000"
}
];
const FIXED_AMOUNTS_USD = [
{ label: "$1", amount: "1" },
{ label: "$10", amount: "10" },
{ label: "$100", amount: "100" }
];
const CHARACTERS = [ const CHARACTERS = [
"1", "1",
"2", "2",
@@ -220,7 +230,7 @@ export const AmountEditable: ParentComponent<{
"9", "9",
".", ".",
"0", "0",
i18n.t("char.del") i18n.t("receive.amount_editable.del")
]; ];
const displaySats = () => toDisplayHandleNaN(localSats(), false); const displaySats = () => toDisplayHandleNaN(localSats(), false);
@@ -295,7 +305,10 @@ export const AmountEditable: ParentComponent<{
let sane; let sane;
if (character === "DEL" || character === i18n.t("char.del")) { if (
character === "DEL" ||
character === i18n.t("receive.amount_editable.del")
) {
if (localValue().length <= 1) { if (localValue().length <= 1) {
sane = "0"; sane = "0";
} else { } else {

View File

@@ -15,8 +15,10 @@ import { PendingNwc } from "./PendingNwc";
import { DecryptDialog } from "./DecryptDialog"; import { DecryptDialog } from "./DecryptDialog";
import { LoadingIndicator } from "./LoadingIndicator"; import { LoadingIndicator } from "./LoadingIndicator";
import { FeedbackLink } from "~/routes/Feedback"; import { FeedbackLink } from "~/routes/Feedback";
import { useI18n } from "~/i18n/context";
export default function App() { export default function App() {
const i18n = useI18n();
const [state, _actions] = useMegaStore(); const [state, _actions] = useMegaStore();
return ( return (
@@ -71,7 +73,7 @@ export default function App() {
<PendingNwc /> <PendingNwc />
</Show> </Show>
</Suspense> </Suspense>
<Card title="Activity"> <Card title={i18n.t("activity.title")}>
<div class="p-1" /> <div class="p-1" />
<VStack> <VStack>
<Suspense> <Suspense>

View File

@@ -79,7 +79,9 @@ export default function BalanceBox(props: { loading?: boolean }) {
</div> </div>
<div class="flex flex-col items-end gap-1 justify-between"> <div class="flex flex-col items-end gap-1 justify-between">
<Show when={state.balance?.unconfirmed != 0n}> <Show when={state.balance?.unconfirmed != 0n}>
<Indicator>Pending</Indicator> <Indicator>
{i18n.t("common.pending")}
</Indicator>
</Show> </Show>
<Show when={state.balance?.unconfirmed === 0n}> <Show when={state.balance?.unconfirmed === 0n}>
<div /> <div />

View File

@@ -4,28 +4,26 @@ import { DIALOG_CONTENT, DIALOG_POSITIONER, OVERLAY } from "./DetailsModal";
import { ModalCloseButton, SmallHeader } from "./layout"; import { ModalCloseButton, SmallHeader } from "./layout";
import { ExternalLink } from "./layout/ExternalLink"; import { ExternalLink } from "./layout/ExternalLink";
import { getExistingSettings } from "~/logic/mutinyWalletSetup"; import { getExistingSettings } from "~/logic/mutinyWalletSetup";
import { useI18n } from "~/i18n/context";
export function BetaWarningModal() { export function BetaWarningModal() {
const i18n = useI18n();
return ( return (
<WarningModal title="Warning: beta software" linkText="Why?"> <WarningModal
<p> title={i18n.t("modals.beta_warning.title")}
We're so glad you're here. But we do want to warn you: Mutiny linkText={i18n.t("common.why")}
Wallet is in beta, and there are still bugs and rough edges. >
</p> <p>{i18n.t("translations:modals.beta_warning.beta_warning")}</p>
<p> <p>{i18n.t("modals.beta_warning.be_careful")}</p>
Please be careful and don't put more money into Mutiny than
you're willing to lose.
</p>
<p> <p>
<ExternalLink href="https://github.com/MutinyWallet/mutiny-web/wiki/Mutiny-Beta-Readme"> <ExternalLink href="https://github.com/MutinyWallet/mutiny-web/wiki/Mutiny-Beta-Readme">
Learn more about the beta {i18n.t("modals.beta_warning.beta_link")}
</ExternalLink> </ExternalLink>
</p> </p>
<p class="small text-neutral-400"> <p class="small text-neutral-400">
If you want to use pretend money to test out Mutiny without {i18n.t("modals.beta_warning.pretend_money")}{" "}
risk,{" "}
<ExternalLink href="https://blog.mutinywallet.com/mutiny-wallet-signet-release/"> <ExternalLink href="https://blog.mutinywallet.com/mutiny-wallet-signet-release/">
check out our Signet version. {i18n.t("modals.beta_warning.signet_link")}
</ExternalLink> </ExternalLink>
</p> </p>
</WarningModal> </WarningModal>

View File

@@ -6,11 +6,13 @@ import { SubmitHandler } from "@modular-forms/solid";
import { ContactForm } from "./ContactForm"; import { ContactForm } from "./ContactForm";
import { ContactFormValues } from "./ContactViewer"; import { ContactFormValues } from "./ContactViewer";
import { DIALOG_CONTENT, DIALOG_POSITIONER } from "~/styles/dialogs"; import { DIALOG_CONTENT, DIALOG_POSITIONER } from "~/styles/dialogs";
import { useI18n } from "~/i18n/context";
export function ContactEditor(props: { export function ContactEditor(props: {
createContact: (contact: ContactFormValues) => void; createContact: (contact: ContactFormValues) => void;
list?: boolean; list?: boolean;
}) { }) {
const i18n = useI18n();
const [isOpen, setIsOpen] = createSignal(false); const [isOpen, setIsOpen] = createSignal(false);
// What we're all here for in the first place: returning a value // What we're all here for in the first place: returning a value
@@ -32,12 +34,14 @@ export function ContactEditor(props: {
<div class="bg-neutral-500 flex-none h-16 w-16 rounded-full flex items-center justify-center text-4xl uppercase "> <div class="bg-neutral-500 flex-none h-16 w-16 rounded-full flex items-center justify-center text-4xl uppercase ">
<span class="leading-[4rem]">+</span> <span class="leading-[4rem]">+</span>
</div> </div>
<SmallHeader class="overflow-ellipsis">new</SmallHeader> <SmallHeader class="overflow-ellipsis">
{i18n.t("contacts.new")}
</SmallHeader>
</button> </button>
</Match> </Match>
<Match when={!props.list}> <Match when={!props.list}>
<TinyButton onClick={() => setIsOpen(true)}> <TinyButton onClick={() => setIsOpen(true)}>
+ Add Contact + {i18n.t("contacts.add_contact")}
</TinyButton> </TinyButton>
</Match> </Match>
</Switch> </Switch>
@@ -57,8 +61,8 @@ export function ContactEditor(props: {
</button> </button>
</div> </div>
<ContactForm <ContactForm
title="New contact" title={i18n.t("contacts.new_contact")}
cta="Create contact" cta={i18n.t("contacts.create_contact")}
handleSubmit={handleSubmit} handleSubmit={handleSubmit}
/> />
</Dialog.Content> </Dialog.Content>

View File

@@ -2,6 +2,7 @@ import { SubmitHandler, createForm, required } from "@modular-forms/solid";
import { Button, LargeHeader, VStack } from "~/components/layout"; import { Button, LargeHeader, VStack } from "~/components/layout";
import { TextField } from "~/components/layout/TextField"; import { TextField } from "~/components/layout/TextField";
import { ContactFormValues } from "./ContactViewer"; import { ContactFormValues } from "./ContactViewer";
import { useI18n } from "~/i18n/context";
export function ContactForm(props: { export function ContactForm(props: {
handleSubmit: SubmitHandler<ContactFormValues>; handleSubmit: SubmitHandler<ContactFormValues>;
@@ -9,6 +10,7 @@ export function ContactForm(props: {
title: string; title: string;
cta: string; cta: string;
}) { }) {
const i18n = useI18n();
const [_contactForm, { Form, Field }] = createForm<ContactFormValues>({ const [_contactForm, { Form, Field }] = createForm<ContactFormValues>({
initialValues: props.initialValues initialValues: props.initialValues
}); });
@@ -23,15 +25,15 @@ export function ContactForm(props: {
<VStack> <VStack>
<Field <Field
name="name" name="name"
validate={[required("We at least need a name")]} validate={[required(i18n.t("contacts.error_name"))]}
> >
{(field, props) => ( {(field, props) => (
<TextField <TextField
{...props} {...props}
placeholder="Satoshi" placeholder={i18n.t("contacts.placeholder")}
value={field.value} value={field.value}
error={field.error} error={field.error}
label="Name" label={i18n.t("contacts.name")}
/> />
)} )}
</Field> </Field>

View File

@@ -7,6 +7,7 @@ import { ContactForm } from "./ContactForm";
import { showToast } from "./Toaster"; import { showToast } from "./Toaster";
import { Contact } from "@mutinywallet/mutiny-wasm"; import { Contact } from "@mutinywallet/mutiny-wasm";
import { DIALOG_CONTENT, DIALOG_POSITIONER } from "~/styles/dialogs"; import { DIALOG_CONTENT, DIALOG_POSITIONER } from "~/styles/dialogs";
import { useI18n } from "~/i18n/context";
export type ContactFormValues = { export type ContactFormValues = {
name: string; name: string;
@@ -18,6 +19,7 @@ export function ContactViewer(props: {
gradient: string; gradient: string;
saveContact: (contact: Contact) => void; saveContact: (contact: Contact) => void;
}) { }) {
const i18n = useI18n();
const [isOpen, setIsOpen] = createSignal(false); const [isOpen, setIsOpen] = createSignal(false);
const [isEditing, setIsEditing] = createSignal(false); const [isEditing, setIsEditing] = createSignal(false);
@@ -71,8 +73,8 @@ export function ContactViewer(props: {
<Switch> <Switch>
<Match when={isEditing()}> <Match when={isEditing()}>
<ContactForm <ContactForm
title="Edit contact" title={i18n.t("contacts.edit_contact")}
cta="Save contact" cta={i18n.t("contacts.save_contact")}
handleSubmit={handleSubmit} handleSubmit={handleSubmit}
initialValues={props.contact} initialValues={props.contact}
/> />
@@ -91,9 +93,13 @@ export function ContactViewer(props: {
<h1 class="text-2xl font-semibold uppercase mt-2 mb-4"> <h1 class="text-2xl font-semibold uppercase mt-2 mb-4">
{props.contact.name} {props.contact.name}
</h1> </h1>
<Card title="Payment history"> <Card
title={i18n.t(
"contacts.payment_history"
)}
>
<NiceP> <NiceP>
No payments yet with{" "} {i18n.t("contacts.no_payments")}{" "}
<span class="font-semibold"> <span class="font-semibold">
{props.contact.name} {props.contact.name}
</span> </span>
@@ -107,19 +113,22 @@ export function ContactViewer(props: {
intent="green" intent="green"
onClick={() => setIsEditing(true)} onClick={() => setIsEditing(true)}
> >
Edit {i18n.t("contacts.edit")}
</Button> </Button>
<Button <Button
intent="blue" intent="blue"
onClick={() => { onClick={() => {
showToast({ showToast({
title: "Unimplemented", title: i18n.t(
description: "contacts.unimplemented"
"We don't do that yet" ),
description: i18n.t(
"contacts.not_available"
)
}); });
}} }}
> >
Pay {i18n.t("contacts.pay")}
</Button> </Button>
</div> </div>
</div> </div>

View File

@@ -1,8 +1,10 @@
import { Show } from "solid-js"; import { Show } from "solid-js";
import { QRCodeSVG } from "solid-qr-code"; import { QRCodeSVG } from "solid-qr-code";
import { useI18n } from "~/i18n/context";
import { useCopy } from "~/utils/useCopy"; import { useCopy } from "~/utils/useCopy";
export function CopyableQR(props: { value: string }) { export function CopyableQR(props: { value: string }) {
const i18n = useI18n();
const [copy, copied] = useCopy({ copiedTimeout: 1000 }); const [copy, copied] = useCopy({ copiedTimeout: 1000 });
return ( return (
<div <div
@@ -12,7 +14,7 @@ export function CopyableQR(props: { value: string }) {
> >
<Show when={copied()}> <Show when={copied()}>
<div class="absolute w-full h-full bg-neutral-900/60 z-50 rounded-xl flex flex-col items-center justify-center transition-all"> <div class="absolute w-full h-full bg-neutral-900/60 z-50 rounded-xl flex flex-col items-center justify-center transition-all">
<p class="text-xl font-bold">Copied</p> <p class="text-xl font-bold">{i18n.t("common.copied")}</p>
</div> </div>
</Show> </Show>
<QRCodeSVG <QRCodeSVG

View File

@@ -5,8 +5,10 @@ import { InfoBox } from "~/components/InfoBox";
import { useMegaStore } from "~/state/megaStore"; import { useMegaStore } from "~/state/megaStore";
import eify from "~/utils/eify"; import eify from "~/utils/eify";
import { A } from "solid-start"; import { A } from "solid-start";
import { useI18n } from "~/i18n/context";
export function DecryptDialog() { export function DecryptDialog() {
const i18n = useI18n();
const [state, actions] = useMegaStore(); const [state, actions] = useMegaStore();
const [password, setPassword] = createSignal(""); const [password, setPassword] = createSignal("");
@@ -27,7 +29,7 @@ export function DecryptDialog() {
const err = eify(e); const err = eify(e);
console.error(e); console.error(e);
if (err.message === "wrong") { if (err.message === "wrong") {
setError("Invalid password"); setError(i18n.t("settings.decrypt.error_wrong_password"));
} else { } else {
throw e; throw e;
} }
@@ -42,7 +44,7 @@ export function DecryptDialog() {
return ( return (
<SimpleDialog <SimpleDialog
title="Enter your password" title={i18n.t("settings.decrypt.title")}
// Only show the dialog if we need a password and there's no setup error // Only show the dialog if we need a password and there's no setup error
open={state.needs_password && !state.setup_error} open={state.needs_password && !state.setup_error}
> >
@@ -62,12 +64,12 @@ export function DecryptDialog() {
<InfoBox accent="red">{error()}</InfoBox> <InfoBox accent="red">{error()}</InfoBox>
</Show> </Show>
<Button intent="blue" loading={loading()} onClick={decrypt}> <Button intent="blue" loading={loading()} onClick={decrypt}>
Decrypt Wallet {i18n.t("settings.decrypt.decrypt_wallet")}
</Button> </Button>
</div> </div>
</form> </form>
<A class="self-end text-m-grey-400" href="/settings/restore"> <A class="self-end text-m-grey-400" href="/settings/restore">
Forgot Password? {i18n.t("settings.decrypt.forgot_password_link")}
</A> </A>
</SimpleDialog> </SimpleDialog>
); );

View File

@@ -30,6 +30,7 @@ import { Network } from "~/logic/mutinyWalletSetup";
import { AmountSmall } from "./Amount"; import { AmountSmall } from "./Amount";
import { ExternalLink } from "./layout/ExternalLink"; import { ExternalLink } from "./layout/ExternalLink";
import { InfoBox } from "./InfoBox"; import { InfoBox } from "./InfoBox";
import { useI18n } from "~/i18n/context";
type ChannelClosure = { type ChannelClosure = {
channel_id: string; channel_id: string;
@@ -48,6 +49,7 @@ function LightningHeader(props: {
info: MutinyInvoice; info: MutinyInvoice;
tags: MutinyTagItem[]; tags: MutinyTagItem[];
}) { }) {
const i18n = useI18n();
const [state, _actions] = useMegaStore(); const [state, _actions] = useMegaStore();
return ( return (
@@ -56,7 +58,9 @@ function LightningHeader(props: {
<img src={bolt} alt="lightning bolt" class="w-8 h-8" /> <img src={bolt} alt="lightning bolt" class="w-8 h-8" />
</div> </div>
<h1 class="uppercase font-semibold"> <h1 class="uppercase font-semibold">
{props.info.inbound ? "Lightning receive" : "Lightning send"} {props.info.inbound
? i18n.t("modals.transaction_details.lightning_receive")
: i18n.t("modals.transaction_details.lightning_send")}
</h1> </h1>
<ActivityAmount <ActivityAmount
center center
@@ -85,6 +89,7 @@ function OnchainHeader(props: {
tags: MutinyTagItem[]; tags: MutinyTagItem[];
kind?: HackActivityType; kind?: HackActivityType;
}) { }) {
const i18n = useI18n();
const [state, _actions] = useMegaStore(); const [state, _actions] = useMegaStore();
const isSend = () => { const isSend = () => {
@@ -118,12 +123,12 @@ function OnchainHeader(props: {
</div> </div>
<h1 class="uppercase font-semibold"> <h1 class="uppercase font-semibold">
{props.kind === "ChannelOpen" {props.kind === "ChannelOpen"
? "Channel Open" ? i18n.t("modals.transaction_details.channel_open")
: props.kind === "ChannelClose" : props.kind === "ChannelClose"
? "Channel Close" ? i18n.t("modals.transaction_details.channel_close")
: isSend() : isSend()
? "On-chain send" ? i18n.t("modals.transaction_details.onchain_send")
: "On-chain receive"} : i18n.t("modals.transaction_details.onchain_receive")}
</h1> </h1>
<Show when={props.kind !== "ChannelClose"}> <Show when={props.kind !== "ChannelClose"}>
<ActivityAmount <ActivityAmount
@@ -179,38 +184,45 @@ export function MiniStringShower(props: { text: string }) {
} }
function LightningDetails(props: { info: MutinyInvoice }) { function LightningDetails(props: { info: MutinyInvoice }) {
const i18n = useI18n();
return ( return (
<VStack> <VStack>
<ul class="flex flex-col gap-4"> <ul class="flex flex-col gap-4">
<KeyValue key="Status"> <KeyValue key={i18n.t("modals.transaction_details.status")}>
<span class="text-neutral-300"> <span class="text-neutral-300">
{props.info.paid ? "Paid" : "Unpaid"} {props.info.paid
? i18n.t("modals.transaction_details.paid")
: i18n.t("modals.transaction_details.unpaid")}
</span> </span>
</KeyValue> </KeyValue>
<KeyValue key="When"> <KeyValue key={i18n.t("modals.transaction_details.when")}>
<span class="text-neutral-300"> <span class="text-neutral-300">
{prettyPrintTime(Number(props.info.last_updated))} {prettyPrintTime(Number(props.info.last_updated))}
</span> </span>
</KeyValue> </KeyValue>
<Show when={props.info.description}> <Show when={props.info.description}>
<KeyValue key="Description"> <KeyValue
key={i18n.t("modals.transaction_details.description")}
>
<span class="text-neutral-300 truncate"> <span class="text-neutral-300 truncate">
{props.info.description} {props.info.description}
</span> </span>
</KeyValue> </KeyValue>
</Show> </Show>
<KeyValue key="Fees"> <KeyValue key={i18n.t("modals.transaction_details.fees")}>
<span class="text-neutral-300"> <span class="text-neutral-300">
<AmountSmall amountSats={props.info.fees_paid} /> <AmountSmall amountSats={props.info.fees_paid} />
</span> </span>
</KeyValue> </KeyValue>
<KeyValue key="Bolt11"> <KeyValue key={i18n.t("modals.transaction_details.bolt11")}>
<MiniStringShower text={props.info.bolt11 ?? ""} /> <MiniStringShower text={props.info.bolt11 ?? ""} />
</KeyValue> </KeyValue>
<KeyValue key="Payment Hash"> <KeyValue
key={i18n.t("modals.transaction_details.payment_hash")}
>
<MiniStringShower text={props.info.payment_hash ?? ""} /> <MiniStringShower text={props.info.payment_hash ?? ""} />
</KeyValue> </KeyValue>
<KeyValue key="Preimage"> <KeyValue key={i18n.t("modals.transaction_details.preimage")}>
<MiniStringShower text={props.info.preimage ?? ""} /> <MiniStringShower text={props.info.preimage ?? ""} />
</KeyValue> </KeyValue>
</ul> </ul>
@@ -219,6 +231,7 @@ function LightningDetails(props: { info: MutinyInvoice }) {
} }
function OnchainDetails(props: { info: OnChainTx; kind?: HackActivityType }) { function OnchainDetails(props: { info: OnChainTx; kind?: HackActivityType }) {
const i18n = useI18n();
const [state, _actions] = useMegaStore(); const [state, _actions] = useMegaStore();
const confirmationTime = () => { const confirmationTime = () => {
@@ -251,13 +264,15 @@ function OnchainDetails(props: { info: OnChainTx; kind?: HackActivityType }) {
<VStack> <VStack>
{/* <pre>{JSON.stringify(channelInfo() || "", null, 2)}</pre> */} {/* <pre>{JSON.stringify(channelInfo() || "", null, 2)}</pre> */}
<ul class="flex flex-col gap-4"> <ul class="flex flex-col gap-4">
<KeyValue key="Status"> <KeyValue key={i18n.t("modals.transaction_details.status")}>
<span class="text-neutral-300"> <span class="text-neutral-300">
{confirmationTime() ? "Confirmed" : "Unconfirmed"} {confirmationTime()
? i18n.t("modals.transaction_details.confirmed")
: i18n.t("modals.transaction_details.unconfirmed")}
</span> </span>
</KeyValue> </KeyValue>
<Show when={confirmationTime()}> <Show when={confirmationTime()}>
<KeyValue key="When"> <KeyValue key={i18n.t("modals.transaction_details.when")}>
<span class="text-neutral-300"> <span class="text-neutral-300">
{confirmationTime() {confirmationTime()
? prettyPrintTime(Number(confirmationTime())) ? prettyPrintTime(Number(confirmationTime()))
@@ -266,32 +281,38 @@ function OnchainDetails(props: { info: OnChainTx; kind?: HackActivityType }) {
</KeyValue> </KeyValue>
</Show> </Show>
<Show when={props.info.fee && props.info.fee > 0}> <Show when={props.info.fee && props.info.fee > 0}>
<KeyValue key="Fee"> <KeyValue key={i18n.t("modals.transaction_details.fee")}>
<span class="text-neutral-300"> <span class="text-neutral-300">
<AmountSmall amountSats={props.info.fee} /> <AmountSmall amountSats={props.info.fee} />
</span> </span>
</KeyValue> </KeyValue>
</Show> </Show>
<KeyValue key="Txid"> <KeyValue key={i18n.t("modals.transaction_details.txid")}>
<MiniStringShower text={props.info.txid ?? ""} /> <MiniStringShower text={props.info.txid ?? ""} />
</KeyValue> </KeyValue>
<Switch> <Switch>
<Match when={props.kind === "ChannelOpen" && channelInfo()}> <Match when={props.kind === "ChannelOpen" && channelInfo()}>
<KeyValue key="Balance"> <KeyValue
key={i18n.t("modals.transaction_details.balance")}
>
<span class="text-neutral-300"> <span class="text-neutral-300">
<AmountSmall <AmountSmall
amountSats={channelInfo()?.balance} amountSats={channelInfo()?.balance}
/> />
</span> </span>
</KeyValue> </KeyValue>
<KeyValue key="Reserve"> <KeyValue
key={i18n.t("modals.transaction_details.reserve")}
>
<span class="text-neutral-300"> <span class="text-neutral-300">
<AmountSmall <AmountSmall
amountSats={channelInfo()?.reserve} amountSats={channelInfo()?.reserve}
/> />
</span> </span>
</KeyValue> </KeyValue>
<KeyValue key="Peer"> <KeyValue
key={i18n.t("modals.transaction_details.peer")}
>
<span class="text-neutral-300"> <span class="text-neutral-300">
<MiniStringShower <MiniStringShower
text={channelInfo()?.peer ?? ""} text={channelInfo()?.peer ?? ""}
@@ -301,15 +322,14 @@ function OnchainDetails(props: { info: OnChainTx; kind?: HackActivityType }) {
</Match> </Match>
<Match when={props.kind === "ChannelOpen"}> <Match when={props.kind === "ChannelOpen"}>
<InfoBox accent="blue"> <InfoBox accent="blue">
No channel details found, which means this channel {i18n.t("modals.transaction_details.no_details")}
has likely been closed.
</InfoBox> </InfoBox>
</Match> </Match>
</Switch> </Switch>
</ul> </ul>
<div class="text-center"> <div class="text-center">
<ExternalLink href={mempoolTxUrl(props.info.txid, network)}> <ExternalLink href={mempoolTxUrl(props.info.txid, network)}>
View Transaction {i18n.t("common.view_transaction")}
</ExternalLink> </ExternalLink>
</div> </div>
</VStack> </VStack>
@@ -317,23 +337,24 @@ function OnchainDetails(props: { info: OnChainTx; kind?: HackActivityType }) {
} }
function ChannelCloseDetails(props: { info: ChannelClosure }) { function ChannelCloseDetails(props: { info: ChannelClosure }) {
const i18n = useI18n();
return ( return (
<VStack> <VStack>
{/* <pre>{JSON.stringify(props.info.value, null, 2)}</pre> */} {/* <pre>{JSON.stringify(props.info.value, null, 2)}</pre> */}
<ul class="flex flex-col gap-4"> <ul class="flex flex-col gap-4">
<KeyValue key="Channel ID"> <KeyValue key={i18n.t("modals.transaction_details.channel_id")}>
<MiniStringShower text={props.info.channel_id ?? ""} /> <MiniStringShower text={props.info.channel_id ?? ""} />
</KeyValue> </KeyValue>
<Show when={props.info.timestamp}> <Show when={props.info.timestamp}>
<KeyValue key="When"> <KeyValue key={i18n.t("modals.transaction_details.when")}>
<span class="text-neutral-300"> <span class="text-neutral-300">
{props.info.timestamp {props.info.timestamp
? prettyPrintTime(Number(props.info.timestamp)) ? prettyPrintTime(Number(props.info.timestamp))
: "Pending"} : i18n.t("common.pending")}
</span> </span>
</KeyValue> </KeyValue>
</Show> </Show>
<KeyValue key="Reason"> <KeyValue key={i18n.t("modals.transaction_details.reason")}>
<p class="text-neutral-300 text-right"> <p class="text-neutral-300 text-right">
{props.info.reason ?? ""} {props.info.reason ?? ""}
</p> </p>
@@ -349,6 +370,7 @@ export function DetailsIdModal(props: {
id: string; id: string;
setOpen: (open: boolean) => void; setOpen: (open: boolean) => void;
}) { }) {
const i18n = useI18n();
const [state, _actions] = useMegaStore(); const [state, _actions] = useMegaStore();
const id = () => props.id; const id = () => props.id;
@@ -473,7 +495,7 @@ export function DetailsIdModal(props: {
<Show when={props.kind !== "ChannelClose"}> <Show when={props.kind !== "ChannelClose"}>
<div class="flex justify-center"> <div class="flex justify-center">
<CopyButton <CopyButton
title="Copy" title={i18n.t("common.copy")}
text={json()} text={json()}
/> />
</div> </div>

View File

@@ -1,6 +1,7 @@
import { Dialog } from "@kobalte/core"; import { Dialog } from "@kobalte/core";
import { ParentComponent } from "solid-js"; import { ParentComponent } from "solid-js";
import { Button, SmallHeader } from "./layout"; import { Button, SmallHeader } from "./layout";
import { useI18n } from "~/i18n/context";
const OVERLAY = "fixed inset-0 z-50 bg-black/50 backdrop-blur-sm"; const OVERLAY = "fixed inset-0 z-50 bg-black/50 backdrop-blur-sm";
const DIALOG_POSITIONER = "fixed inset-0 z-50 flex items-center justify-center"; const DIALOG_POSITIONER = "fixed inset-0 z-50 flex items-center justify-center";
@@ -14,6 +15,7 @@ export const ConfirmDialog: ParentComponent<{
onCancel: () => void; onCancel: () => void;
onConfirm: () => void; onConfirm: () => void;
}> = (props) => { }> = (props) => {
const i18n = useI18n();
return ( return (
<Dialog.Root open={props.open} onOpenChange={props.onCancel}> <Dialog.Root open={props.open} onOpenChange={props.onCancel}>
<Dialog.Portal> <Dialog.Portal>
@@ -22,20 +24,26 @@ export const ConfirmDialog: ParentComponent<{
<Dialog.Content class={DIALOG_CONTENT}> <Dialog.Content class={DIALOG_CONTENT}>
<div class="flex justify-between mb-2"> <div class="flex justify-between mb-2">
<Dialog.Title> <Dialog.Title>
<SmallHeader>Are you sure?</SmallHeader> <SmallHeader>
{i18n.t(
"modals.confirm_dialog.are_you_sure"
)}
</SmallHeader>
</Dialog.Title> </Dialog.Title>
</div> </div>
<Dialog.Description class="flex flex-col gap-4"> <Dialog.Description class="flex flex-col gap-4">
{props.children} {props.children}
<div class="flex gap-4 w-full justify-end"> <div class="flex gap-4 w-full justify-end">
<Button onClick={props.onCancel}>Cancel</Button> <Button onClick={props.onCancel}>
{i18n.t("modals.confirm_dialog.cancel")}
</Button>
<Button <Button
intent="red" intent="red"
onClick={props.onConfirm} onClick={props.onConfirm}
loading={props.loading} loading={props.loading}
disabled={props.loading} disabled={props.loading}
> >
Confirm {i18n.t("modals.confirm_dialog.confirm")}
</Button> </Button>
</div> </div>
</Dialog.Description> </Dialog.Description>

View File

@@ -8,35 +8,38 @@ import {
SmallHeader SmallHeader
} from "~/components/layout"; } from "~/components/layout";
import { ExternalLink } from "./layout/ExternalLink"; import { ExternalLink } from "./layout/ExternalLink";
import { useI18n } from "~/i18n/context";
export default function ErrorDisplay(props: { error: Error }) { export default function ErrorDisplay(props: { error: Error }) {
const i18n = useI18n();
return ( return (
<SafeArea> <SafeArea>
<Title>Oh no!</Title> <Title>{i18n.t("error.general.oh_no")}</Title>
<DefaultMain> <DefaultMain>
<LargeHeader>Error</LargeHeader> <LargeHeader>{i18n.t("error.title")}</LargeHeader>
<SmallHeader>This never should've happened</SmallHeader> <SmallHeader>
{i18n.t("error.general.never_should_happen")}
</SmallHeader>
<p class="bg-white/10 rounded-xl p-4 font-mono"> <p class="bg-white/10 rounded-xl p-4 font-mono">
<span class="font-bold">{props.error.name}</span>:{" "} <span class="font-bold">{props.error.name}</span>:{" "}
{props.error.message} {props.error.message}
</p> </p>
<NiceP> <NiceP>
Try reloading this page or clicking the "Dangit" button. If {i18n.t("error.general.try_reloading")}{" "}
you keep having problems,{" "}
<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("error.general.support_link")}
</ExternalLink> </ExternalLink>
</NiceP> </NiceP>
<NiceP> <NiceP>
Getting desperate? Try the{" "} {i18n.t("error.general.getting_desperate")}{" "}
<A href="/emergencykit">emergency kit.</A> <A href="/emergencykit">{i18n.t("error.emergency_link")}</A>
</NiceP> </NiceP>
<div class="h-full" /> <div class="h-full" />
<Button <Button
onClick={() => (window.location.href = "/")} onClick={() => (window.location.href = "/")}
intent="red" intent="red"
> >
Dangit {i18n.t("common.dangit")}
</Button> </Button>
</DefaultMain> </DefaultMain>
</SafeArea> </SafeArea>

View File

@@ -8,23 +8,31 @@ import copyBlack from "~/assets/icons/copy-black.svg";
import shareBlack from "~/assets/icons/share-black.svg"; import shareBlack from "~/assets/icons/share-black.svg";
import chainBlack from "~/assets/icons/chain-black.svg"; import chainBlack from "~/assets/icons/chain-black.svg";
import boltBlack from "~/assets/icons/bolt-black.svg"; import boltBlack from "~/assets/icons/bolt-black.svg";
import { useI18n } from "~/i18n/context";
function KindIndicator(props: { kind: ReceiveFlavor }) { function KindIndicator(props: { kind: ReceiveFlavor }) {
const i18n = useI18n();
return ( return (
<div class="text-black flex flex-col items-end"> <div class="text-black flex flex-col items-end">
<Switch> <Switch>
<Match when={props.kind === "onchain"}> <Match when={props.kind === "onchain"}>
<h3 class="font-semibold">On-chain</h3> <h3 class="font-semibold">
{i18n.t("receive.integrated_qr.onchain")}
</h3>
<img src={chainBlack} alt="chain" /> <img src={chainBlack} alt="chain" />
</Match> </Match>
<Match when={props.kind === "lightning"}> <Match when={props.kind === "lightning"}>
<h3 class="font-semibold">Lightning</h3> <h3 class="font-semibold">
{i18n.t("receive.integrated_qr.lightning")}
</h3>
<img src={boltBlack} alt="bolt" /> <img src={boltBlack} alt="bolt" />
</Match> </Match>
<Match when={props.kind === "unified"}> <Match when={props.kind === "unified"}>
<h3 class="font-semibold">Unified</h3> <h3 class="font-semibold">
{i18n.t("receive.integrated_qr.unified")}
</h3>
<div class="flex gap-1"> <div class="flex gap-1">
<img src={chainBlack} alt="chain" /> <img src={chainBlack} alt="chain" />
<img src={boltBlack} alt="bolt" /> <img src={boltBlack} alt="bolt" />
@@ -56,6 +64,7 @@ export function IntegratedQr(props: {
amountSats: string; amountSats: string;
kind: ReceiveFlavor; kind: ReceiveFlavor;
}) { }) {
const i18n = useI18n();
const [copy, copied] = useCopy({ copiedTimeout: 1000 }); const [copy, copied] = useCopy({ copiedTimeout: 1000 });
return ( return (
<div <div
@@ -65,7 +74,7 @@ export function IntegratedQr(props: {
> >
<Show when={copied()}> <Show when={copied()}>
<div class="absolute w-full h-full bg-neutral-900/60 z-50 rounded-xl flex flex-col items-center justify-center transition-all"> <div class="absolute w-full h-full bg-neutral-900/60 z-50 rounded-xl flex flex-col items-center justify-center transition-all">
<p class="text-xl font-bold">Copied</p> <p class="text-xl font-bold">{i18n.t("common.copied")}</p>
</div> </div>
</Show> </Show>
<div <div

View File

@@ -7,6 +7,7 @@ import {
OVERLAY OVERLAY
} from "~/components/DetailsModal"; } from "~/components/DetailsModal";
import { CopyButton } from "./ShareCard"; import { CopyButton } from "./ShareCard";
import { useI18n } from "~/i18n/context";
export function JsonModal(props: { export function JsonModal(props: {
title: string; title: string;
@@ -16,6 +17,7 @@ export function JsonModal(props: {
setOpen: (open: boolean) => void; setOpen: (open: boolean) => void;
children?: JSX.Element; children?: JSX.Element;
}) { }) {
const i18n = useI18n();
const json = createMemo(() => const json = createMemo(() =>
props.plaintext ? props.plaintext : JSON.stringify(props.data, null, 2) props.plaintext ? props.plaintext : JSON.stringify(props.data, null, 2)
); );
@@ -41,7 +43,10 @@ export function JsonModal(props: {
</pre> </pre>
</div> </div>
{props.children} {props.children}
<CopyButton title="Copy" text={json()} /> <CopyButton
title={i18n.t("common.copy")}
text={json()}
/>
</Dialog.Description> </Dialog.Description>
</Dialog.Content> </Dialog.Content>
</div> </div>

View File

@@ -21,6 +21,7 @@ import { Restart } from "./Restart";
import { ResyncOnchain } from "./ResyncOnchain"; import { ResyncOnchain } from "./ResyncOnchain";
import { ResetRouter } from "./ResetRouter"; import { ResetRouter } from "./ResetRouter";
import { MiniStringShower } from "./DetailsModal"; import { MiniStringShower } from "./DetailsModal";
import { useI18n } from "~/i18n/context";
// TODO: hopefully I don't have to maintain this type forever but I don't know how to pass it around otherwise // TODO: hopefully I don't have to maintain this type forever but I don't know how to pass it around otherwise
type RefetchPeersType = ( type RefetchPeersType = (
@@ -28,6 +29,7 @@ type RefetchPeersType = (
) => MutinyPeer[] | Promise<MutinyPeer[] | undefined> | null | undefined; ) => MutinyPeer[] | Promise<MutinyPeer[] | undefined> | null | undefined;
function PeerItem(props: { peer: MutinyPeer }) { function PeerItem(props: { peer: MutinyPeer }) {
const i18n = useI18n();
const [state, _] = useMegaStore(); const [state, _] = useMegaStore();
const handleDisconnectPeer = async () => { const handleDisconnectPeer = async () => {
@@ -65,7 +67,7 @@ function PeerItem(props: { peer: MutinyPeer }) {
layout="xs" layout="xs"
onClick={handleDisconnectPeer} onClick={handleDisconnectPeer}
> >
Disconnect {i18n.t("settings.admin.kitchen_sink.disconnect")}
</Button> </Button>
</VStack> </VStack>
</Collapsible.Content> </Collapsible.Content>
@@ -74,6 +76,7 @@ function PeerItem(props: { peer: MutinyPeer }) {
} }
function PeersList() { function PeersList() {
const i18n = useI18n();
const [state, _] = useMegaStore(); const [state, _] = useMegaStore();
const getPeers = async () => { const getPeers = async () => {
@@ -86,20 +89,26 @@ function PeersList() {
return ( return (
<> <>
<InnerCard title="Peers"> <InnerCard title={i18n.t("settings.admin.kitchen_sink.peers")}>
{/* By wrapping this in a suspense I don't cause the page to jump to the top */} {/* By wrapping this in a suspense I don't cause the page to jump to the top */}
<Suspense> <Suspense>
<VStack> <VStack>
<For <For
each={peers.latest} each={peers.latest}
fallback={<code>No peers</code>} fallback={
<code>
{i18n.t(
"settings.admin.kitchen_sink.no_peers"
)}
</code>
}
> >
{(peer) => <PeerItem peer={peer} />} {(peer) => <PeerItem peer={peer} />}
</For> </For>
</VStack> </VStack>
</Suspense> </Suspense>
<Button layout="small" onClick={refetch}> <Button layout="small" onClick={refetch}>
Refresh Peers {i18n.t("settings.admin.kitchen_sink.refresh_peers")}
</Button> </Button>
</InnerCard> </InnerCard>
<ConnectPeer refetchPeers={refetch} /> <ConnectPeer refetchPeers={refetch} />
@@ -108,6 +117,7 @@ function PeersList() {
} }
function ConnectPeer(props: { refetchPeers: RefetchPeersType }) { function ConnectPeer(props: { refetchPeers: RefetchPeersType }) {
const i18n = useI18n();
const [state, _] = useMegaStore(); const [state, _] = useMegaStore();
const [value, setValue] = createSignal(""); const [value, setValue] = createSignal("");
@@ -139,18 +149,18 @@ function ConnectPeer(props: { refetchPeers: RefetchPeersType }) {
class="flex flex-col gap-4" class="flex flex-col gap-4"
> >
<TextField.Label class="text-sm font-semibold uppercase"> <TextField.Label class="text-sm font-semibold uppercase">
Connect Peer {i18n.t("settings.admin.kitchen_sink.connect_peer")}
</TextField.Label> </TextField.Label>
<TextField.Input <TextField.Input
class="w-full p-2 rounded-lg text-black" class="w-full p-2 rounded-lg text-black"
placeholder="028241..." placeholder="028241..."
/> />
<TextField.ErrorMessage class="text-red-500"> <TextField.ErrorMessage class="text-red-500">
Expecting a value... {i18n.t("settings.admin.kitchen_sink.expect_a_value")}
</TextField.ErrorMessage> </TextField.ErrorMessage>
</TextField.Root> </TextField.Root>
<Button layout="small" type="submit"> <Button layout="small" type="submit">
Connect {i18n.t("settings.admin.kitchen_sink.connect")}
</Button> </Button>
</form> </form>
</InnerCard> </InnerCard>
@@ -164,6 +174,7 @@ type RefetchChannelsListType = (
type PendingChannelAction = "close" | "force_close" | "abandon"; type PendingChannelAction = "close" | "force_close" | "abandon";
function ChannelItem(props: { channel: MutinyChannel; network?: Network }) { function ChannelItem(props: { channel: MutinyChannel; network?: Network }) {
const i18n = useI18n();
const [state, _] = useMegaStore(); const [state, _] = useMegaStore();
const [pendingChannelAction, setPendingChannelAction] = const [pendingChannelAction, setPendingChannelAction] =
@@ -206,28 +217,28 @@ function ChannelItem(props: { channel: MutinyChannel; network?: Network }) {
props.network props.network
)} )}
> >
View Transaction {i18n.t("common.view_transaction")}
</ExternalLink> </ExternalLink>
<Button <Button
intent="glowy" intent="glowy"
layout="xs" layout="xs"
onClick={() => setPendingChannelAction("close")} onClick={() => setPendingChannelAction("close")}
> >
Close Channel {i18n.t("settings.admin.kitchen_sink.close_channel")}
</Button> </Button>
<Button <Button
intent="glowy" intent="glowy"
layout="xs" layout="xs"
onClick={() => setPendingChannelAction("force_close")} onClick={() => setPendingChannelAction("force_close")}
> >
Force close Channel {i18n.t("settings.admin.kitchen_sink.force_close")}
</Button> </Button>
<Button <Button
intent="glowy" intent="glowy"
layout="xs" layout="xs"
onClick={() => setPendingChannelAction("abandon")} onClick={() => setPendingChannelAction("abandon")}
> >
Abandon Channel {i18n.t("settings.admin.kitchen_sink.abandon_channel")}
</Button> </Button>
</VStack> </VStack>
<ConfirmDialog <ConfirmDialog
@@ -238,21 +249,24 @@ function ChannelItem(props: { channel: MutinyChannel; network?: Network }) {
> >
<Switch> <Switch>
<Match when={pendingChannelAction() === "close"}> <Match when={pendingChannelAction() === "close"}>
<p>Are you sure you want to close this channel?</p> <p>
{i18n.t(
"settings.admin.kitchen_sink.confirm_close_channel"
)}
</p>
</Match> </Match>
<Match when={pendingChannelAction() === "force_close"}> <Match when={pendingChannelAction() === "force_close"}>
<p> <p>
Are you sure you want to force close this {i18n.t(
channel? Your funds will take a few days to "settings.admin.kitchen_sink.confirm_force_close"
redeem on chain. )}
</p> </p>
</Match> </Match>
<Match when={pendingChannelAction() === "abandon"}> <Match when={pendingChannelAction() === "abandon"}>
<p> <p>
Are you sure you want to abandon this channel? {i18n.t(
Typically only do this if the opening "settings.admin.kitchen_sink.confirm_abandon_channel"
transaction will never confirm. Otherwise, you )}
will lose funds.
</p> </p>
</Match> </Match>
</Switch> </Switch>
@@ -263,6 +277,7 @@ function ChannelItem(props: { channel: MutinyChannel; network?: Network }) {
} }
function ChannelsList() { function ChannelsList() {
const i18n = useI18n();
const [state, _] = useMegaStore(); const [state, _] = useMegaStore();
const getChannels = async () => { const getChannels = async () => {
@@ -277,10 +292,19 @@ function ChannelsList() {
return ( return (
<> <>
<InnerCard title="Channels"> <InnerCard title={i18n.t("settings.admin.kitchen_sink.channels")}>
{/* By wrapping this in a suspense I don't cause the page to jump to the top */} {/* By wrapping this in a suspense I don't cause the page to jump to the top */}
<Suspense> <Suspense>
<For each={channels()} fallback={<code>No channels</code>}> <For
each={channels()}
fallback={
<code>
{i18n.t(
"settings.admin.kitchen_sink.no_channels"
)}
</code>
}
>
{(channel) => ( {(channel) => (
<ChannelItem channel={channel} network={network} /> <ChannelItem channel={channel} network={network} />
)} )}
@@ -294,7 +318,7 @@ function ChannelsList() {
refetch(); refetch();
}} }}
> >
Refresh Channels {i18n.t("settings.admin.kitchen_sink.refresh_channels")}
</Button> </Button>
</InnerCard> </InnerCard>
<OpenChannel refetchChannels={refetch} /> <OpenChannel refetchChannels={refetch} />
@@ -303,6 +327,7 @@ function ChannelsList() {
} }
function OpenChannel(props: { refetchChannels: RefetchChannelsListType }) { function OpenChannel(props: { refetchChannels: RefetchChannelsListType }) {
const i18n = useI18n();
const [state, _] = useMegaStore(); const [state, _] = useMegaStore();
const [creationError, setCreationError] = createSignal<Error>(); const [creationError, setCreationError] = createSignal<Error>();
@@ -354,7 +379,7 @@ function OpenChannel(props: { refetchChannels: RefetchChannelsListType }) {
class="flex flex-col gap-2" class="flex flex-col gap-2"
> >
<TextField.Label class="text-sm font-semibold uppercase"> <TextField.Label class="text-sm font-semibold uppercase">
Pubkey {i18n.t("settings.admin.kitchen_sink.pubkey")}
</TextField.Label> </TextField.Label>
<TextField.Input class="w-full p-2 rounded-lg text-black" /> <TextField.Input class="w-full p-2 rounded-lg text-black" />
</TextField.Root> </TextField.Root>
@@ -364,7 +389,7 @@ function OpenChannel(props: { refetchChannels: RefetchChannelsListType }) {
class="flex flex-col gap-2" class="flex flex-col gap-2"
> >
<TextField.Label class="text-sm font-semibold uppercase"> <TextField.Label class="text-sm font-semibold uppercase">
Amount {i18n.t("settings.admin.kitchen_sink.amount")}
</TextField.Label> </TextField.Label>
<TextField.Input <TextField.Input
type="number" type="number"
@@ -372,7 +397,7 @@ function OpenChannel(props: { refetchChannels: RefetchChannelsListType }) {
/> />
</TextField.Root> </TextField.Root>
<Button layout="small" type="submit"> <Button layout="small" type="submit">
Open Channel {i18n.t("settings.admin.kitchen_sink.open_channel")}
</Button> </Button>
</form> </form>
</InnerCard> </InnerCard>
@@ -387,7 +412,7 @@ function OpenChannel(props: { refetchChannels: RefetchChannelsListType }) {
network network
)} )}
> >
View Transaction {i18n.t("common.view_transaction")}
</ExternalLink> </ExternalLink>
</Show> </Show>
<Show when={creationError()}> <Show when={creationError()}>
@@ -398,6 +423,7 @@ function OpenChannel(props: { refetchChannels: RefetchChannelsListType }) {
} }
function ListNodes() { function ListNodes() {
const i18n = useI18n();
const [state, _] = useMegaStore(); const [state, _] = useMegaStore();
const getNodeIds = async () => { const getNodeIds = async () => {
@@ -408,9 +434,16 @@ function ListNodes() {
const [nodeIds] = createResource(getNodeIds); const [nodeIds] = createResource(getNodeIds);
return ( return (
<InnerCard title="Nodes"> <InnerCard title={i18n.t("settings.admin.kitchen_sink.nodes")}>
<Suspense> <Suspense>
<For each={nodeIds()} fallback={<code>No nodes</code>}> <For
each={nodeIds()}
fallback={
<code>
{i18n.t("settings.admin.kitchen_sink.no_nodes")}
</code>
}
>
{(nodeId) => <MiniStringShower text={nodeId} />} {(nodeId) => <MiniStringShower text={nodeId} />}
</For> </For>
</Suspense> </Suspense>

View File

@@ -1,22 +1,24 @@
import { Progress } from "@kobalte/core"; import { Progress } from "@kobalte/core";
import { Show } from "solid-js"; import { Show } from "solid-js";
import { useI18n } from "~/i18n/context";
import { useMegaStore } from "~/state/megaStore"; import { useMegaStore } from "~/state/megaStore";
export function LoadingBar(props: { value: number; max: number }) { export function LoadingBar(props: { value: number; max: number }) {
const i18n = useI18n();
function valueToStage(value: number) { function valueToStage(value: number) {
switch (value) { switch (value) {
case 0: case 0:
return "Just getting started"; return i18n.t("modals.loading.default");
case 1: case 1:
return "Double checking something"; return i18n.t("modals.loading.double_checking");
case 2: case 2:
return "Downloading"; return i18n.t("modals.loading.downloading");
case 3: case 3:
return "Setup"; return i18n.t("modals.loading.setup");
case 4: case 4:
return "Done"; return i18n.t("modals.loading.done");
default: default:
return "Just getting started"; return i18n.t("modals.loading.default");
} }
} }
return ( return (
@@ -24,7 +26,9 @@ export function LoadingBar(props: { value: number; max: number }) {
value={props.value} value={props.value}
minValue={0} minValue={0}
maxValue={props.max} maxValue={props.max}
getValueLabel={({ value }) => `Loading: ${valueToStage(value)}`} getValueLabel={({ value }) =>
i18n.t("modals.loading.loading", { stage: valueToStage(value) })
}
class="w-full flex flex-col gap-2" class="w-full flex flex-col gap-2"
> >
<Progress.ValueLabel class="text-sm text-m-grey-400" /> <Progress.ValueLabel class="text-sm text-m-grey-400" />

View File

@@ -10,20 +10,20 @@ export function FeesModal(props: { icon?: boolean }) {
const i18n = useI18n(); const i18n = useI18n();
return ( return (
<MoreInfoModal <MoreInfoModal
title={i18n.t("whats_with_the_fees")} title={i18n.t("modals.more_info.whats_with_the_fees")}
linkText={ linkText={
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("common.why")
) )
} }
> >
<p>{i18n.t("more_info_modal_p1")}</p> <p>{i18n.t("modals.more_info.self_custodial")}</p>
<p>{i18n.t("more_info_modal_p2")}</p> <p>{i18n.t("modals.more_info.future_payments")}</p>
<p> <p>
<ExternalLink href="https://github.com/MutinyWallet/mutiny-web/wiki/Understanding-liquidity"> <ExternalLink href="https://github.com/MutinyWallet/mutiny-web/wiki/Understanding-liquidity">
{i18n.t("learn_more_about_liquidity")} {i18n.t("modals.more_info.liquidity")}
</ExternalLink> </ExternalLink>
</p> </p>
</MoreInfoModal> </MoreInfoModal>

View File

@@ -5,8 +5,10 @@ import { showToast } from "./Toaster";
import save from "~/assets/icons/save.svg"; import save from "~/assets/icons/save.svg";
import close from "~/assets/icons/close.svg"; import close from "~/assets/icons/close.svg";
import restore from "~/assets/icons/upload.svg"; import restore from "~/assets/icons/upload.svg";
import { useI18n } from "~/i18n/context";
export function OnboardWarning() { export function OnboardWarning() {
const i18n = useI18n();
const [state, actions] = useMegaStore(); const [state, actions] = useMegaStore();
const [dismissedBackup, setDismissedBackup] = createSignal( const [dismissedBackup, setDismissedBackup] = createSignal(
sessionStorage.getItem("dismissed_backup") ?? false sessionStorage.getItem("dismissed_backup") ?? false
@@ -31,11 +33,13 @@ export function OnboardWarning() {
</div> </div>
<div class="flex md:flex-row flex-col items-center gap-4"> <div class="flex md:flex-row flex-col items-center gap-4">
<div class="flex flex-col"> <div class="flex flex-col">
<SmallHeader>Welcome!</SmallHeader> <SmallHeader>
{i18n.t("modals.onboarding.welcome")}
</SmallHeader>
<p class="text-base font-light"> <p class="text-base font-light">
If you've used Mutiny before you can restore {i18n.t(
from a backup. Otherwise you can skip this and "modals.onboarding.restore_from_backup"
enjoy your new wallet! )}
</p> </p>
</div> </div>
<Button <Button
@@ -44,12 +48,14 @@ export function OnboardWarning() {
class="self-start md:self-auto" class="self-start md:self-auto"
onClick={() => { onClick={() => {
showToast({ showToast({
title: "Unimplemented", title: i18n.t("common.error_unimplemented"),
description: "We don't do that yet" description: i18n.t(
"modals.onboarding.not_available"
)
}); });
}} }}
> >
Restore {i18n.t("settings.restore.title")}
</Button> </Button>
</div> </div>
<button <button
@@ -72,10 +78,11 @@ export function OnboardWarning() {
</div> </div>
<div class="flex flex-row max-md:items-center justify-between gap-4"> <div class="flex flex-row max-md:items-center justify-between gap-4">
<div class="flex flex-col"> <div class="flex flex-col">
<SmallHeader>Secure your funds</SmallHeader> <SmallHeader>
{i18n.t("modals.onboarding.secure_your_funds")}
</SmallHeader>
<p class="text-base font-light max-md:hidden"> <p class="text-base font-light max-md:hidden">
You have money stored in this browser. Let's {i18n.t("modals.onboarding.make_backup")}
make sure you have a backup.
</p> </p>
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
@@ -85,7 +92,7 @@ export function OnboardWarning() {
class="self-auto" class="self-auto"
href="/settings/backup" href="/settings/backup"
> >
Backup {i18n.t("settings.backup.title")}
</ButtonLink> </ButtonLink>
</div> </div>
</div> </div>

View File

@@ -20,6 +20,7 @@ import { InfoBox } from "./InfoBox";
import eify from "~/utils/eify"; import eify from "~/utils/eify";
import { A } from "solid-start"; import { A } from "solid-start";
import { createDeepSignal } from "~/utils/deepSignal"; import { createDeepSignal } from "~/utils/deepSignal";
import { useI18n } from "~/i18n/context";
type PendingItem = { type PendingItem = {
id: string; id: string;
@@ -29,6 +30,7 @@ type PendingItem = {
}; };
export function PendingNwc() { export function PendingNwc() {
const i18n = useI18n();
const [state, _actions] = useMegaStore(); const [state, _actions] = useMegaStore();
const [error, setError] = createSignal<Error>(); const [error, setError] = createSignal<Error>();
@@ -109,7 +111,7 @@ export function PendingNwc() {
return ( return (
<Show when={pendingRequests() && pendingRequests()!.length > 0}> <Show when={pendingRequests() && pendingRequests()!.length > 0}>
<Card title="Pending Requests"> <Card title={i18n.t("settings.connections.pending_nwc.title")}>
<div class="p-1" /> <div class="p-1" />
<VStack> <VStack>
<Show when={error()}> <Show when={error()}>
@@ -183,7 +185,7 @@ export function PendingNwc() {
href="/settings/connections" href="/settings/connections"
class="text-m-red active:text-m-red/80 font-semibold no-underline self-center" class="text-m-red active:text-m-red/80 font-semibold no-underline self-center"
> >
Configure {i18n.t("settings.connections.pending_nwc.configure_link")}
</A> </A>
</Card> </Card>
</Show> </Show>

View File

@@ -1,7 +1,9 @@
import { Button, InnerCard, NiceP, VStack } from "~/components/layout"; import { Button, InnerCard, NiceP, VStack } from "~/components/layout";
import { useMegaStore } from "~/state/megaStore"; import { useMegaStore } from "~/state/megaStore";
import { useI18n } from "~/i18n/context";
export function ResetRouter() { export function ResetRouter() {
const i18n = useI18n();
const [state, _] = useMegaStore(); const [state, _] = useMegaStore();
async function reset() { async function reset() {
@@ -15,12 +17,9 @@ export function ResetRouter() {
return ( return (
<InnerCard> <InnerCard>
<VStack> <VStack>
<NiceP> <NiceP>{i18n.t("error.reset_router.payments_failing")}</NiceP>
Failing to make payments? Try resetting the lightning
router.
</NiceP>
<Button intent="red" onClick={reset}> <Button intent="red" onClick={reset}>
Reset Router {i18n.t("error.reset_router.reset_router")}
</Button> </Button>
</VStack> </VStack>
</InnerCard> </InnerCard>

View File

@@ -1,8 +1,10 @@
import { createSignal } from "solid-js"; import { createSignal } from "solid-js";
import { Button, InnerCard, NiceP, VStack } from "~/components/layout"; import { Button, InnerCard, NiceP, VStack } from "~/components/layout";
import { useI18n } from "~/i18n/context";
import { useMegaStore } from "~/state/megaStore"; import { useMegaStore } from "~/state/megaStore";
export function Restart() { export function Restart() {
const i18n = useI18n();
const [state, _] = useMegaStore(); const [state, _] = useMegaStore();
const [hasStopped, setHasStopped] = createSignal(false); const [hasStopped, setHasStopped] = createSignal(false);
@@ -23,14 +25,14 @@ export function Restart() {
return ( return (
<InnerCard> <InnerCard>
<VStack> <VStack>
<NiceP> <NiceP>{i18n.t("error.restart.title")}</NiceP>
Something *extra* screwy going on? Stop the nodes!
</NiceP>
<Button <Button
intent={hasStopped() ? "green" : "red"} intent={hasStopped() ? "green" : "red"}
onClick={toggle} onClick={toggle}
> >
{hasStopped() ? "Start" : "Stop"} {hasStopped()
? i18n.t("error.restart.start")
: i18n.t("error.restart.stop")}
</Button> </Button>
</VStack> </VStack>
</InnerCard> </InnerCard>

View File

@@ -1,7 +1,9 @@
import { Button, InnerCard, NiceP, VStack } from "~/components/layout"; import { Button, InnerCard, NiceP, VStack } from "~/components/layout";
import { useI18n } from "~/i18n/context";
import { useMegaStore } from "~/state/megaStore"; import { useMegaStore } from "~/state/megaStore";
export function ResyncOnchain() { export function ResyncOnchain() {
const i18n = useI18n();
const [state, _] = useMegaStore(); const [state, _] = useMegaStore();
async function reset() { async function reset() {
@@ -15,12 +17,9 @@ export function ResyncOnchain() {
return ( return (
<InnerCard> <InnerCard>
<VStack> <VStack>
<NiceP> <NiceP>{i18n.t("error.resync.incorrect_balance")}</NiceP>
On-chain balance seems incorrect? Try re-syncing the
on-chain wallet.
</NiceP>
<Button intent="red" onClick={reset}> <Button intent="red" onClick={reset}>
Resync wallet {i18n.t("error.resync.resync_wallet")}
</Button> </Button>
</VStack> </VStack>
</InnerCard> </InnerCard>

View File

@@ -12,6 +12,7 @@ import { ImportExport } from "./ImportExport";
import { Logs } from "./Logs"; import { Logs } from "./Logs";
import { DeleteEverything } from "./DeleteEverything"; import { DeleteEverything } from "./DeleteEverything";
import { FeedbackLink } from "~/routes/Feedback"; import { FeedbackLink } from "~/routes/Feedback";
import { useI18n } from "~/i18n/context";
function ErrorFooter() { function ErrorFooter() {
return ( return (
@@ -26,86 +27,100 @@ function ErrorFooter() {
export default function SetupErrorDisplay(props: { initialError: Error }) { export default function SetupErrorDisplay(props: { initialError: Error }) {
// Error shouldn't be reactive, so we assign to it so it just gets rendered with the first value // Error shouldn't be reactive, so we assign to it so it just gets rendered with the first value
const i18n = useI18n();
const error = props.initialError; const error = props.initialError;
return ( return (
<SafeArea> <SafeArea>
<Switch> <Switch>
<Match when={error.message.startsWith("Existing tab")}> <Match when={error.message.startsWith("Existing tab")}>
<Title>Multiple tabs detected</Title> <Title>{i18n.t("error.on_boot.existing_tab.title")}</Title>
<DefaultMain> <DefaultMain>
<LargeHeader>Multiple tabs detected</LargeHeader> <LargeHeader>
{i18n.t("error.on_boot.existing_tab.title")}
</LargeHeader>
<p class="bg-white/10 rounded-xl p-4 font-mono"> <p class="bg-white/10 rounded-xl p-4 font-mono">
<span class="font-bold">{error.name}</span>:{" "} <span class="font-bold">{error.name}</span>:{" "}
{error.message} {error.message}
</p> </p>
<NiceP> <NiceP>
Mutiny currently only supports use in one tab at a {i18n.t("error.on_boot.existing_tab.description")}
time. It looks like you have another tab open with
Mutiny running. Please close that tab and refresh
this page, or close this tab and refresh the other
one.
</NiceP> </NiceP>
<ErrorFooter /> <ErrorFooter />
</DefaultMain> </DefaultMain>
</Match> </Match>
<Match when={error.message.startsWith("Browser error")}> <Match when={error.message.startsWith("Browser error")}>
<Title>Incompatible browser</Title> <Title>
{i18n.t("error.on_boot.incompatible_browser.title")}
</Title>
<DefaultMain> <DefaultMain>
<LargeHeader>Incompatible browser detected</LargeHeader> <LargeHeader>
{i18n.t(
"error.on_boot.incompatible_browser.header"
)}
</LargeHeader>
<p class="bg-white/10 rounded-xl p-4 font-mono"> <p class="bg-white/10 rounded-xl p-4 font-mono">
<span class="font-bold">{error.name}</span>:{" "} <span class="font-bold">{error.name}</span>:{" "}
{error.message} {error.message}
</p> </p>
<NiceP> <NiceP>
Mutiny requires a modern browser that supports {i18n.t(
WebAssembly, LocalStorage, and IndexedDB. Some "error.on_boot.incompatible_browser.description"
browsers disable these features in private mode. )}
</NiceP> </NiceP>
<NiceP> <NiceP>
Please make sure your browser supports all these {i18n.t(
features, or consider trying another browser. You "error.on_boot.incompatible_browser.try_different_browser"
might also try disabling certain extensions or )}
"shields" that block these features.
</NiceP> </NiceP>
<NiceP> <NiceP>
(We'd love to support more private browsers, but we {i18n.t(
have to save your wallet data to browser storage or "error.on_boot.incompatible_browser.browser_storage"
else you will lose funds.) )}
</NiceP> </NiceP>
<ExternalLink href="https://github.com/MutinyWallet/mutiny-web/wiki/Browser-Compatibility"> <ExternalLink href="https://github.com/MutinyWallet/mutiny-web/wiki/Browser-Compatibility">
Supported Browsers {i18n.t(
"error.on_boot.incompatible_browser.browsers_link"
)}
</ExternalLink> </ExternalLink>
<ErrorFooter /> <ErrorFooter />
</DefaultMain> </DefaultMain>
</Match> </Match>
<Match when={true}> <Match when={true}>
<Title>Failed to load</Title> <Title>
{i18n.t("error.on_boot.loading_failed.title")}
</Title>
<DefaultMain> <DefaultMain>
<LargeHeader>Failed to load Mutiny</LargeHeader> <LargeHeader>
{i18n.t("error.on_boot.loading_failed.header")}
</LargeHeader>
<p class="bg-white/10 rounded-xl p-4 font-mono"> <p class="bg-white/10 rounded-xl p-4 font-mono">
<span class="font-bold">{error.name}</span>:{" "} <span class="font-bold">{error.name}</span>:{" "}
{error.message} {error.message}
</p> </p>
<NiceP> <NiceP>
Something went wrong while booting up Mutiny Wallet. {i18n.t("error.on_boot.loading_failed.description")}
</NiceP> </NiceP>
<NiceP> <NiceP>
If your wallet seems broken, here are some tools to {i18n.t(
try to debug and repair it. "error.on_boot.loading_failed.repair_options"
)}
</NiceP> </NiceP>
<NiceP> <NiceP>
If you have any questions on what these buttons do, {i18n.t("error.on_boot.loading_failed.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(
"error.on_boot.loading_failed.support_link"
)}
</ExternalLink> </ExternalLink>
</NiceP> </NiceP>
<ImportExport emergency /> <ImportExport emergency />
<Logs /> <Logs />
<div class="rounded-xl p-4 flex flex-col gap-2 bg-m-red"> <div class="rounded-xl p-4 flex flex-col gap-2 bg-m-red">
<SmallHeader>Danger zone</SmallHeader> <SmallHeader>
{i18n.t("settings.danger_zone")}
</SmallHeader>
<DeleteEverything emergency /> <DeleteEverything emergency />
</div> </div>

View File

@@ -7,6 +7,7 @@ import shareBlack from "~/assets/icons/share-black.svg";
import eyeIcon from "~/assets/icons/eye.svg"; import eyeIcon from "~/assets/icons/eye.svg";
import { Show, createSignal } from "solid-js"; import { Show, createSignal } from "solid-js";
import { JsonModal } from "./JsonModal"; import { JsonModal } from "./JsonModal";
import { useI18n } from "~/i18n/context";
const STYLE = const STYLE =
"px-4 py-2 rounded-xl border-2 border-white flex gap-2 items-center font-semibold hover:text-m-blue transition-colors"; "px-4 py-2 rounded-xl border-2 border-white flex gap-2 items-center font-semibold hover:text-m-blue transition-colors";
@@ -15,13 +16,14 @@ export function ShareButton(props: {
receiveString: string; receiveString: string;
whiteBg?: boolean; whiteBg?: boolean;
}) { }) {
const i18n = useI18n();
async function share(receiveString: string) { async function share(receiveString: string) {
// If the browser doesn't support share we can just copy the address // If the browser doesn't support share we can just copy the address
if (!navigator.share) { if (!navigator.share) {
console.error("Share not supported"); console.error("Share not supported");
} }
const shareData: ShareData = { const shareData: ShareData = {
title: "Mutiny Wallet", title: i18n.t("common.title"),
text: receiveString text: receiveString
}; };
try { try {
@@ -33,7 +35,7 @@ export function ShareButton(props: {
return ( return (
<button class={STYLE} onClick={(_) => share(props.receiveString)}> <button class={STYLE} onClick={(_) => share(props.receiveString)}>
<span>Share</span> <span>{i18n.t("modals.share")}</span>
<img src={props.whiteBg ? shareBlack : shareIcon} alt="share" /> <img src={props.whiteBg ? shareBlack : shareIcon} alt="share" />
</button> </button>
); );
@@ -57,13 +59,14 @@ export function TruncateMiddle(props: { text: string; whiteBg?: boolean }) {
} }
export function StringShower(props: { text: string }) { export function StringShower(props: { text: string }) {
const i18n = useI18n();
const [open, setOpen] = createSignal(false); const [open, setOpen] = createSignal(false);
return ( return (
<> <>
<JsonModal <JsonModal
open={open()} open={open()}
plaintext={props.text} plaintext={props.text}
title="Details" title={i18n.t("modals.details")}
setOpen={setOpen} setOpen={setOpen}
/> />
<div class="w-full grid grid-cols-[minmax(0,_1fr)_auto]"> <div class="w-full grid grid-cols-[minmax(0,_1fr)_auto]">
@@ -81,6 +84,7 @@ export function CopyButton(props: {
title?: string; title?: string;
whiteBg?: boolean; whiteBg?: boolean;
}) { }) {
const i18n = useI18n();
const [copy, copied] = useCopy({ copiedTimeout: 1000 }); const [copy, copied] = useCopy({ copiedTimeout: 1000 });
function handleCopy() { function handleCopy() {
@@ -89,7 +93,9 @@ export function CopyButton(props: {
return ( return (
<button class={STYLE} onClick={handleCopy}> <button class={STYLE} onClick={handleCopy}>
{copied() ? "Copied" : props.title ?? "Copy"} {copied()
? i18n.t("common.copied")
: props.title ?? i18n.t("common.copy")}
<img src={props.whiteBg ? copyBlack : copyIcon} alt="copy" /> <img src={props.whiteBg ? copyBlack : copyIcon} alt="copy" />
</button> </button>
); );

View File

@@ -153,7 +153,7 @@ export const FullscreenLoader = () => {
<p class="max-w-[20rem] text-neutral-400"> <p class="max-w-[20rem] text-neutral-400">
{i18n.t("error.load_time.stuck")}{" "} {i18n.t("error.load_time.stuck")}{" "}
<A class="text-white" href="/emergencykit"> <A class="text-white" href="/emergencykit">
{i18n.t("error.load_time.emergency_link")} {i18n.t("error.emergency_link")}
</A> </A>
</p> </p>
</Show> </Show>

View File

@@ -4,6 +4,7 @@ import { use } from "i18next";
import LanguageDetector from "i18next-browser-languagedetector"; 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";
import ko from "~/i18n/ko/translations";
export const resources = { export const resources = {
en: { en: {
@@ -11,6 +12,9 @@ export const resources = {
}, },
pt: { pt: {
translations: pt translations: pt
},
ko: {
translations: ko
} }
}; };

View File

@@ -1,5 +1,6 @@
export default { export default {
common: { common: {
title: "Mutiny Wallet",
nice: "Nice", nice: "Nice",
home: "Home", home: "Home",
sats: "SATS", sats: "SATS",
@@ -9,10 +10,33 @@ export default {
send: "Send", send: "Send",
receive: "Receive", receive: "Receive",
dangit: "Dangit", dangit: "Dangit",
back: "Back" back: "Back",
coming_soon: "(coming soon)",
copy: "Copy",
copied: "Copied",
continue: "Continue",
error_unimplemented: "Unimplemented",
why: "Why?",
private_tags: "Private tags",
view_transaction: "View Transaction",
pending: "Pending"
}, },
char: { contacts: {
del: "DEL" new: "new",
add_contact: "Add Contact",
new_contact: "New Contact",
create_contact: "Create contact",
edit_contact: "Edit contact",
save_contact: "Save contact",
payment_history: "Payment history",
no_payments: "No payments yet with",
edit: "Edit",
pay: "Pay",
name: "Name",
placeholder: "Satoshi",
unimplemented: "Unimplemented",
not_available: "We don't do that yet",
error_name: "We at least need a name"
}, },
receive: { receive: {
receive_bitcoin: "Receive Bitcoin", receive_bitcoin: "Receive Bitcoin",
@@ -22,16 +46,48 @@ export default {
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",
keep_mutiny_open: "Keep Mutiny open to complete the payment.",
choose_payment_format: "Choose payment format",
unified_label: "Unified",
unified_caption:
"Combines a bitcoin address and a lightning invoice. Sender chooses payment method.",
lightning_label: "Lightning invoice",
lightning_caption:
"Ideal for small transactions. Usually lower fees than on-chain.",
onchain_label: "Bitcoin address",
onchain_caption:
"On-chain, just like Satoshi did it. Ideal for very large transactions.",
unified_setup_fee:
"A lightning setup fee of {{amount}} SATS will be charged if paid over lightning.",
lightning_setup_fee:
"A lightning setup fee of {{amount}} SATS will be charged for this receive.",
amount: "Amount",
fee: "+ Fee",
total: "Total",
spendable: "Spendable",
channel_size: "Channel size",
channel_reserve: "- Channel reserve",
amount_editable: { amount_editable: {
receive_too_small: receive_too_small:
"Your first lightning receive needs to be {{amount}} sats or greater. A setup fee will be deducted from the requested amount.", "Your first lightning receive needs to be {{amount}} SATS or greater. A setup fee will be deducted from the requested amount.",
setup_fee_lightning: setup_fee_lightning:
"A lightning setup fee will be charged if paid over lightning.", "A lightning setup fee will be charged if paid over lightning.",
too_big_for_beta: too_big_for_beta:
"That's a lot of sats. You do know Mutiny Wallet is still in beta, yeah?", "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.", more_than_21m: "There are only 21 million bitcoin.",
set_amount: "Set amount", set_amount: "Set amount",
max: "MAX" max: "MAX",
fix_amounts: {
ten_k: "10k",
one_hundred_k: "100k",
one_million: "1m"
},
del: "DEL"
},
integrated_qr: {
onchain: "On-chain",
lightning: "Lightning",
unified: "Unified"
} }
}, },
send: { send: {
@@ -39,10 +95,21 @@ export default {
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",
send_bitcoin: "Send Bitcoin",
paste: "Paste",
scan_qr: "Scan QR",
payment_initiated: "Payment Initiated",
payment_sent: "Payment Sent",
destination: "Destination",
progress_bar: { progress_bar: {
of: "of", of: "of",
sats_sent: "sats sent" sats_sent: "sats sent"
} },
error_low_balance:
"We do not have enough balance to pay the given amount.",
error_clipboard: "Clipboard not supported",
error_keysend: "Keysend failed",
error_LNURL: "LNURL Pay failed"
}, },
feedback: { feedback: {
header: "Give us feedback!", header: "Give us feedback!",
@@ -51,9 +118,8 @@ export default {
more: "Got more to say?", more: "Got more to say?",
tracking: tracking:
"Mutiny doesn't track or spy on your behavior, so your feedback is incredibly helpful.", "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: "If you're comfortable with GitHub you can also",
github_two: ".", create_issue: "create an issue.",
create_issue: "create an issue",
link: "Feedback?", link: "Feedback?",
feedback_placeholder: "Bugs, feature requests, feedback, etc.", feedback_placeholder: "Bugs, feature requests, feedback, etc.",
info_label: "Include contact info", info_label: "Include contact info",
@@ -67,24 +133,52 @@ export default {
invalid_feedback: "Please say something!", invalid_feedback: "Please say something!",
need_contact: "We need some way to contact you", need_contact: "We need some way to contact you",
invalid_email: "That doesn't look like an email address to me", invalid_email: "That doesn't look like an email address to me",
error: "Error submitting feedback", error: "Error submitting feedback {{error}}",
try_again: "Please try again later." try_again: "Please try again later."
}, },
activity: { activity: {
title: "Activity",
mutiny: "Mutiny",
nostr: "Nostr",
view_all: "View all", view_all: "View all",
receive_some_sats_to_get_started: "Receive some sats to get started", receive_some_sats_to_get_started: "Receive some sats to get started",
channel_open: "Channel Open", channel_open: "Channel Open",
channel_close: "Channel Close", channel_close: "Channel Close",
unknown: "Unknown" unknown: "Unknown",
import_contacts:
"Import your contacts from nostr to see who they're zapping.",
coming_soon: "Coming soon"
},
redshift: {
title: "Redshift",
unknown: "Unknown",
what_happened: "What happened?",
starting_amount: "Starting amount",
fees_paid: "Fees paid",
change: "Change",
outbound_channel: "Outbound channel",
return_channel: "Return channel",
where_this_goes: "Where is this going?",
watch_it_go: "Watch it go!",
choose_your: "Choose your",
utxo_to_begin: "UTXO to begin",
unshifted_utxo: "Unshifted UTXOs",
redshifted: "Redshifted",
utxos: "UTXOs",
no_utxos_empty_state: "No utxos (empty state)",
utxo_label: "UTXO",
utxo_caption: "Trade in your UTXO for a fresh UTXO",
lightning_label: "Lightning",
lightning_caption: "Convert your UTXO into Lightning",
oh_dear: "Oh dear",
here_is_error: "Here's what happened:"
}, },
redshift: {},
scanner: { scanner: {
paste: "Paste Something", paste: "Paste Something",
cancel: "Cancel" cancel: "Cancel"
}, },
settings: { settings: {
header: "Settings", header: "Settings",
mutiny_plus: "MUTINY+",
support: "Learn how to support Mutiny", support: "Learn how to support Mutiny",
general: "GENERAL", general: "GENERAL",
beta_features: "BETA FEATURES", beta_features: "BETA FEATURES",
@@ -97,7 +191,33 @@ export default {
warning_one: warning_one:
"If you know what you're doing you're in the right place.", "If you know what you're doing you're in the right place.",
warning_two: warning_two:
"These are internal tools we use to debug and test the app. Please be careful!" "These are internal tools we use to debug and test the app. Please be careful!",
kitchen_sink: {
disconnect: "Disconnect",
peers: "Peers",
no_peers: "No peers",
refresh_peers: "Refresh Peers",
connect_peer: "Connect Peer",
expect_a_value: "Expecting a value...",
connect: "Connect",
close_channel: "Close Channel",
force_close: "Force close Channel",
abandon_channel: "Abandon Channel",
confirm_close_channel:
"Are you sure you want to close this channel?",
confirm_force_close:
"Are you sure you want to force close this channel? Your funds will take a few days to redeem on chain.",
confirm_abandon_channel:
"Are you sure you want to abandon this channel? Typically only do this if the opening transaction will never confirm. Otherwise, you will lose funds.",
channels: "Channels",
no_channels: "No Channels",
refresh_channels: "Refresh Channels",
pubkey: "Pubkey",
amount: "Amount",
open_channel: "Open Channel",
nodes: "Nodes",
no_nodes: "No nodes"
}
}, },
backup: { backup: {
title: "Backup", title: "Backup",
@@ -141,8 +261,13 @@ export default {
new_connection_label: "Name", new_connection_label: "Name",
new_connection_placeholder: "My favorite nostr client...", new_connection_placeholder: "My favorite nostr client...",
create_connection: "Create Connection", create_connection: "Create Connection",
relay: "Relay",
authorize: authorize:
"Authorize external services to request payments from your wallet. Pairs great with Nostr clients." "Authorize external services to request payments from your wallet. Pairs great with Nostr clients.",
pending_nwc: {
title: "Pending Requests",
configure_link: "Configure"
}
}, },
emergency_kit: { emergency_kit: {
title: "Emergency Kit", title: "Emergency Kit",
@@ -180,17 +305,99 @@ export default {
deleted_description: "Deleted all data" deleted_description: "Deleted all data"
} }
}, },
encrypt: {}, encrypt: {
lnurl_auth: { header: "Encrypt your seed words",
title: "LNURL Auth" hot_wallet_warning:
"Mutiny is a &rdquo;hot wallet&rdquo; so it needs your seed word to operate, but you can optionally encrypt those words with a password.",
password_tip:
"That way, if someone gets access to your browser, they still won't have access to your funds.",
optional: "(optional)",
existing_password: "Existing password",
existing_password_caption:
"Leave blank if you haven't set a password yet.",
new_password_label: "Password",
new_password_placeholder: "Enter a password",
new_password_caption:
"This password will be used to encrypt your seed words. If you forget it, you will need to re-enter your seed words to access your funds. You did write down your seed words, right?",
confirm_password_label: "Confirm Password",
confirm_password_placeholder: "Enter the same password",
encrypt: "Encrypt",
skip: "Skip",
error_match: "Passwords do not match"
},
decrypt: {
title: "Enter your password",
decrypt_wallet: "Decrypt Wallet",
forgot_password_link: "Forgot Password?",
error_wrong_password: "Invalid Password"
},
lnurl_auth: {
title: "LNURL Auth",
auth: "Auth",
expected: "Expecting something like LNURL..."
},
plus: {
title: "Mutiny+",
join: "Join",
sats_per_month: "for {{amount}} sats a month.",
lightning_balance:
"You'll need at least {{amount}} sats in your lightning balance to get started. Try before you buy!",
restore: "Restore Subscription",
ready_to_join: "Ready to join",
click_confirm: "Click confirm to pay for your first month.",
open_source: "Mutiny is open source and self-hostable.",
optional_pay: "But also you can pay for it.",
paying_for: "Paying for",
supports_dev:
"helps support ongoing development and unlocks early access to new features and premium functionality:",
thanks: "You're part of the mutiny! Enjoy the following perks:",
renewal_time: "You'll get a renewal payment request around",
cancel: "To cancel your subscription just don't pay. You can also disable the Mutiny+",
wallet_connection: "Wallet Connection.",
subscribe: "Subscribe",
error_no_plan: "No plans found",
error_failure: "Couldn't subscribe",
error_no_subscription: "No existing subscription found",
satisfaction: "Smug satisfaction",
gifting: "Gifting",
multi_device: "Multi-device access",
more: "... and more to come"
}, },
plus: {},
restore: { restore: {
title: "Restore" title: "Restore",
all_twelve: "You need to enter all 12 words",
wrong_word: "Wrong word",
paste: "Dangerously Paste from Clipboard",
confirm_text:
"Are you sure you want to restore to this wallet? Your existing wallet will be deleted!",
restore_tip:
"You can restore an existing Mutiny Wallet from your 12 word seed phrase. This will replace your existing wallet, so make sure you know what you're doing!",
multi_browser_warning:
"Do not use on multiple browsers at the same time.",
error_clipboard: "Clipboard not supported",
error_word_number: "Wrong number of words",
error_invalid_seed: "Invalid seed phrase"
}, },
servers: { servers: {
title: "Servers", title: "Servers",
caption: "Don't trust us! Use your own servers to back Mutiny." caption: "Don't trust us! Use your own servers to back Mutiny.",
link: "Learn more about self-hosting",
proxy_label: "Websockets Proxy",
proxy_caption:
"How your lightning node communicates with the rest of the network.",
error_proxy: "Should be a url starting with wss://",
esplora_label: "Esplora",
esplora_caption: "Block data for on-chain information.",
error_esplora: "That doesn't look like a URL",
rgs_label: "RGS",
rgs_caption:
"Rapid Gossip Sync. Network data about the lightning network used for routing.",
error_rgs: "That doesn't look like a URL",
lsp_label: "LSP",
lsp_caption:
"Lightning Service Provider. Automatically opens channels to you for inbound liquidity. Also wraps invoices for privacy.",
error_lsp: "That doesn't look like a URL",
save: "Save"
} }
}, },
swap: { swap: {
@@ -210,26 +417,139 @@ export default {
confirm_swap: "Confirm Swap" confirm_swap: "Confirm Swap"
}, },
error: { error: {
title: "Error",
emergency_link: "emergency kit.",
restart: {
title: "Something *extra* screwy going on? Stop the nodes!",
start: "Start",
stop: "Stop"
},
general: {
oh_no: "Oh no!",
never_should_happen: "This never should've happened",
try_reloading:
"Try reloading this page or clicking the &rdquo;Dangit&rdquo; button. If you keep having problems,",
support_link: "reach out to us for support.",
getting_desperate: "Getting desperate? Try the"
},
load_time: { load_time: {
stuck: "Stuck on this screen? Try reloading. If that doesn't work, check out the", stuck: "Stuck on this screen? Try reloading. If that doesn't work, check out the"
emergency_link: "emergency kit."
}, },
not_found: { not_found: {
title: "Not Found", title: "Not Found",
wtf_paul: "This is probably Paul's fault." wtf_paul: "This is probably Paul's fault."
},
reset_router: {
payments_failing:
"Failing to make payments? Try resetting the lightning router.",
reset_router: "Reset Router"
},
resync: {
incorrect_balance:
"On-chain balance seems incorrect? Try re-syncing the on-chain wallet.",
resync_wallet: "Resync wallet"
},
on_boot: {
existing_tab: {
title: "Multiple tabs detected",
description:
"Mutiny currently only supports use in one tab at a time. It looks like you have another tab open with Mutiny running. Please close that tab and refresh this page, or close this tab and refresh the other one."
},
incompatible_browser: {
title: "Incompatible browser",
header: "Incompatible browser detected",
description:
"Mutiny requires a modern browser that supports WebAssembly, LocalStorage, and IndexedDB. Some browsers disable these features in private mode.",
try_different_browser:
"Please make sure your browser supports all these features, or consider trying another browser. You might also try disabling certain extensions or &rdquo;shields&rdquo; that block these features.",
browser_storage:
"(We'd love to support more private browsers, but we have to save your wallet data to browser storage or else you will lose funds.)",
browsers_link: "Supported Browsers"
},
loading_failed: {
title: "Failed to load",
header: "Failed to load Mutiny",
description:
"Something went wrong while booting up Mutiny Wallet.",
repair_options:
"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",
support_link: "reach out to us for support."
}
} }
}, },
create_an_issue: "Create an issue", modals: {
send_bitcoin: "Send Bitcoin", share: "Share",
view_transaction: "View Transaction", details: "Details",
why: "Why?", loading: {
more_info_modal_p1: loading: "Loading: {{stage}}",
"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.", default: "Just getting started",
more_info_modal_p2: double_checking: "Double checking something",
"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.", downloading: "Downloading",
learn_more_about_liquidity: "Learn more about liquidity", setup: "Setup",
whats_with_the_fees: "What's with the fees?", done: "Done"
private_tags: "Private tags", },
continue: "Continue", onboarding: {
keep_mutiny_open: "Keep Mutiny open to complete the payment." welcome: "Welcome!",
restore_from_backup:
"If you've used Mutiny before you can restore from a backup. Otherwise you can skip this and enjoy your new wallet!",
not_available: "We don't do that yet",
secure_your_funds: "Secure your funds",
make_backup:
"You have money stored in this browser. Let's make sure you have a backup."
},
beta_warning: {
title: "Warning: beta software",
beta_warning:
"We're so glad you're here. But we do want to warn you: Mutiny Wallet is in beta, and there are still bugs and rough edges.",
be_careful:
"Please be careful and don't put more money into Mutiny than you're willing to lose.",
beta_link: "Learn more about the beta",
pretend_money:
"If you want to use pretend money to test out Mutiny without risk,",
signet_link: "check out our Signet version."
},
transaction_details: {
lightning_receive: "Lightning receive",
lightning_send: "Lightning send",
channel_open: "Channel open",
channel_close: "Channel close",
onchain_receive: "On-chain receive",
onchain_send: "On-chain send",
paid: "Paid",
unpaid: "Unpaid",
status: "Status",
when: "When",
description: "Description",
fee: "Fee",
fees: "Fees",
bolt11: "Bolt11",
payment_hash: "Payment Hash",
preimage: "Preimage",
txid: "Txid",
balance: "Balance",
reserve: "Reserve",
peer: "Peer",
channel_id: "Channel ID",
reason: "Reasson",
confirmed: "Confirmed",
unconfirmed: "Unconfirmed",
no_details:
"No channel details found, which means this channel has likely been closed."
},
more_info: {
whats_with_the_fees: "What's with the fees?",
self_custodial:
"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.",
future_payments:
"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.",
liquidity: "Learn more about liquidity"
},
confirm_dialog: {
are_you_sure: "Are you sure?",
cancel: "Cancel",
confirm: "Confirm"
}
}
}; };

545
src/i18n/ko/translations.ts Normal file
View File

@@ -0,0 +1,545 @@
export default {
common: {
title: "Mutiny 지갑",
nice: "멋지다",
home: "홈",
sats: "SATS",
sat: "SAT",
usd: "USD",
fee: "수수료",
send: "보내기",
receive: "받기",
dangit: "땡글",
back: "뒤로",
coming_soon: "(곧 출시 예정)",
copy: "복사",
copied: "복사됨",
continue: "계속",
error_unimplemented: "미구현",
why: "왜?",
view_transaction: "거래 보기",
private_tags: "비공개 태그",
pending: "대기 중"
},
contacts: {
new: "새로 만들기",
add_contact: "연락처 추가",
new_contact: "새 연락처",
create_contact: "연락처 생성",
edit_contact: "연락처 수정",
save_contact: "연락처 저장",
payment_history: "결제 기록",
no_payments: "아직 결제 기록이 없습니다.",
edit: "수정",
pay: "지불",
name: "이름",
placeholder: "사토시",
unimplemented: "미구현",
not_available: "아직 제공되지 않습니다.",
error_name: "이름은 필수입니다."
},
receive: {
receive_bitcoin: "비트코인 받기",
edit: "수정",
checking: "확인 중",
choose_format: "포맷 선택",
payment_received: "결제 완료",
payment_initiated: "결제 시작됨",
receive_add_the_sender: "송신자를 기록에 추가하세요.",
choose_payment_format: "결제 포맷 선택",
unified_label: "통합",
unified_caption:
"비트코인 주소와 라이트닝 인보이스를 결합합니다. 송신자가 결제 방법을 선택합니다.",
lightning_label: "라이트닝 인보이스",
lightning_caption:
"작은 거래에 적합합니다. 보통 온체인 수수료보다 낮습니다.",
onchain_label: "비트코인 주소",
onchain_caption:
"온체인, 사토시가 한 것처럼. 아주 큰 거래에 적합합니다.",
unified_setup_fee:
"라이트닝으로 지불하는 경우 {{amount}} SATS의 라이트닝 설치 비용이 부과됩니다.",
lightning_setup_fee:
"이 받기에는 {{amount}} SATS의 라이트닝 설치 비용이 부과됩니다.",
amount: "금액",
fee: "+ 수수료",
total: "합계",
spendable: "사용 가능",
channel_size: "채널 크기",
channel_reserve: "- 채널 예비금",
amount_editable: {
receive_too_small:
"첫 라이트닝 받기는 {{amount}} SATS 이상이어야 합니다. 요청한 금액에서 설정 비용이 차감됩니다.",
setup_fee_lightning:
"라이트닝으로 지불하는 경우 라이트닝 설치 비용이 부과됩니다.",
too_big_for_beta:
"많은 SATS입니다. Mutiny Wallet이 여전히 베타 버전임을 알고 계시겠죠?",
more_than_21m: "비트코인은 총 2,100만 개밖에 없습니다.",
set_amount: "금액 설정",
max: "최대",
fix_amounts: {
ten_k: "1만",
one_hundred_k: "10만",
one_million: "100만"
},
del: "삭제"
},
integrated_qr: {
onchain: "온체인",
lightning: "라이트닝",
unified: "통합"
}
},
send: {
sending: "보내는 중...",
confirm_send: "보내기 확인",
contact_placeholder: "기록을 위해 수신자 추가",
start_over: "다시 시작",
paste: "붙여넣기",
scan_qr: "QR 스캔",
payment_initiated: "결제 시작됨",
payment_sent: "결제 완료",
destination: "수신처",
progress_bar: {
of: "/",
sats_sent: "SATS 보냄"
},
error_low_balance: "지불할 금액보다 충분한 잔액이 없습니다.",
error_clipboard: "클립보드를 지원하지 않습니다.",
error_keysend: "KeySend 실패",
error_LNURL: "LNURL Pay 실패"
},
feedback: {
header: "피드백 주세요!",
received: "피드백이 수신되었습니다!",
thanks: "문제가 발생했음을 알려주셔서 감사합니다.",
more: "더 하실 말씀이 있으신가요?",
tracking:
"Mutiny는 사용자 행동을 추적하거나 감시하지 않기 때문에 피드백이 매우 유용합니다.",
github_one: "GitHub에 익숙하시다면",
github_two: "를 사용하여",
create_issue: "이슈를 생성하세요",
link: "피드백?",
feedback_placeholder: "버그, 기능 요청, 피드백 등",
info_label: "연락처 정보 포함",
info_caption: "문제에 대한 후속 조치를 필요로 하는 경우",
email: "이메일",
email_caption: "일회용 이메일 사용 가능",
nostr: "Nostr",
nostr_caption: "신선한 npub",
nostr_label: "Nostr npub 또는 NIP-05",
send_feedback: "피드백 보내기",
invalid_feedback: "피드백을 입력하세요.",
need_contact: "연락처 정보가 필요합니다.",
invalid_email: "올바른 이메일 주소가 아닙니다.",
error: "피드백 전송 오류",
try_again: "나중에 다시 시도하세요."
},
activity: {
title: "활동",
mutiny: "Mutiny",
nostr: "Nostr",
view_all: "전체 보기",
receive_some_sats_to_get_started: "시작하려면 일부 SATS를 받으세요",
channel_open: "채널 오픈",
channel_close: "채널 닫기",
unknown: "알 수 없음",
import_contacts:
"Nostr에서 연락처를 가져와 누가 체널을 열고 있는지 확인하세요.",
coming_soon: "곧 출시 예정"
},
redshift: {
title: "레드시프트",
unknown: "알 수 없음",
what_happened: "무슨 일이 발생했나요?",
where_this_goes: "이것은 어디로 가나요?",
watch_it_go: "보기",
choose_your: "선택하세요",
utxo_to_begin: "시작할 UTXO",
unshifted_utxo: "전환되지 않은 UTXO",
redshifted: "레드시프트된",
utxos: "UTXO",
no_utxos_empty_state: "UTXO가 없습니다.",
utxo_label: "UTXO",
utxo_caption: "새 UTXO와 교환하세요.",
lightning_label: "라이트닝",
lightning_caption: "UTXO를 라이트닝으로 전환하세요.",
oh_dear: "오 디얼!",
here_is_error: "다음과 같은 오류가 발생했습니다:"
},
scanner: {
paste: "붙여넣기",
cancel: "취소"
},
settings: {
header: "설정",
support: "Mutiny 지원 방법 알아보기",
general: "일반",
beta_features: "베타 기능",
debug_tools: "디버그 도구",
danger_zone: "위험 지역",
admin: {
title: "관리자 페이지",
caption: "내부 디버그 도구입니다. 신중하게 사용하세요!",
header: "비밀 디버그 도구",
warning_one: "잘 알고 있는 경우 올바른 위치입니다.",
warning_two:
"디버그 및 테스트에 사용하는 내부 도구입니다. 주의하세요!",
kitchen_sink: {
disconnect: "연결 끊기",
peers: "피어",
no_peers: "피어 없음",
refresh_peers: "피어 새로고침",
connect_peer: "피어 연결",
expect_a_value: "값을 입력하세요...",
connect: "연결",
close_channel: "채널 종료",
force_close: "강제 종료",
abandon_channel: "채널 포기",
confirm_close_channel: "이 채널을 종료하시겠습니까?",
confirm_force_close:
"이 채널을 강제로 종료하시겠습니까? 자금은 몇 일 이후에 체인상에서 사용 가능해집니다.",
confirm_abandon_channel:
"이 채널을 포기하시겠습니까? 대개 개방 트랜잭션이 확인되지 않는다면 이렇게 하십시오. 그렇지 않으면 자금을 잃게 될 수 있습니다.",
channels: "채널",
no_channels: "채널 없음",
refresh_channels: "채널 새로고침",
pubkey: "퍼블릭 키",
amount: "금액",
open_channel: "채널 개설",
nodes: "노드",
no_nodes: "노드 없음"
}
},
backup: {
title: "백업",
secure_funds: "자금을 안전하게 보호하세요.",
twelve_words_tip:
"12개의 단어를 보여드립니다. 12개의 단어를 기록하세요.",
warning_one:
"브라우저 기록을 지우거나 기기를 분실하면 이 12개의 단어만으로 지갑을 복원할 수 있습니다.",
warning_two:
"Mutiny는 사용자의 자산을 사용자 스스로 관리해야 합니다...",
confirm: "12개의 단어를 기록했습니다.",
responsibility: "자금이 사용자 스스로의 책임임을 이해합니다.",
liar: "속이려는 것이 아닙니다.",
seed_words: {
reveal: "씨드 단어 공개",
hide: "숨기기",
copy: "클립보드에 복사",
copied: "복사됨!"
}
},
channels: {
title: "라이트닝 채널",
outbound: "송신",
inbound: "수신",
have_channels: "라이트닝 채널이",
have_channels_one: "개 있습니다.",
have_channels_many: "개 있습니다.",
inbound_outbound_tip:
"송신은 라이트닝으로 지출할 수 있는 금액을 나타냅니다. 수신은 수수료 없이 받을 수 있는 금액을 나타냅니다.",
no_channels:
"아직 채널이 없는 것 같습니다. 먼저 라이트닝으로 몇 sats를 받거나 체인상 자금을 채널로 바꾸세요. 시작해보세요!"
},
connections: {
title: "지갑 연결",
error_name: "이름을 입력하세요.",
error_connection: "지갑 연결 생성에 실패했습니다.",
add_connection: "연결 추가",
manage_connections: "연결 관리",
disable_connection: "비활성화",
enable_connection: "활성화",
new_connection: "새로운 연결",
new_connection_label: "이름",
new_connection_placeholder: "내가 좋아하는 nostr 클라이언트...",
create_connection: "연결 생성",
authorize:
"외부 서비스가 지갑에서 결제를 요청할 수 있도록 인증합니다. nostr 클라이언트와 잘 맞습니다.",
pending_nwc: {
title: "대기 중인 요청",
configure_link: "설정"
}
},
emergency_kit: {
title: "비상 키트",
caption: "지갑 문제를 진단하고 해결하는 도구입니다.",
emergency_tip:
"지갑이 망가지는 것 같다면 이 도구를 사용하여 문제를 진단하고 해결하세요.",
questions:
"이 버튼들이 무엇을 하는지 궁금하다면, 지원을 받으시려면",
link: "연락처를 통해 문의해주세요.",
import_export: {
title: "지갑 상태 내보내기",
error_password: "비밀번호가 필요합니다.",
error_read_file: "파일 읽기 오류",
error_no_text: "파일에서 텍스트를 찾을 수 없습니다.",
tip: "Mutiny 지갑 상태 전체를 파일로 내보내서 새 브라우저에 가져와서 복원할 수 있습니다. 보통 동작합니다!",
caveat_header: "주의 사항:",
caveat: "내보낸 후에는 원래 브라우저에서 아무 동작도 수행하지 마세요. 그렇게 하면 다시 내보내야 합니다. 성공적인 가져오기 후에는 원래 브라우저의 상태를 초기화하는 것이 좋습니다.",
save_state: "상태를 파일로 저장",
import_state: "파일에서 상태 가져오기",
confirm_replace: "상태를 다음으로 대체하시겠습니까?",
password: "복호화를 위해 비밀번호 입력",
decrypt_wallet: "지갑 복호화"
},
logs: {
title: "디버그 로그 다운로드",
something_screwy: "문제가 발생했나요? 로그를 확인하세요!",
download_logs: "로그 다운로드"
},
delete_everything: {
delete: "모두 삭제",
confirm: "노드 상태가 모두 삭제됩니다. 복구할 수 없습니다!",
deleted: "삭제됨",
deleted_description: "모든 데이터 삭제됨"
}
},
encrypt: {
header: "시드 단어 암호화",
hot_wallet_warning:
"Mutiny는 &rdquo;핫 월렛&rdquo;이므로 시드 단어를 사용하여 작동하지만 선택적으로 비밀번호로 암호화할 수 있습니다.",
password_tip:
"이렇게 하면 다른 사람이 브라우저에 접근하더라도 자금에 접근할 수 없습니다.",
optional: "(선택 사항)",
existing_password: "기존 비밀번호",
existing_password_caption:
"비밀번호를 설정하지 않았다면 비워 두세요.",
new_password_label: "비밀번호",
new_password_placeholder: "비밀번호를 입력하세요",
new_password_caption:
"이 비밀번호는 시드 단어를 암호화하는 데 사용됩니다. 이를 잊어버리면 자금에 접근하려면 시드 단어를 다시 입력해야 합니다. 시드 단어를 기록해 두었나요?",
confirm_password_label: "비밀번호 확인",
confirm_password_placeholder: "동일한 비밀번호를 입력하세요",
encrypt: "암호화",
skip: "건너뛰기",
error_match: "비밀번호가 일치하지 않습니다."
},
decrypt: {
title: "비밀번호를 입력하세요",
decrypt_wallet: "지갑 복호화",
forgot_password_link: "비밀번호를 잊으셨나요?",
error_wrong_password: "유효하지 않은 비밀번호"
},
lnurl_auth: {
title: "LNURL 인증",
auth: "인증",
expected: "LNURL과 같은 형식으로 입력해주세요."
},
plus: {
title: "Mutiny+",
join: "가입",
for: "에 대해",
sats_per_month: "sats 월별 비용입니다.",
you_need: "적어도 다음 금액이 필요합니다.",
lightning_balance:
"라이트닝 잔액에서 sats를 지불하세요. 먼저 시험해보세요!",
restore: "구독 복원",
ready_to_join: "가입할 준비가 되었습니다.",
click_confirm: "첫 달 비용을 지불하려면 확인을 클릭하세요.",
open_source: "Mutiny는 오픈 소스이며 스스로 호스팅할 수 있습니다.",
optional_pay: "또한 지불할 수도 있습니다.",
paying_for: "지불 대상:",
supports_dev:
"는 지속적인 개발을 지원하고 새 기능과 프리미엄 기능의 조기 액세스를 제공합니다.",
thanks: "Mutiny의 일원이 되셨습니다! 다음 혜택을 즐기세요:",
renewal_time: "다음 시기에 갱신 요청이 도착합니다.",
cancel: "구독을 취소하려면 결제하지 않으세요. 또는 Mutiny+ 기능을 비활성화할 수도 있습니다.",
wallet_connection: "지갑 연결 기능.",
subscribe: "구독하기",
error_no_plan: "",
error_failure: "",
error_no_subscription: "기존 구독이 없습니다.",
satisfaction: "만족함",
gifting: "선물하기",
multi_device: "다중 장치 접속",
more: "... 그리고 더 많은 기능이 추가될 예정입니다."
},
restore: {
title: "복원",
all_twelve: "12개 단어를 모두 입력해야 합니다.",
wrong_word: "잘못된 단어",
paste: "클립보드에서 붙여넣기 (위험)",
confirm_text:
"이 지갑으로 복원하시겠습니까? 기존 지갑이 삭제됩니다!",
restore_tip:
"기존 Mutiny 지갑을 12개의 씨드 단어로 복원할 수 있습니다. 기존 지갑이 대체됩니다. 신중하게 사용하세요!",
multi_browser_warning: "여러 브라우저에서 동시에 사용하지 마세요.",
error_clipboard: "클립보드를 지원하지 않습니다.",
error_word_number: "잘못된 단어 개수",
error_invalid_seed: "잘못된 씨드 단어입니다."
},
servers: {
title: "서버",
caption:
"우리를 믿지 마세요! Mutiny를 백업하기 위해 자체 서버를 사용하세요.",
link: "자체 호스팅에 대해 자세히 알아보기",
proxy_label: "웹소켓 프록시",
proxy_caption: "라이트닝 노드가 네트워크와 통신하는 방법입니다.",
error_proxy: "wss://로 시작하는 URL이어야 합니다.",
esplora_label: "Esplora",
esplora_caption: "온체인 정보를 위한 블록 데이터입니다.",
error_esplora: "URL처럼 보이지 않습니다.",
rgs_label: "RGS",
rgs_caption:
"Rapid Gossip Sync. 라우팅을 위해 사용되는 라이트닝 네트워크에 대한 네트워크 데이터입니다.",
error_rgs: "URL처럼 보이지 않습니다.",
lsp_label: "LSP",
lsp_caption:
"라이트닝 서비스 공급자. 인바운드 유동성을 위해 자동으로 채널을 열고, 개인 정보 보호를 위해 인보이스를 래핑합니다.",
error_lsp: "URL처럼 보이지 않습니다.",
save: "저장"
}
},
swap: {
peer_not_found: "피어를 찾을 수 없음",
channel_too_small:
"{{amount}} sats보다 작은 채널을 만드는 것은 그저 어리석은 짓입니다.",
insufficient_funds: "이 채널을 만들기에 충분한 자금이 없습니다.",
header: "라이트닝으로 스왑",
initiated: "스왑 시작됨",
sats_added: "sats가 라이트닝 잔액에 추가됩니다.",
use_existing: "기존 피어 사용",
choose_peer: "피어 선택",
peer_connect_label: "새 피어 연결",
peer_connect_placeholder: "피어 연결 문자열",
connect: "연결",
connecting: "연결 중...",
confirm_swap: "스왑 확인"
},
error: {
title: "오류",
emergency_link: "긴급 킷.",
restart: "문제가 *더* 발생했나요? 노드를 중지하세요!",
general: {
oh_no: "앗!",
never_should_happen: "이런 일은 일어나면 안 됩니다.",
try_reloading:
"이 페이지를 새로 고치거나 &rdquo;얘들아&rdquo; 버튼을 눌러보세요. 계속해서 문제가 발생하면",
support_link: "지원을 요청하세요.",
getting_desperate: "좀 답답하신가요? 다음을 시도해보세요."
},
load_time: {
stuck: "이 화면에 멈춰있나요? 다시 로드해보세요. 그래도 동작하지 않으면 다음을 확인하세요."
},
not_found: {
title: "찾을 수 없음",
wtf_paul: "이건 아마 폴의 잘못입니다."
},
reset_router: {
payments_failing:
"결제 실패하고 있나요? 라이트닝 라우터를 초기화해보세요.",
reset_router: "라우터 초기화"
},
resync: {
incorrect_balance:
"온체인 잔액이 잘못된 것 같나요? 온체인 월렛을 다시 동기화해보세요.",
resync_wallet: "월렛 다시 동기화"
},
on_boot: {
existing_tab: {
title: "여러 탭 감지됨",
description:
"현재 Mutiny Wallet을 한 번에 한 탭에서만 사용할 수 있습니다. Mutiny가 실행 중인 다른 탭이 열려 있습니다. 해당 탭을 닫고 이 페이지를 새로 고치거나, 이 탭을 닫고 다른 탭을 새로 고치세요."
},
incompatible_browser: {
title: "호환되지 않는 브라우저",
header: "호환되지 않는 브라우저가 감지되었습니다.",
description:
"Mutiny Wallet은 WebAssembly, LocalStorage 및 IndexedDB를 지원하는 현대적인 브라우저를 필요로 합니다. 일부 브라우저는 이러한 기능을 비활성화하는 경우도 있습니다.",
try_different_browser:
"이러한 모든 기능을 지원하는 브라우저를 사용하는지 확인하거나 다른 브라우저를 시도하세요. 또는 이러한 기능을 차단하는 특정 확장 기능이나 &rdquo;보호 기능&rdquo;을 비활성화해보세요.",
browser_storage:
"(더 많은 프라이버시 브라우저를 지원하고 싶지만, 월렛 데이터를 브라우저 저장소에 저장해야 하므로 그렇게 할 수 없습니다. )",
browsers_link: "지원되는 브라우저"
},
loading_failed: {
title: "로드 실패",
header: "Mutiny 로드 실패",
description:
"Mutiny Wallet을 부팅하는 동안 문제가 발생했습니다.",
repair_options:
"월렛이 손상된 것 같다면, 디버그 및 복구를 시도하기 위한 몇 가지 도구입니다.",
questions: "이러한 버튼이 무엇을 하는지 궁금하다면,",
support_link: "지원을 요청하세요."
}
}
},
modals: {
share: "공유",
details: "상세정보",
loading: {
loading: "로딩 중:",
default: "시작 중",
double_checking: "검증 중",
downloading: "다운로드 중",
setup: "설정 중",
done: "완료"
},
onboarding: {
welcome: "환영합니다!",
restore_from_backup:
"이미 Mutiny를 사용한 적이 있으시다면 백업에서 복원할 수 있습니다. 그렇지 않다면 이 단계를 건너뛰고 새로운 지갑을 즐기실 수 있습니다!",
not_available: "아직 이 기능은 지원하지 않습니다",
secure_your_funds: "자금을 안전하게 보호하세요",
make_backup:
"이 브라우저에 자금이 저장되어 있습니다. 백업이 되어 있는지 확인해 봅시다."
},
beta_warning: {
title: "경고: 베타 버전 소프트웨어",
beta_warning:
"저희가 여러분을 여기서 맞이할 수 있게 되어 기쁩니다. 그러나 경고하고 싶습니다: Mutiny Wallet은 베타 버전이며 여전히 버그와 미흡한 점이 있을 수 있습니다.",
be_careful:
"Mutiny에 지금보다 더 많은 자금을 투자하지 않도록 주의하세요.",
beta_Link: "베타 버전에 대해 자세히 알아보기",
pretend_money:
"위험 없이 Mutiny를 테스트하려면 가상 자금을 사용하려면",
signet_link: "Signet 버전을 확인하세요."
},
transaction_details: {
lightning_receive: "라이트닝 입금",
lightning_send: "라이트닝 송금",
channel_open: "채널 개설",
channel_close: "채널 종료",
onchain_receive: "체인상 입금",
onchain_send: "체인상 송금",
paid: "지불 완료",
unpaid: "미지불",
status: "상태",
when: "시간",
description: "설명",
fee: "수수료",
fees: "수수료",
bolt11: "Bolt11",
payment_hash: "지불 해시",
preimage: "사전 이미지",
txid: "거래 ID",
balance: "잔고",
reserve: "리저브",
peer: "피어",
channel_id: "채널 ID",
reason: "이유",
confirmed: "확인됨",
unconfirmed: "확인 대기",
no_details:
"채널 상세정보를 찾을 수 없습니다. 이는 해당 채널이 종료된 것으로 보입니다."
},
more_info: {
whats_with_the_fees: "수수료는 어떻게 되나요?",
self_custodial:
"Mutiny는 자체 보관 월렛입니다. 라이트닝 지불을 시작하려면 라이트닝 채널을 개설해야 하며, 이는 최소 금액과 설정 비용이 필요합니다.",
future_payments:
"앞으로의 송금 및 입금은 일반 네트워크 수수료와 노말 서비스 수수료만 부과되며, 채널에 인바운드 용량이 부족한 경우에만 추가 수수료가 발생합니다.",
liquidity: "유동성에 대해 자세히 알아보기"
},
confirm_dialog: {
are_you_sure: "확실합니까?",
cancel: "취소",
confirm: "확인"
}
},
create_an_issue: "이슈 생성",
send_bitcoin: "비트코인 전송",
continue: "계속하기",
keep_mutiny_open: "결제를 완료하기 위해 Mutiny를 열어두세요."
};

View File

@@ -20,8 +20,10 @@ import { useMegaStore } from "~/state/megaStore";
import { Contact } from "@mutinywallet/mutiny-wasm"; import { Contact } from "@mutinywallet/mutiny-wasm";
import { showToast } from "~/components/Toaster"; import { showToast } from "~/components/Toaster";
import { LoadingShimmer } from "~/components/BalanceBox"; import { LoadingShimmer } from "~/components/BalanceBox";
import { useI18n } from "~/i18n/context";
function ContactRow() { function ContactRow() {
const i18n = useI18n();
const [state, _actions] = useMegaStore(); const [state, _actions] = useMegaStore();
const [contacts, { refetch }] = createResource(async () => { const [contacts, { refetch }] = createResource(async () => {
try { try {
@@ -55,7 +57,7 @@ function ContactRow() {
// //
async function saveContact(_contact: ContactFormValues) { async function saveContact(_contact: ContactFormValues) {
showToast(new Error("Unimplemented")); showToast(new Error(i18n.t("common.error_unimplemented")));
// await editContact(contact) // await editContact(contact)
refetch(); refetch();
} }
@@ -84,27 +86,28 @@ const TAB =
"flex-1 inline-block px-8 py-4 text-lg font-semibold rounded-lg ui-selected:bg-white/10 bg-neutral-950 hover:bg-white/10"; "flex-1 inline-block px-8 py-4 text-lg font-semibold rounded-lg ui-selected:bg-white/10 bg-neutral-950 hover:bg-white/10";
export default function Activity() { export default function Activity() {
const i18n = useI18n();
const [state, _actions] = useMegaStore(); const [state, _actions] = useMegaStore();
return ( return (
<MutinyWalletGuard> <MutinyWalletGuard>
<SafeArea> <SafeArea>
<DefaultMain> <DefaultMain>
<BackLink /> <BackLink />
<LargeHeader>Activity</LargeHeader> <LargeHeader>{i18n.t("activity.title")}</LargeHeader>
<ContactRow /> <ContactRow />
<Tabs.Root defaultValue="mutiny"> <Tabs.Root defaultValue="mutiny">
<Tabs.List class="relative flex justify-around mt-4 mb-8 gap-1 bg-neutral-950 p-1 rounded-xl"> <Tabs.List class="relative flex justify-around mt-4 mb-8 gap-1 bg-neutral-950 p-1 rounded-xl">
<Tabs.Trigger value="mutiny" class={TAB}> <Tabs.Trigger value="mutiny" class={TAB}>
Mutiny {i18n.t("activity.mutiny")}
</Tabs.Trigger> </Tabs.Trigger>
<Tabs.Trigger value="nostr" class={TAB}> <Tabs.Trigger value="nostr" class={TAB}>
Nostr {i18n.t("activity.nostr")}
</Tabs.Trigger> </Tabs.Trigger>
{/* <Tabs.Indicator class="absolute bg-m-blue transition-all bottom-[-1px] h-[2px]" /> */} {/* <Tabs.Indicator class="absolute bg-m-blue transition-all bottom-[-1px] h-[2px]" /> */}
</Tabs.List> </Tabs.List>
<Tabs.Content value="mutiny"> <Tabs.Content value="mutiny">
{/* <MutinyActivity /> */} {/* <MutinyActivity /> */}
<Card title="Activity"> <Card title={i18n.t("activity.title")}>
<div class="p-1" /> <div class="p-1" />
<VStack> <VStack>
<Suspense> <Suspense>
@@ -122,11 +125,10 @@ export default function Activity() {
<VStack> <VStack>
<div class="my-8 flex flex-col items-center gap-4 text-center max-w-[20rem] mx-auto"> <div class="my-8 flex flex-col items-center gap-4 text-center max-w-[20rem] mx-auto">
<NiceP> <NiceP>
Import your contacts from nostr to see {i18n.t("activity.import_contacts")}
who they're zapping.
</NiceP> </NiceP>
<Button disabled intent="blue"> <Button disabled intent="blue">
Coming soon {i18n.t("activity.coming_soon")}
</Button> </Button>
</div> </div>
</VStack> </VStack>

View File

@@ -145,7 +145,7 @@ function FeedbackForm(props: { onSubmitted: () => void }) {
if (!res.ok) { if (!res.ok) {
throw new Error( throw new Error(
`${i18n.t("feedback.error")}: ${res.statusText}` i18n.t("feedback.error", { error: `: ${res.statusText}` })
); );
} }
@@ -155,9 +155,9 @@ function FeedbackForm(props: { onSubmitted: () => void }) {
props.onSubmitted(); props.onSubmitted();
} else { } else {
throw new Error( throw new Error(
`${i18n.t("feedback.error")}. ${i18n.t( i18n.t("feedback.error", {
"feedback.try_again" error: `. ${i18n.t("feedback.try_again")}`
)}` })
); );
} }
} catch (e) { } catch (e) {
@@ -327,11 +327,10 @@ export default function Feedback() {
<LargeHeader>{i18n.t("feedback.header")}</LargeHeader> <LargeHeader>{i18n.t("feedback.header")}</LargeHeader>
<NiceP>{i18n.t("feedback.tracking")}</NiceP> <NiceP>{i18n.t("feedback.tracking")}</NiceP>
<NiceP> <NiceP>
{i18n.t("feedback.github_one")}{" "} {i18n.t("feedback.github")}{" "}
<ExternalLink href="https://github.com/MutinyWallet/mutiny-web/issues"> <ExternalLink href="https://github.com/MutinyWallet/mutiny-web/issues">
{i18n.t("feedback.create_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

@@ -27,7 +27,7 @@ import NavBar from "~/components/NavBar";
import { useMegaStore } from "~/state/megaStore"; import { useMegaStore } from "~/state/megaStore";
import { objectToSearchParams } from "~/utils/objectToSearchParams"; import { objectToSearchParams } from "~/utils/objectToSearchParams";
import mempoolTxUrl from "~/utils/mempoolTxUrl"; import mempoolTxUrl from "~/utils/mempoolTxUrl";
import { AmountSats, AmountFiat, AmountSmall } from "~/components/Amount"; import { AmountSats, AmountFiat } from "~/components/Amount";
import { BackLink } from "~/components/layout/BackLink"; import { BackLink } from "~/components/layout/BackLink";
import { TagEditor } from "~/components/TagEditor"; import { TagEditor } from "~/components/TagEditor";
import { StyledRadioGroup } from "~/components/layout/Radio"; import { StyledRadioGroup } from "~/components/layout/Radio";
@@ -44,9 +44,9 @@ import { InfoBox } from "~/components/InfoBox";
import { FeesModal } from "~/components/MoreInfoModal"; import { FeesModal } from "~/components/MoreInfoModal";
import { IntegratedQr } from "~/components/IntegratedQR"; import { IntegratedQr } from "~/components/IntegratedQR";
import side2side from "~/assets/icons/side-to-side.svg"; import side2side from "~/assets/icons/side-to-side.svg";
import { useI18n } from "~/i18n/context";
import eify from "~/utils/eify"; import eify from "~/utils/eify";
import { matchError } from "~/logic/errorDispatch"; import { matchError } from "~/logic/errorDispatch";
import { useI18n } from "~/i18n/context";
import { Fee } from "~/components/Fee"; import { Fee } from "~/components/Fee";
type OnChainTx = { type OnChainTx = {
@@ -73,48 +73,30 @@ type OnChainTx = {
}; };
}; };
const RECEIVE_FLAVORS = [
{
value: "unified",
label: "Unified",
caption:
"Combines a bitcoin address and a lightning invoice. Sender chooses payment method."
},
{
value: "lightning",
label: "Lightning invoice",
caption:
"Ideal for small transactions. Usually lower fees than on-chain."
},
{
value: "onchain",
label: "Bitcoin address",
caption:
"On-chain, just like Satoshi did it. Ideal for very large transactions."
}
];
export type ReceiveFlavor = "unified" | "lightning" | "onchain"; export type ReceiveFlavor = "unified" | "lightning" | "onchain";
type ReceiveState = "edit" | "show" | "paid"; type ReceiveState = "edit" | "show" | "paid";
type PaidState = "lightning_paid" | "onchain_paid"; type PaidState = "lightning_paid" | "onchain_paid";
function FeeWarning(props: { fee: bigint; flavor: ReceiveFlavor }) { function FeeWarning(props: { fee: bigint; flavor: ReceiveFlavor }) {
const i18n = useI18n();
return ( return (
// TODO: probably won't always be fixed 2500? // TODO: probably won't always be fixed 2500?
<Show when={props.fee > 1000n}> <Show when={props.fee > 1000n}>
<Switch> <Switch>
<Match when={props.flavor === "unified"}> <Match when={props.flavor === "unified"}>
<InfoBox accent="blue"> <InfoBox accent="blue">
A lightning setup fee of{" "} {i18n.t("receive.unified_setup_fee", {
<AmountSmall amountSats={props.fee} /> will be charged amount: props.fee.toLocaleString()
if paid over lightning. <FeesModal /> })}
<FeesModal />
</InfoBox> </InfoBox>
</Match> </Match>
<Match when={props.flavor === "lightning"}> <Match when={props.flavor === "lightning"}>
<InfoBox accent="blue"> <InfoBox accent="blue">
A lightning setup fee of{" "} {i18n.t("receive.lightning_setup_fee", {
<AmountSmall amountSats={props.fee} /> will be charged amount: props.fee.toLocaleString()
for this receive. <FeesModal /> })}
<FeesModal />
</InfoBox> </InfoBox>
</Match> </Match>
</Switch> </Switch>
@@ -151,6 +133,24 @@ export default function Receive() {
// loading state for the continue button // loading state for the continue button
const [loading, setLoading] = createSignal(false); const [loading, setLoading] = createSignal(false);
const RECEIVE_FLAVORS = [
{
value: "unified",
label: i18n.t("receive.unified_label"),
caption: i18n.t("receive.unified_caption")
},
{
value: "lightning",
label: i18n.t("receive.lightning_label"),
caption: i18n.t("receive.lightning_caption")
},
{
value: "onchain",
label: i18n.t("receive.onchain_label"),
caption: i18n.t("receive.onchain_caption")
}
];
const receiveString = createMemo(() => { const receiveString = createMemo(() => {
if (unified() && receiveState() === "show") { if (unified() && receiveState() === "show") {
if (flavor() === "unified") { if (flavor() === "unified") {
@@ -350,7 +350,7 @@ export default function Receive() {
> >
<BackButton <BackButton
onClick={() => setReceiveState("edit")} onClick={() => setReceiveState("edit")}
title={`${i18n.t("receive.edit")}`} title={i18n.t("receive.edit")}
showOnDesktop showOnDesktop
/> />
</Show> </Show>
@@ -376,7 +376,7 @@ export default function Receive() {
exitRoute={amount() ? "/receive" : "/"} exitRoute={amount() ? "/receive" : "/"}
/> />
<Card title={i18n.t("private_tags")}> <Card title={i18n.t("common.private_tags")}>
<TagEditor <TagEditor
selectedValues={selectedValues()} selectedValues={selectedValues()}
setSelectedValues={setSelectedValues} setSelectedValues={setSelectedValues}
@@ -394,7 +394,7 @@ export default function Receive() {
onClick={onSubmit} onClick={onSubmit}
loading={loading()} loading={loading()}
> >
{i18n.t("continue")} {i18n.t("common.continue")}
</Button> </Button>
</div> </div>
</Match> </Match>
@@ -406,7 +406,7 @@ export default function Receive() {
kind={flavor()} kind={flavor()}
/> />
<p class="text-neutral-400 text-center"> <p class="text-neutral-400 text-center">
{i18n.t("keep_mutiny_open")} {i18n.t("receive.keep_mutiny_open")}
</p> </p>
{/* Only show method chooser when we have an invoice */} {/* Only show method chooser when we have an invoice */}
<Show when={bip21Raw()?.invoice}> <Show when={bip21Raw()?.invoice}>
@@ -420,7 +420,9 @@ export default function Receive() {
<img class="w-4 h-4" src={side2side} /> <img class="w-4 h-4" src={side2side} />
</button> </button>
<SimpleDialog <SimpleDialog
title="Choose payment format" title={i18n.t(
"receive.choose_payment_format"
)}
open={methodChooserOpen()} open={methodChooserOpen()}
setOpen={(open) => setOpen={(open) =>
setMethodChooserOpen(open) setMethodChooserOpen(open)
@@ -503,7 +505,7 @@ export default function Receive() {
network network
)} )}
> >
{i18n.t("view_transaction")} {i18n.t("common.view_transaction")}
</ExternalLink> </ExternalLink>
</Show> </Show>
</SuccessModal> </SuccessModal>

View File

@@ -44,6 +44,7 @@ import mempoolTxUrl from "~/utils/mempoolTxUrl";
import { AmountSats } from "~/components/Amount"; import { AmountSats } from "~/components/Amount";
import { getRedshifted, setRedshifted } from "~/utils/fakeLabels"; import { getRedshifted, setRedshifted } from "~/utils/fakeLabels";
import { Network } from "~/logic/mutinyWalletSetup"; import { Network } from "~/logic/mutinyWalletSetup";
import { useI18n } from "~/i18n/context";
type ShiftOption = "utxo" | "lightning"; type ShiftOption = "utxo" | "lightning";
@@ -87,6 +88,7 @@ const dummyRedshift: RedshiftResult = {
}; };
function RedshiftReport(props: { redshift: RedshiftResult; utxo: UtxoItem }) { function RedshiftReport(props: { redshift: RedshiftResult; utxo: UtxoItem }) {
const i18n = useI18n();
const [state, _actions] = useMegaStore(); const [state, _actions] = useMegaStore();
const getUtXos = async () => { const getUtXos = async () => {
@@ -155,7 +157,7 @@ function RedshiftReport(props: { redshift: RedshiftResult; utxo: UtxoItem }) {
</Show> </Show>
</VStack> */} </VStack> */}
<VStack> <VStack>
<NiceP>What happened?</NiceP> <NiceP>{i18n.t("redshift.what_happened")}</NiceP>
<Show when={redshiftResource()}> <Show when={redshiftResource()}>
<Card> <Card>
<VStack biggap> <VStack biggap>
@@ -164,22 +166,22 @@ function RedshiftReport(props: { redshift: RedshiftResult; utxo: UtxoItem }) {
<Utxo item={inputUtxo()!} /> <Utxo item={inputUtxo()!} />
</Show> </Show>
</KV> */} </KV> */}
<KV key="Starting amount"> <KV key={i18n.t("redshift.starting_amount")}>
<AmountSats <AmountSats
amountSats={redshiftResource()!.amount_sats} amountSats={redshiftResource()!.amount_sats}
/> />
</KV> </KV>
<KV key="Fees paid"> <KV key={i18n.t("redshift.fees_paid")}>
<AmountSats <AmountSats
amountSats={redshiftResource()!.fees_paid} amountSats={redshiftResource()!.fees_paid}
/> />
</KV> </KV>
<KV key="Change"> <KV key={i18n.t("redshift.change")}>
<AmountSats <AmountSats
amountSats={redshiftResource()!.change_amt} amountSats={redshiftResource()!.change_amt}
/> />
</KV> </KV>
<KV key="Outbound channel"> <KV key={i18n.t("redshift.outbound_channel")}>
<VStack> <VStack>
<pre class="whitespace-pre-wrap break-all"> <pre class="whitespace-pre-wrap break-all">
{ {
@@ -198,12 +200,12 @@ function RedshiftReport(props: { redshift: RedshiftResult; utxo: UtxoItem }) {
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >
View on mempool {i18n.t("common.view_transaction")}
</a> </a>
</VStack> </VStack>
</KV> </KV>
<Show when={redshiftResource()!.output_channel}> <Show when={redshiftResource()!.output_channel}>
<KV key="Return channel"> <KV key={i18n.t("redshift.return_channel")}>
<VStack> <VStack>
<pre class="whitespace-pre-wrap break-all"> <pre class="whitespace-pre-wrap break-all">
{redshiftResource()!.output_channel} {redshiftResource()!.output_channel}
@@ -219,7 +221,7 @@ function RedshiftReport(props: { redshift: RedshiftResult; utxo: UtxoItem }) {
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >
View on mempool {i18n.t("common.view_transaction")}
</a> </a>
</VStack> </VStack>
</KV> </KV>
@@ -232,20 +234,8 @@ function RedshiftReport(props: { redshift: RedshiftResult; utxo: UtxoItem }) {
); );
} }
const SHIFT_OPTIONS = [
{
value: "utxo",
label: "UTXO",
caption: "Trade your UTXO for a fresh UTXO"
},
{
value: "lightning",
label: "Lightning",
caption: "Convert your UTXO into Lightning"
}
];
export function Utxo(props: { item: UtxoItem; onClick?: () => void }) { export function Utxo(props: { item: UtxoItem; onClick?: () => void }) {
const i18n = useI18n();
const redshifted = createMemo(() => getRedshifted(props.item.outpoint)); const redshifted = createMemo(() => getRedshifted(props.item.outpoint));
return ( return (
<> <>
@@ -260,9 +250,15 @@ export function Utxo(props: { item: UtxoItem; onClick?: () => void }) {
<div class="flex gap-2"> <div class="flex gap-2">
<Show <Show
when={redshifted()} when={redshifted()}
fallback={<h2 class={MISSING_LABEL}>Unknown</h2>} fallback={
<h2 class={MISSING_LABEL}>
{i18n.t("redshift.unknown")}
</h2>
}
> >
<h2 class={REDSHIFT_LABEL}>Redshift</h2> <h2 class={REDSHIFT_LABEL}>
{i18n.t("redshift.title")}
</h2>
</Show> </Show>
</div> </div>
<SmallAmount amount={props.item.txout.value} /> <SmallAmount amount={props.item.txout.value} />
@@ -293,6 +289,7 @@ function ShiftObserver(props: {
setShiftStage: (stage: ShiftStage) => void; setShiftStage: (stage: ShiftStage) => void;
redshiftId: string; redshiftId: string;
}) { }) {
const i18n = useI18n();
const [_state, _actions] = useMegaStore(); const [_state, _actions] = useMegaStore();
const [fakeStage, _setFakeStage] = createSignal(2); const [fakeStage, _setFakeStage] = createSignal(2);
@@ -348,7 +345,7 @@ function ShiftObserver(props: {
return ( return (
<> <>
<NiceP>Watch it go!</NiceP> <NiceP>{i18n.t("redshift.watch_it_go")}</NiceP>
<Card> <Card>
<VStack> <VStack>
<pre class="self-center">{FAKE_STATES[fakeStage()]}</pre> <pre class="self-center">{FAKE_STATES[fakeStage()]}</pre>
@@ -370,6 +367,7 @@ const KV: ParentComponent<{ key: string }> = (props) => {
}; };
export default function Redshift() { export default function Redshift() {
const i18n = useI18n();
const [state, _actions] = useMegaStore(); const [state, _actions] = useMegaStore();
const [shiftStage, setShiftStage] = createSignal<ShiftStage>("choose"); const [shiftStage, setShiftStage] = createSignal<ShiftStage>("choose");
@@ -377,6 +375,19 @@ export default function Redshift() {
const [chosenUtxo, setChosenUtxo] = createSignal<UtxoItem>(); const [chosenUtxo, setChosenUtxo] = createSignal<UtxoItem>();
const SHIFT_OPTIONS = [
{
value: "utxo",
label: i18n.t("redshift.utxo_label"),
caption: i18n.t("redshift.utxo_caption")
},
{
value: "lightning",
label: i18n.t("redshift.lightning_label"),
caption: i18n.t("redshift.lightning_caption")
}
];
const getUtXos = async () => { const getUtXos = async () => {
console.log("Getting utxos"); console.log("Getting utxos");
return (await state.mutiny_wallet?.list_utxos()) as UtxoItem[]; return (await state.mutiny_wallet?.list_utxos()) as UtxoItem[];
@@ -439,14 +450,19 @@ export default function Redshift() {
<SafeArea> <SafeArea>
<DefaultMain> <DefaultMain>
<BackLink /> <BackLink />
<LargeHeader>Redshift (coming soon)</LargeHeader> <LargeHeader>
{i18n.t("redshift.title")}{" "}
{i18n.t("common.coming_soon")}
</LargeHeader>
<div class="relative filter grayscale pointer-events-none opacity-75"> <div class="relative filter grayscale pointer-events-none opacity-75">
<VStack biggap> <VStack biggap>
{/* <pre>{JSON.stringify(redshiftResource(), null, 2)}</pre> */} {/* <pre>{JSON.stringify(redshiftResource(), null, 2)}</pre> */}
<Switch> <Switch>
<Match when={shiftStage() === "choose"}> <Match when={shiftStage() === "choose"}>
<VStack> <VStack>
<NiceP>Where is this going?</NiceP> <NiceP>
{i18n.t("redshift.where_this_goes")}
</NiceP>
<StyledRadioGroup <StyledRadioGroup
accent="red" accent="red"
value={shiftType()} value={shiftType()}
@@ -460,7 +476,7 @@ export default function Redshift() {
</VStack> </VStack>
<VStack> <VStack>
<NiceP> <NiceP>
Choose your{" "} {i18n.t("redshift.choose_your")}{" "}
<span class="inline-block"> <span class="inline-block">
<img <img
class="h-4" class="h-4"
@@ -468,10 +484,14 @@ export default function Redshift() {
alt="sine wave" alt="sine wave"
/> />
</span>{" "} </span>{" "}
UTXO to begin {i18n.t("redshift.utxo_to_begin")}
</NiceP> </NiceP>
<Suspense> <Suspense>
<Card title="Unshifted UTXOs"> <Card
title={i18n.t(
"redshift.unshifted_utxo"
)}
>
<Switch> <Switch>
<Match when={utxos.loading}> <Match when={utxos.loading}>
<LoadingSpinner wide /> <LoadingSpinner wide />
@@ -485,8 +505,9 @@ export default function Redshift() {
} }
> >
<code> <code>
No utxos (empty {i18n.t(
state) "redshift.no_utxos_empty_state"
)}
</code> </code>
</Match> </Match>
<Match <Match
@@ -521,9 +542,13 @@ export default function Redshift() {
titleElement={ titleElement={
<SmallHeader> <SmallHeader>
<span class="text-m-red"> <span class="text-m-red">
Redshifted{" "} {i18n.t(
"redshift.redshifted"
)}{" "}
</span> </span>
UTXOs {i18n.t(
"redshift.utxos"
)}
</SmallHeader> </SmallHeader>
} }
> >
@@ -540,8 +565,9 @@ export default function Redshift() {
} }
> >
<code> <code>
No utxos (empty {i18n.t(
state) "redshift.no_utxos_empty_state"
)}
</code> </code>
</Match> </Match>
<Match <Match
@@ -594,15 +620,17 @@ export default function Redshift() {
intent="red" intent="red"
onClick={resetState} onClick={resetState}
> >
Nice {i18n.t("common.nice")}
</Button> </Button>
</VStack> </VStack>
</Match> </Match>
<Match when={shiftStage() === "failure"}> <Match when={shiftStage() === "failure"}>
<NiceP>Oh dear</NiceP> <NiceP>{i18n.t("redshift.oh_dear")}</NiceP>
<NiceP>Here's what happened:</NiceP> <NiceP>
{i18n.t("redshift.here_is_error")}
</NiceP>
<Button intent="red" onClick={resetState}> <Button intent="red" onClick={resetState}>
Dangit {i18n.t("common.dangit")}
</Button> </Button>
</Match> </Match>
</Switch> </Switch>

View File

@@ -132,9 +132,10 @@ function DestinationInput(props: {
handleDecode: () => void; handleDecode: () => void;
handlePaste: () => void; handlePaste: () => void;
}) { }) {
const i18n = useI18n();
return ( return (
<VStack> <VStack>
<SmallHeader>Destination</SmallHeader> <SmallHeader>{i18n.t("send.destination")}</SmallHeader>
<textarea <textarea
value={props.fieldDestination} value={props.fieldDestination}
onInput={(e) => { onInput={(e) => {
@@ -149,19 +150,19 @@ function DestinationInput(props: {
intent="blue" intent="blue"
onClick={props.handleDecode} onClick={props.handleDecode}
> >
Continue {i18n.t("common.continue")}
</Button> </Button>
<HStack> <HStack>
<Button onClick={props.handlePaste}> <Button onClick={props.handlePaste}>
<div class="flex flex-col gap-2 items-center"> <div class="flex flex-col gap-2 items-center">
<Paste /> <Paste />
<span>Paste</span> <span>{i18n.t("send.paste")}</span>
</div> </div>
</Button> </Button>
<ButtonLink href="/scanner"> <ButtonLink href="/scanner">
<div class="flex flex-col gap-2 items-center"> <div class="flex flex-col gap-2 items-center">
<Scan /> <Scan />
<span>Scan QR</span> <span>{i18n.t("send.scan_qr")}</span>
</div> </div>
</ButtonLink> </ButtonLink>
</HStack> </HStack>
@@ -260,9 +261,7 @@ export default function Send() {
if (source() === "lightning") { if (source() === "lightning") {
return ( return (
(state.balance?.lightning ?? 0n) <= amountSats() && (state.balance?.lightning ?? 0n) <= amountSats() &&
setError( setError(i18n.t("send.error_low_balance"))
"We do not have enough balance to pay the given amount."
)
); );
} }
}; };
@@ -395,7 +394,7 @@ export default function Send() {
text = value; text = value;
} else { } else {
if (!navigator.clipboard.readText) { if (!navigator.clipboard.readText) {
return showToast(new Error("Clipboard not supported")); return showToast(new Error(i18n.t("send.error_clipboard")));
} }
text = await navigator.clipboard.readText(); text = await navigator.clipboard.readText();
} }
@@ -486,7 +485,7 @@ export default function Send() {
// TODO: handle timeouts // TODO: handle timeouts
if (!payment?.paid) { if (!payment?.paid) {
throw new Error("Keysend failed"); throw new Error(i18n.t("send.error_keysend"));
} else { } else {
sentDetails.amount = amountSats(); sentDetails.amount = amountSats();
} }
@@ -501,7 +500,7 @@ export default function Send() {
); );
if (!payment?.paid) { if (!payment?.paid) {
throw new Error("Lnurl Pay failed"); throw new Error(i18n.t("send.error_LNURL"));
} else { } else {
sentDetails.amount = amountSats(); sentDetails.amount = amountSats();
} }
@@ -575,7 +574,7 @@ export default function Send() {
title={i18n.t("send.start_over")} title={i18n.t("send.start_over")}
/> />
</Show> </Show>
<LargeHeader>{i18n.t("send_bitcoin")}</LargeHeader> <LargeHeader>{i18n.t("send.send_bitcoin")}</LargeHeader>
<SuccessModal <SuccessModal
confirmText={ confirmText={
sentDetails()?.amount sentDetails()?.amount
@@ -596,7 +595,9 @@ export default function Send() {
<MegaEx /> <MegaEx />
<h1 class="w-full mt-4 mb-2 text-2xl font-semibold text-center md:text-3xl"> <h1 class="w-full mt-4 mb-2 text-2xl font-semibold text-center md:text-3xl">
{sentDetails()?.amount {sentDetails()?.amount
? "Payment Initiated" ? source() === "onchain"
? i18n.t("send.payment_initiated")
: i18n.t("send.payment_sent")
: sentDetails()?.failure_reason} : sentDetails()?.failure_reason}
</h1> </h1>
{/*TODO: add failure hint logic for different failure conditions*/} {/*TODO: add failure hint logic for different failure conditions*/}
@@ -605,7 +606,9 @@ export default function Send() {
<MegaCheck /> <MegaCheck />
<h1 class="w-full mt-4 mb-2 text-2xl font-semibold text-center md:text-3xl"> <h1 class="w-full mt-4 mb-2 text-2xl font-semibold text-center md:text-3xl">
{sentDetails()?.amount {sentDetails()?.amount
? "Payment Initiated" ? source() === "onchain"
? i18n.t("send.payment_initiated")
: i18n.t("send.payment_sent")
: sentDetails()?.failure_reason} : sentDetails()?.failure_reason}
</h1> </h1>
<div class="flex flex-col gap-1 items-center"> <div class="flex flex-col gap-1 items-center">
@@ -631,7 +634,7 @@ export default function Send() {
network network
)} )}
> >
{i18n.t("view_transaction")} {i18n.t("common.view_transaction")}
</ExternalLink> </ExternalLink>
</Show> </Show>
</Match> </Match>
@@ -652,7 +655,7 @@ export default function Send() {
setSource={setSource} setSource={setSource}
both={!!address() && !!invoice()} both={!!address() && !!invoice()}
/> />
<Card title="Destination"> <Card title={i18n.t("send.destination")}>
<VStack> <VStack>
<DestinationShower <DestinationShower
source={source()} source={source()}
@@ -664,7 +667,7 @@ export default function Send() {
clearAll={clearAll} clearAll={clearAll}
/> />
<SmallHeader> <SmallHeader>
{i18n.t("private_tags")} {i18n.t("common.private_tags")}
</SmallHeader> </SmallHeader>
<TagEditor <TagEditor
selectedValues={selectedContacts()} selectedValues={selectedContacts()}

View File

@@ -328,7 +328,7 @@ export default function Swap() {
network network
)} )}
> >
{i18n.t("view_transaction")} {i18n.t("common.view_transaction")}
</ExternalLink> </ExternalLink>
</Show> </Show>
{/* <pre>{JSON.stringify(channelOpenResult()?.channel?.value, null, 2)}</pre> */} {/* <pre>{JSON.stringify(channelOpenResult()?.channel?.value, null, 2)}</pre> */}

View File

@@ -108,7 +108,11 @@ function Nwc() {
activityLight={profile.enabled ? "on" : "off"} activityLight={profile.enabled ? "on" : "off"}
> >
<VStack> <VStack>
<KeyValue key="Relay"> <KeyValue
key={i18n.t(
"settings.connections.relay"
)}
>
<MiniStringShower <MiniStringShower
text={profile.relay} text={profile.relay}
/> />

View File

@@ -41,7 +41,9 @@ export default function Encrypt() {
validate: (values) => { validate: (values) => {
const errors: Record<string, string> = {}; const errors: Record<string, string> = {};
if (values.password !== values.confirmPassword) { if (values.password !== values.confirmPassword) {
errors.confirmPassword = "Passwords do not match"; errors.confirmPassword = i18n.t(
"settings.encrypt.error_match"
);
} }
return errors; return errors;
} }
@@ -73,18 +75,15 @@ export default function Encrypt() {
title={i18n.t("settings.header")} title={i18n.t("settings.header")}
/> />
<LargeHeader> <LargeHeader>
Encrypt your seed words (optional) {`${i18n.t("settings.encrypt.header")} ${i18n.t(
"settings.encrypt.optional"
)}`}
</LargeHeader> </LargeHeader>
<VStack> <VStack>
<NiceP> <NiceP>
Mutiny is a "hot wallet" so it needs your seed word {i18n.t("settings.encrypt.hot_wallet_warning")}
to operate, but you can optionally encrypt those
words with a password.
</NiceP>
<NiceP>
That way, if someone gets access to your browser,
they still won't have access to your funds.
</NiceP> </NiceP>
<NiceP>{i18n.t("settings.encrypt.password_tip")}</NiceP>
<Form onSubmit={handleFormSubmit}> <Form onSubmit={handleFormSubmit}>
<VStack> <VStack>
<Field name="existingPassword"> <Field name="existingPassword">
@@ -93,9 +92,17 @@ export default function Encrypt() {
{...props} {...props}
{...field} {...field}
type="password" type="password"
label="Existing Password (optional)" label={`${i18n.t(
placeholder="Existing password" "settings.encrypt.existing_password"
caption="Leave blank if you haven't set a password yet." )} ${i18n.t(
"settings.encrypt.optional"
)}`}
placeholder={i18n.t(
"settings.encrypt.existing_password"
)}
caption={i18n.t(
"settings.encrypt.existing_password_caption"
)}
/> />
)} )}
</Field> </Field>
@@ -105,9 +112,15 @@ export default function Encrypt() {
{...props} {...props}
{...field} {...field}
type="password" type="password"
label="Password" label={i18n.t(
placeholder="Enter a password" "settings.encrypt.new_password_label"
caption="This password will be used to encrypt your seed words. If you forget it, you will need to re-enter your seed words to access your funds. You did write down your seed words, right?" )}
placeholder={i18n.t(
"settings.encrypt.new_password_placeholder"
)}
caption={i18n.t(
"settings.encrypt.new_password_caption"
)}
/> />
)} )}
</Field> </Field>
@@ -117,8 +130,12 @@ export default function Encrypt() {
{...props} {...props}
{...field} {...field}
type="password" type="password"
label="Confirm Password" label={i18n.t(
placeholder="Enter the same password" "settings.encrypt.confirm_password_label"
)}
placeholder={i18n.t(
"settings.encrypt.confirm_password_placeholder"
)}
/> />
)} )}
</Field> </Field>
@@ -129,12 +146,12 @@ export default function Encrypt() {
</Show> </Show>
<div /> <div />
<Button intent="blue" loading={loading()}> <Button intent="blue" loading={loading()}>
Encrypt {i18n.t("settings.encrypt.encrypt")}
</Button> </Button>
</VStack> </VStack>
</Form> </Form>
<ButtonLink href="/settings" intent="green"> <ButtonLink href="/settings" intent="green">
Skip {i18n.t("settings.encrypt.skip")}
</ButtonLink> </ButtonLink>
</VStack> </VStack>
</DefaultMain> </DefaultMain>

View File

@@ -11,8 +11,10 @@ import {
} from "~/components/layout"; } from "~/components/layout";
import { BackLink } from "~/components/layout/BackLink"; import { BackLink } from "~/components/layout/BackLink";
import { useMegaStore } from "~/state/megaStore"; import { useMegaStore } from "~/state/megaStore";
import { useI18n } from "~/i18n/context";
export default function LnUrlAuth() { export default function LnUrlAuth() {
const i18n = useI18n();
const [state, _] = useMegaStore(); const [state, _] = useMegaStore();
const [value, setValue] = createSignal(""); const [value, setValue] = createSignal("");
@@ -30,8 +32,13 @@ export default function LnUrlAuth() {
<MutinyWalletGuard> <MutinyWalletGuard>
<SafeArea> <SafeArea>
<DefaultMain> <DefaultMain>
<BackLink href="/settings" title="Settings" /> <BackLink
<LargeHeader>LNURL Auth</LargeHeader> href="/settings"
title={i18n.t("settings.header")}
/>
<LargeHeader>
{i18n.t("settings.lnurl_auth.title")}
</LargeHeader>
<InnerCard> <InnerCard>
<form class="flex flex-col gap-4" onSubmit={onSubmit}> <form class="flex flex-col gap-4" onSubmit={onSubmit}>
<TextField.Root <TextField.Root
@@ -46,18 +53,18 @@ export default function LnUrlAuth() {
class="flex flex-col gap-4" class="flex flex-col gap-4"
> >
<TextField.Label class="text-sm font-semibold uppercase"> <TextField.Label class="text-sm font-semibold uppercase">
LNURL Auth {i18n.t("settings.lnurl_auth.title")}
</TextField.Label> </TextField.Label>
<TextField.Input <TextField.Input
class="w-full p-2 rounded-lg text-black" class="w-full p-2 rounded-lg text-black"
placeholder="LNURL..." placeholder="LNURL..."
/> />
<TextField.ErrorMessage class="text-red-500"> <TextField.ErrorMessage class="text-red-500">
Expecting something like LNURL... {i18n.t("settings.lnurl_auth.expected")}
</TextField.ErrorMessage> </TextField.ErrorMessage>
</TextField.Root> </TextField.Root>
<Button layout="small" type="submit"> <Button layout="small" type="submit">
Auth {i18n.t("settings.lnurl_auth.auth")}
</Button> </Button>
</form> </form>
</InnerCard> </InnerCard>

View File

@@ -26,28 +26,34 @@ import { useMegaStore } from "~/state/megaStore";
import eify from "~/utils/eify"; import eify from "~/utils/eify";
import party from "~/assets/party.gif"; import party from "~/assets/party.gif";
import { LoadingShimmer } from "~/components/BalanceBox"; import { LoadingShimmer } from "~/components/BalanceBox";
import { useI18n } from "~/i18n/context";
function Perks(props: { alreadySubbed?: boolean }) { function Perks(props: { alreadySubbed?: boolean }) {
const i18n = useI18n();
return ( return (
<ul class="list-disc ml-8 font-light text-lg"> <ul class="list-disc ml-8 font-light text-lg">
<Show when={props.alreadySubbed}> <Show when={props.alreadySubbed}>
<li>Smug satisfaction</li> <li>{i18n.t("settings.plus.satisfaction")}</li>
</Show> </Show>
<li> <li>
Redshift <em>(coming soon)</em> {i18n.t("redshift.title")}{" "}
<em>{i18n.t("common.coming_soon")}</em>
</li> </li>
<li> <li>
Gifting <em>(coming soon)</em> {i18n.t("settings.plus.gifting")}{" "}
<em>{i18n.t("common.coming_soon")}</em>
</li> </li>
<li> <li>
Multi-device access <em>(coming soon)</em> {i18n.t("settings.plus.multi_device")}{" "}
<em>{i18n.t("common.coming_soon")}</em>
</li> </li>
<li>... and more to come</li> <li>{i18n.t("settings.plus.more")}</li>
</ul> </ul>
); );
} }
function PlusCTA() { function PlusCTA() {
const i18n = useI18n();
const [state, actions] = useMegaStore(); const [state, actions] = useMegaStore();
const [subbing, setSubbing] = createSignal(false); const [subbing, setSubbing] = createSignal(false);
@@ -73,13 +79,14 @@ function PlusCTA() {
setError(undefined); setError(undefined);
if (planDetails()?.id === undefined || planDetails()?.id === null) if (planDetails()?.id === undefined || planDetails()?.id === null)
throw new Error("No plans found"); throw new Error(i18n.t("settings.plus.error_no_plan"));
const invoice = await state.mutiny_wallet?.subscribe_to_plan( const invoice = await state.mutiny_wallet?.subscribe_to_plan(
planDetails().id planDetails().id
); );
if (!invoice?.bolt11) throw new Error("Couldn't subscribe"); if (!invoice?.bolt11)
throw new Error(i18n.t("settings.plus.error_failure"));
await state.mutiny_wallet?.pay_subscription_invoice( await state.mutiny_wallet?.pay_subscription_invoice(
invoice?.bolt11 invoice?.bolt11
@@ -102,7 +109,9 @@ function PlusCTA() {
setRestoring(true); setRestoring(true);
await actions.checkForSubscription(); await actions.checkForSubscription();
if (!state.subscription_timestamp) { if (!state.subscription_timestamp) {
setError(new Error("No existing subscription found")); setError(
new Error(i18n.t("settings.plus.error_no_subscription"))
);
} }
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@@ -121,19 +130,26 @@ function PlusCTA() {
<Show when={planDetails()}> <Show when={planDetails()}>
<VStack> <VStack>
<NiceP> <NiceP>
Join <strong class="text-white">Mutiny+</strong> for{" "} {i18n.t("settings.plus.join")}{" "}
{Number(planDetails().amount_sat).toLocaleString()} sats a <strong class="text-white">
month. {i18n.t("settings.plus.title")}
</strong>{" "}
{i18n.t("settings.plus.sats_per_month", {
amount: Number(
planDetails().amount_sat
).toLocaleString()
})}
</NiceP> </NiceP>
<Show when={error()}> <Show when={error()}>
<InfoBox accent="red">{error()!.message}</InfoBox> <InfoBox accent="red">{error()!.message}</InfoBox>
</Show> </Show>
<Show when={!hasEnough()}> <Show when={!hasEnough()}>
<TinyText> <TinyText>
You'll need at least{" "} {i18n.t("settings.plus.lightning_balance", {
{Number(planDetails().amount_sat).toLocaleString()} sats amount: Number(
in your lightning balance to get started. Try before you planDetails().amount_sat
buy! ).toLocaleString()
})}
</TinyText> </TinyText>
</Show> </Show>
<div class="flex gap-2"> <div class="flex gap-2">
@@ -143,7 +159,7 @@ function PlusCTA() {
onClick={() => setConfirmOpen(true)} onClick={() => setConfirmOpen(true)}
disabled={!hasEnough()} disabled={!hasEnough()}
> >
Join {i18n.t("settings.plus.join")}
</Button> </Button>
<Button <Button
intent="green" intent="green"
@@ -151,7 +167,7 @@ function PlusCTA() {
onClick={restore} onClick={restore}
loading={restoring()} loading={restoring()}
> >
Restore Subscription {i18n.t("settings.plus.restore")}
</Button> </Button>
</div> </div>
</VStack> </VStack>
@@ -162,8 +178,11 @@ function PlusCTA() {
onCancel={() => setConfirmOpen(false)} onCancel={() => setConfirmOpen(false)}
> >
<p> <p>
Ready to join <strong class="text-white">Mutiny+</strong>? {i18n.t("settings.plus.ready_to_join")}{" "}
Click confirm to pay for your first month. <strong class="text-white">
{i18n.t("settings.plus.title")}
</strong>
?{i18n.t("settings.plus.click_confirm")}
</p> </p>
</ConfirmDialog> </ConfirmDialog>
</Show> </Show>
@@ -171,25 +190,26 @@ function PlusCTA() {
} }
export default function Plus() { export default function Plus() {
const i18n = useI18n();
const [state, _actions] = useMegaStore(); const [state, _actions] = useMegaStore();
return ( return (
<MutinyWalletGuard> <MutinyWalletGuard>
<SafeArea> <SafeArea>
<DefaultMain> <DefaultMain>
<BackLink href="/settings" title="Settings" /> <BackLink
<LargeHeader>Mutiny+</LargeHeader> href="/settings"
title={i18n.t("settings.header")}
/>
<LargeHeader>{i18n.t("settings.plus.title")}</LargeHeader>
<VStack> <VStack>
<Switch> <Switch>
<Match when={state.mutiny_plus}> <Match when={state.mutiny_plus}>
<img src={party} class="w-1/2 mx-auto" /> <img src={party} class="w-1/2 mx-auto" />
<NiceP> <NiceP>{i18n.t("settings.plus.thanks")}</NiceP>
You're part of the mutiny! Enjoy the
following perks:
</NiceP>
<Perks alreadySubbed /> <Perks alreadySubbed />
<NiceP> <NiceP>
You'll get a renewal payment request around{" "} {i18n.t("settings.plus.renewal_time")}{" "}
<strong class="text-white"> <strong class="text-white">
{new Date( {new Date(
state.subscription_timestamp! * 1000 state.subscription_timestamp! * 1000
@@ -198,29 +218,32 @@ export default function Plus() {
. .
</NiceP> </NiceP>
<NiceP> <NiceP>
To cancel your subscription just don't pay. {i18n.t("settings.plus.cancel")}{" "}
You can also disable the Mutiny+{" "}
<A href="/settings/connections"> <A href="/settings/connections">
Wallet Connection. {i18n.t(
"settings.plus.wallet_connection"
)}
</A> </A>
</NiceP> </NiceP>
</Match> </Match>
<Match when={!state.mutiny_plus}> <Match when={!state.mutiny_plus}>
<NiceP> <NiceP>
Mutiny is open source and self-hostable.{" "} {i18n.t("settings.plus.open_source")}{" "}
<strong> <strong>
But also you can pay for it. {i18n.t("settings.plus.optional_pay")}
</strong> </strong>
</NiceP> </NiceP>
<NiceP> <NiceP>
Paying for{" "} {i18n.t("settings.plus.paying_for")}{" "}
<strong class="text-white">Mutiny+</strong>{" "} <strong class="text-white">
helps support ongoing development and {i18n.t("settings.plus.title")}
unlocks early access to new features and </strong>{" "}
premium functionality: {i18n.t("settings.plus.supports_dev")}
</NiceP> </NiceP>
<Perks /> <Perks />
<FancyCard title="Subscribe"> <FancyCard
title={i18n.t("settings.plus.subscribe")}
>
<Suspense fallback={<LoadingShimmer />}> <Suspense fallback={<LoadingShimmer />}>
<PlusCTA /> <PlusCTA />
</Suspense> </Suspense>

View File

@@ -29,6 +29,7 @@ import { WORDS_EN } from "~/utils/words";
import { InfoBox } from "~/components/InfoBox"; import { InfoBox } from "~/components/InfoBox";
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";
type SeedWordsForm = { type SeedWordsForm = {
words: string[]; words: string[];
@@ -78,6 +79,7 @@ export function SeedTextField(props: TextFieldProps) {
} }
function TwelveWordsEntry() { function TwelveWordsEntry() {
const i18n = useI18n();
const [state, _actions] = useMegaStore(); const [state, _actions] = useMegaStore();
const [error, setError] = createSignal<Error>(); const [error, setError] = createSignal<Error>();
@@ -101,7 +103,9 @@ function TwelveWordsEntry() {
text = value; text = value;
} else { } else {
if (!navigator.clipboard.readText) { if (!navigator.clipboard.readText) {
return showToast(new Error("Clipboard not supported")); return showToast(
new Error(i18n.t("settings.restore.error_clipboard"))
);
} }
text = await navigator.clipboard.readText(); text = await navigator.clipboard.readText();
} }
@@ -110,7 +114,9 @@ function TwelveWordsEntry() {
const words = text.split(/[\s\n]+/); const words = text.split(/[\s\n]+/);
if (words.length !== 12) { if (words.length !== 12) {
return showToast(new Error("Wrong number of words")); return showToast(
new Error(i18n.t("settings.restore.error_word_number"))
);
} }
setValues(seedWordsForm, "words", words); setValues(seedWordsForm, "words", words);
@@ -150,7 +156,7 @@ function TwelveWordsEntry() {
const valid = values.words?.every(validateWord); const valid = values.words?.every(validateWord);
if (!valid) { if (!valid) {
setError(new Error("Invalid seed phrase")); setError(new Error(i18n.t("settings.restore.error_invalid_seed")));
return; return;
} }
@@ -180,11 +186,15 @@ function TwelveWordsEntry() {
name={`words.${index()}`} name={`words.${index()}`}
validate={[ validate={[
required( required(
"You need to enter all 12 words" i18n.t(
"settings.restore.all_twelve"
)
), ),
custom( custom(
validateWord, validateWord,
"Wrong word" i18n.t(
"settings.restore.wrong_word"
)
) )
]} ]}
> >
@@ -209,7 +219,7 @@ function TwelveWordsEntry() {
type="button" type="button"
> >
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span>Dangerously Paste from Clipboard</span> <span>{i18n.t("settings.restore.paste")}</span>
<img <img
src={pasteIcon} src={pasteIcon}
alt="paste" alt="paste"
@@ -224,7 +234,7 @@ function TwelveWordsEntry() {
intent="red" intent="red"
disabled={seedWordsForm.invalid || !seedWordsForm.dirty} disabled={seedWordsForm.invalid || !seedWordsForm.dirty}
> >
Restore {i18n.t("settings.restore.title")}
</Button> </Button>
</Form> </Form>
<ConfirmDialog <ConfirmDialog
@@ -233,29 +243,25 @@ function TwelveWordsEntry() {
onCancel={() => setConfirmOpen(false)} onCancel={() => setConfirmOpen(false)}
loading={confirmLoading()} loading={confirmLoading()}
> >
<p> <p>{i18n.t("settings.restore.confirm_text")}</p>
Are you sure you want to restore to this wallet? Your
existing wallet will be deleted!
</p>
</ConfirmDialog> </ConfirmDialog>
</> </>
); );
} }
export default function RestorePage() { export default function RestorePage() {
const i18n = useI18n();
return ( return (
<SafeArea> <SafeArea>
<DefaultMain> <DefaultMain>
<BackLink title="Settings" href="/settings" /> <BackLink title={i18n.t("settings.header")} href="/settings" />
<LargeHeader>Restore</LargeHeader> <LargeHeader>{i18n.t("settings.restore.title")}</LargeHeader>
<VStack> <VStack>
<NiceP> <NiceP>
<p>{i18n.t("settings.restore.restore_tip")}</p>
<p> <p>
You can restore an existing Mutiny Wallet from your {i18n.t("settings.restore.multi_browser_warning")}
12 word seed phrase. This will replace your existing
wallet, so make sure you know what you're doing!
</p> </p>
<p>Do not use on multiple browsers at the same time.</p>
</NiceP> </NiceP>
<TwelveWordsEntry /> <TwelveWordsEntry />
</VStack> </VStack>

View File

@@ -19,8 +19,10 @@ import eify from "~/utils/eify";
import { ExternalLink } from "~/components/layout/ExternalLink"; import { ExternalLink } from "~/components/layout/ExternalLink";
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";
export function SettingsStringsEditor() { export function SettingsStringsEditor() {
const i18n = useI18n();
const existingSettings = getExistingSettings(); const existingSettings = getExistingSettings();
const [settingsForm, { Form, Field }] = const [settingsForm, { Form, Field }] =
createForm<MutinyWalletSettingStrings>({ createForm<MutinyWalletSettingStrings>({
@@ -40,68 +42,66 @@ export function SettingsStringsEditor() {
} }
return ( return (
<Card title="Servers"> <Card title={i18n.t("settings.servers.title")}>
<Form onSubmit={handleSubmit} class="flex flex-col gap-4"> <Form onSubmit={handleSubmit} class="flex flex-col gap-4">
<NiceP> <NiceP>{i18n.t("settings.servers.caption")}</NiceP>
Don't trust us! Use your own servers to back Mutiny.
</NiceP>
<ExternalLink href="https://github.com/MutinyWallet/mutiny-web/wiki/Self-hosting"> <ExternalLink href="https://github.com/MutinyWallet/mutiny-web/wiki/Self-hosting">
Learn more about self-hosting {i18n.t("settings.servers.link")}
</ExternalLink> </ExternalLink>
<div /> <div />
<Field <Field
name="proxy" name="proxy"
validate={[url("Should be a url starting with wss://")]} validate={[url(i18n.t("settings.servers.error_proxy"))]}
> >
{(field, props) => ( {(field, props) => (
<TextField <TextField
{...props} {...props}
value={field.value} value={field.value}
error={field.error} error={field.error}
label="Websockets Proxy" label={i18n.t("settings.servers.proxy_label")}
caption="How your lightning node communicates with the rest of the network." caption={i18n.t("settings.servers.proxy_caption")}
/> />
)} )}
</Field> </Field>
<Field <Field
name="esplora" name="esplora"
validate={[url("That doesn't look like a URL")]} validate={[url(i18n.t("settings.servers.error_esplora"))]}
> >
{(field, props) => ( {(field, props) => (
<TextField <TextField
{...props} {...props}
value={field.value} value={field.value}
error={field.error} error={field.error}
label="Esplora" label={i18n.t("settings.servers.esplora_label")}
caption="Block data for on-chain information." caption={i18n.t("settings.servers.esplora_caption")}
/> />
)} )}
</Field> </Field>
<Field <Field
name="rgs" name="rgs"
validate={[url("That doesn't look like a URL")]} validate={[url(i18n.t("settings.servers.error_rgs"))]}
> >
{(field, props) => ( {(field, props) => (
<TextField <TextField
{...props} {...props}
value={field.value} value={field.value}
error={field.error} error={field.error}
label="RGS" label={i18n.t("settings.servers.rgs_label")}
caption="Rapid Gossip Sync. Network data about the lightning network used for routing." caption={i18n.t("settings.servers.rgs_caption")}
/> />
)} )}
</Field> </Field>
<Field <Field
name="lsp" name="lsp"
validate={[url("That doesn't look like a URL")]} validate={[url(i18n.t("settings.servers.error_lsp"))]}
> >
{(field, props) => ( {(field, props) => (
<TextField <TextField
{...props} {...props}
value={field.value} value={field.value}
error={field.error} error={field.error}
label="LSP" label={i18n.t("settings.servers.lsp_label")}
caption="Lightning Service Provider. Automatically opens channels to you for inbound liquidity. Also wraps invoices for privacy." caption={i18n.t("settings.servers.lsp_caption")}
/> />
)} )}
</Field> </Field>
@@ -111,7 +111,7 @@ export function SettingsStringsEditor() {
disabled={!settingsForm.dirty} disabled={!settingsForm.dirty}
intent="blue" intent="blue"
> >
Save {i18n.t("settings.servers.save")}
</Button> </Button>
</Form> </Form>
</Card> </Card>
@@ -119,12 +119,18 @@ export function SettingsStringsEditor() {
} }
export default function Servers() { export default function Servers() {
const i18n = useI18n();
return ( return (
<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.servers.title")}
</LargeHeader>
<SettingsStringsEditor /> <SettingsStringsEditor />
</DefaultMain> </DefaultMain>
<NavBar activeTab="settings" /> <NavBar activeTab="settings" />

View File

@@ -66,7 +66,7 @@ export default function Settings() {
<LargeHeader>{i18n.t("settings.header")}</LargeHeader> <LargeHeader>{i18n.t("settings.header")}</LargeHeader>
<VStack biggap> <VStack biggap>
<SettingsLinkList <SettingsLinkList
header={i18n.t("settings.mutiny_plus")} header={i18n.t("settings.plus.title")}
links={[ links={[
{ {
href: "/settings/plus", href: "/settings/plus",