diff --git a/package.json b/package.json index 5b9703e..46304ec 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,9 @@ "@solidjs/router": "^0.8.2", "@thisbeyond/solid-select": "^0.14.0", "class-variance-authority": "^0.4.0", + "i18next": "^22.5.1", + "i18next-browser-languagedetector": "^7.0.2", + "i18next-http-backend": "^2.2.1", "nostr-tools": "^1.11.1", "qr-scanner": "^1.4.2", "solid-js": "^1.7.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c66adc3..610f738 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,15 @@ dependencies: class-variance-authority: specifier: ^0.4.0 version: 0.4.0(typescript@4.9.5) + i18next: + specifier: ^22.5.1 + version: 22.5.1 + i18next-browser-languagedetector: + specifier: ^7.0.2 + version: 7.0.2 + i18next-http-backend: + specifier: ^2.2.1 + version: 2.2.1 nostr-tools: specifier: ^1.11.1 version: 1.12.1 @@ -2628,6 +2637,14 @@ packages: dependencies: browserslist: 4.21.9 + /cross-fetch@3.1.6: + resolution: {integrity: sha512-riRvo06crlE8HiqOwIpQhxwdOk4fOeR7FVM/wXoxchFEqMNUjvbs3bfo4OTgMEMHzppd4DxFBDbyySj8Cv781g==} + dependencies: + node-fetch: 2.6.11 + transitivePeerDependencies: + - encoding + dev: false + /cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -3709,6 +3726,26 @@ packages: resolution: {integrity: sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==} engines: {node: '>=14.18.0'} + /i18next-browser-languagedetector@7.0.2: + resolution: {integrity: sha512-5ViaK+gikxfqZ9M3jJ7gJkUzzu/p3HwiqfLoL1bdiL7CUb0IylcTyVLdPaTU3pH5VFWFCiGFuJDg3VkLUikWgg==} + dependencies: + '@babel/runtime': 7.22.5 + dev: false + + /i18next-http-backend@2.2.1: + resolution: {integrity: sha512-ZXIdn/8NJIBJ0X4hzXfc3STYxKrCKh1fYjji9HPyIpEJfvTvy8/ZlTl8RuTizzCPj2ZcWrfaecyOMKs6bQ7u5A==} + dependencies: + cross-fetch: 3.1.6 + transitivePeerDependencies: + - encoding + dev: false + + /i18next@22.5.1: + resolution: {integrity: sha512-8TGPgM3pAD+VRsMtUMNknRz3kzqwp/gPALrWMsDnmC1mKqJwpWyooQRLMcbTwq8z8YwSmuj+ZYvc+xCuEpkssA==} + dependencies: + '@babel/runtime': 7.22.5 + dev: false + /idb@7.1.1: resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==} dev: true @@ -4225,6 +4262,18 @@ packages: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} + /node-fetch@2.6.11: + resolution: {integrity: sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + dependencies: + whatwg-url: 5.0.0 + dev: false + /node-releases@2.0.12: resolution: {integrity: sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==} @@ -5159,6 +5208,10 @@ packages: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} + /tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + dev: false + /tr46@1.0.1: resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} dependencies: @@ -5436,10 +5489,21 @@ packages: transitivePeerDependencies: - debug + /webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + dev: false + /webidl-conversions@4.0.2: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} dev: true + /whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + dev: false + /whatwg-url@7.1.0: resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} dependencies: diff --git a/src/components/Activity.tsx b/src/components/Activity.tsx index fc60758..85d59f5 100644 --- a/src/components/Activity.tsx +++ b/src/components/Activity.tsx @@ -1,6 +1,7 @@ import { NiceP } from "./layout"; import { For, Match, Show, Switch, createEffect, createSignal } from "solid-js"; import { useMegaStore } from "~/state/megaStore"; +import { useI18n } from "~/i18n/context"; import { Contact } from "@mutinywallet/mutiny-wasm"; import { ActivityItem, HackActivityType } from "./ActivityItem"; import { DetailsIdModal } from "./DetailsModal"; @@ -77,6 +78,7 @@ function UnifiedActivityItem(props: { export function CombinedActivity(props: { limit?: number }) { const [state, actions] = useMegaStore(); + const i18n = useI18n(); const [detailsOpen, setDetailsOpen] = createSignal(false); const [detailsKind, setDetailsKind] = createSignal(); @@ -115,7 +117,7 @@ export function CombinedActivity(props: { limit?: number }) {
- Receive some sats to get started + {i18n.t("receive_some_sats_to_get_started")}
= (props) => { + const i18n = useI18n(); const navigate = useNavigate(); const [isOpen, setIsOpen] = createSignal(props.initialOpen); const [state, _actions] = useMegaStore(); @@ -236,7 +238,7 @@ export const AmountEditable: ParentComponent<{ if (network === "bitcoin") { return "Your first lightning receive needs to be 50,000 sats or greater. A setup fee will be deducted from the requested amount."; } else { - return "Your first lightning receive needs to be 10,000 sats or greater. A setup fee will be deducted from the requested amount."; + return i18n.t("amount_editable_first_payment_10k_or_greater"); } } @@ -399,7 +401,7 @@ export const AmountEditable: ParentComponent<{ Set amount +
{i18n.t("set_amount")}
} > @@ -526,7 +528,7 @@ export const AmountEditable: ParentComponent<{ class="w-full flex-none" onClick={handleSubmit} > - Set Amount + {i18n.t("set_amount")} diff --git a/src/components/App.tsx b/src/components/App.tsx index f649034..97ae5f8 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -12,9 +12,11 @@ import { BetaWarningModal } from "~/components/BetaWarningModal"; import settings from "~/assets/icons/settings.svg"; import pixelLogo from "~/assets/mutiny-pixel-logo.png"; import { PendingNwc } from "./PendingNwc"; +import { useI18n } from "~/i18n/context"; export default function App() { const [state, _actions] = useMegaStore(); + const i18n = useI18n(); return ( @@ -68,7 +70,7 @@ export default function App() { href="/activity" class="text-m-red active:text-m-red/80 font-semibold no-underline self-center" > - View All + {i18n.t("view_all")}
@@ -76,7 +78,7 @@ export default function App() { Bugs? Feedback?{" "} - Create an issue + {i18n.t("create_an_issue")}

diff --git a/src/components/I18nProvider.tsx b/src/components/I18nProvider.tsx new file mode 100644 index 0000000..caa4a00 --- /dev/null +++ b/src/components/I18nProvider.tsx @@ -0,0 +1,19 @@ +import { ParentComponent, Show, createResource } from "solid-js"; +import { I18nContext } from "../i18n/context"; +import i18next from "i18next"; +import i18nConfig from "~/i18n/config"; + +export const I18nProvider: ParentComponent = (props) => { + const [i18nConfigured] = createResource(async () => { + await i18nConfig; + return true; + }); + + return ( + + + {props.children} + + + ); +}; diff --git a/src/components/MoreInfoModal.tsx b/src/components/MoreInfoModal.tsx index 4d2d416..b75d30d 100644 --- a/src/components/MoreInfoModal.tsx +++ b/src/components/MoreInfoModal.tsx @@ -3,23 +3,21 @@ import { ParentComponent, createSignal } from "solid-js"; import { DIALOG_CONTENT, DIALOG_POSITIONER, OVERLAY } from "./DetailsModal"; import { ModalCloseButton, SmallHeader } from "./layout"; import { ExternalLink } from "./layout/ExternalLink"; +import { useI18n } from "~/i18n/context"; export function FeesModal() { + const i18n = useI18n(); return ( - +

- 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. + {i18n.t("more_info_modal_p1")}

- 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. + {i18n.t("more_info_modal_p2")}

- Learn more about liquidity + {i18n.t("learn_more_about_liquidity")}

diff --git a/src/components/layout/index.tsx b/src/components/layout/index.tsx index 981c323..9d22f23 100644 --- a/src/components/layout/index.tsx +++ b/src/components/layout/index.tsx @@ -30,7 +30,7 @@ export const SmallHeader: ParentComponent<{ class?: string }> = (props) => { }; export const Card: ParentComponent<{ - title?: string; + title?: string | null; titleElement?: JSX.Element; }> = (props) => { return ( diff --git a/src/i18n/config.ts b/src/i18n/config.ts new file mode 100644 index 0000000..9221378 --- /dev/null +++ b/src/i18n/config.ts @@ -0,0 +1,26 @@ +import i18next from 'i18next'; +import HttpApi from 'i18next-http-backend'; +import LanguageDetector from 'i18next-browser-languagedetector'; +const i18n = i18next + .use(HttpApi) + .use(LanguageDetector) + .init({ + fallbackLng: 'en', + preload: ['en'], + load: 'languageOnly', + ns: ['translations'], + defaultNS: 'translations', + fallbackNS: false, + debug: true, + detection: { + order: ['querystring', 'navigator', 'htmlTag'], + lookupQuerystring: 'lang', + }, + backend: { + loadPath: 'src/i18n/{{lng}}/{{ns}}.json', + } + }, (err, t) => { + // Do we actually wanna log something in case of an unsupported language? + if (err) return console.error(err) + }); +export default i18n; diff --git a/src/i18n/context.ts b/src/i18n/context.ts new file mode 100644 index 0000000..f992da2 --- /dev/null +++ b/src/i18n/context.ts @@ -0,0 +1,12 @@ +import { createContext, useContext } from 'solid-js'; +import { i18n } from "i18next"; + +export const I18nContext = createContext(); + +export function useI18n() { + const context = useContext(I18nContext); + + if (!context) throw new ReferenceError('I18nContext'); + + return context; +} diff --git a/src/i18n/en/translations.json b/src/i18n/en/translations.json new file mode 100644 index 0000000..56284d7 --- /dev/null +++ b/src/i18n/en/translations.json @@ -0,0 +1,19 @@ +{ + "create_an_issue": "Create an issue", + "view_all": "View all", + "receive_some_sats_to_get_started": "Receive some sats to get started", + "send_bitcoin": "Send Bitcoin", + "view_transaction": "View Transaction", + "amount_editable_first_payment_10k_or_greater": "Your first lightning receive needs to be 10,000 sats or greater. A setup fee will be deducted from the requested amount.", + "why?": "Why?", + "more_info_modal_p1": "Mutiny is a self-custodial wallet. To initiate a lightning payment we must open a lightning channel, which requires a minimum amount and a setup fee.", + "more_info_modal_p2": "Future payments, both send and recieve, will only incur normal network fees and a nominal service fee unless your channel runs out of inbound capacity.", + "learn_more_about_liquidity": "Learn more about liquidity", + "set_amount": "Set amount", + "whats_with_the_fees": "What's with the fees?", + "private_tags": "Private tags", + "receive_add_the_sender": "Add the sender for your records", + "continue": "Continue", + "receive_bitcoin": "Receive Bitcoin", + "keep_mutiny_open": "Keep Mutiny open to complete the payment." +} diff --git a/src/i18n/pt/translations.json b/src/i18n/pt/translations.json new file mode 100644 index 0000000..6133f1a --- /dev/null +++ b/src/i18n/pt/translations.json @@ -0,0 +1,18 @@ +{ + "create_an_issue": "Crie uma issue", + "view_all": "Ver todas", + "receive_some_sats_to_get_started": "Receba alguns satoshis para começar", + "send_bitcoin": "Enviar Bitcoin", + "view_transaction": "Ver transação", + "amount_editable_first_payment_10k_or_greater": "Seu primeiro recebimento na lightning precisa ser de pelo menos 10.000 sats. Uma taxa de configuração será deduzida da quantidade requisitada.", + "why?": "Por que?", + "more_info_modal_p1": "Mutiny é uma carteira de auto custódia. Para iniciar um pagamento na lightning, nós precisamos abrir um canal que requer uma quantidade mínima e uma taxa de configuração.", + "more_info_modal_p2": "Transaçoēs futuras, como envios e recebimentos, terão somente taxas normais da rede e uma taxa de serviço nominal a não ser que seu canal fique sem capacidade de entrada.", + "learn_more_about_liquidity": "Aprenda mais sobre liquidez", + "set_amount": "Definir quantidade", + "whats_with_the_fees": "O que há com as taxas?", + "private_tags": "Tags privadas", + "receive_add_the_sender": "Marque quem o enviou para registro próprio", + "continue": "Continuar", + "receive_bitcoin": "Receber Bitcoin" +} diff --git a/src/root.tsx b/src/root.tsx index 81a1aa9..568abf3 100644 --- a/src/root.tsx +++ b/src/root.tsx @@ -16,6 +16,7 @@ import "./root.css"; import { Provider as MegaStoreProvider } from "~/state/megaStore"; import { Toaster } from "~/components/Toaster"; import ErrorDisplay from "./components/ErrorDisplay"; +import { I18nProvider } from "./components/I18nProvider"; export default function Root() { return ( @@ -76,12 +77,14 @@ export default function Root() { }> - - - - - - + + + + + + + + diff --git a/src/routes/Receive.tsx b/src/routes/Receive.tsx index 7f61b42..3288b84 100644 --- a/src/routes/Receive.tsx +++ b/src/routes/Receive.tsx @@ -44,6 +44,7 @@ import { InfoBox } from "~/components/InfoBox"; import { FeesModal } from "~/components/MoreInfoModal"; import { IntegratedQr } from "~/components/IntegratedQR"; import side2side from "~/assets/icons/side-to-side.svg"; +import { useI18n } from "~/i18n/context"; type OnChainTx = { transaction: { @@ -143,6 +144,7 @@ function FeeExplanation(props: { fee: bigint }) { export default function Receive() { const [state, _actions] = useMegaStore(); const navigate = useNavigate(); + const i18n = useI18n(); const [amount, setAmount] = createSignal(""); const [receiveState, setReceiveState] = createSignal("edit"); @@ -331,7 +333,7 @@ export default function Receive() { ) } > - Receive Bitcoin + {i18n.t("receive_bitcoin")} @@ -344,11 +346,11 @@ export default function Receive() { exitRoute={amount() ? "/receive" : "/"} /> - + @@ -359,7 +361,7 @@ export default function Receive() { intent="green" onClick={onSubmit} > - Continue + {i18n.t("continue")} @@ -371,7 +373,7 @@ export default function Receive() { kind={flavor()} />

- Keep Mutiny open to receive the payment. + {i18n.t("keep_mutiny_open")}