diff --git a/README.md b/README.md index a1ee0a38..92fb5351 100644 --- a/README.md +++ b/README.md @@ -51,3 +51,11 @@ Then re-run: ```bash yarn backend ``` + +## Translations + +Extract and compile strings ([docs](https://formatjs.io/docs/tooling/cli/#extraction-and-compilation-with-a-single-script)) (run in react-app folder): + +``` +yarn translations-extract +``` diff --git a/packages/react-app/package.json b/packages/react-app/package.json index 0b41ba39..d7072609 100644 --- a/packages/react-app/package.json +++ b/packages/react-app/package.json @@ -53,6 +53,7 @@ "react": "^16.14.0", "react-blockies": "^1.4.1", "react-dom": "^16.14.0", + "react-intl": "^6.3.2", "react-markdown": "^7.1.0", "react-qr-reader": "^2.2.1", "react-router-dom": "^5.2.0", @@ -63,6 +64,7 @@ "web3modal": "^1.9.1" }, "devDependencies": { + "@formatjs/cli": "^6.0.4", "@testing-library/dom": "^6.12.2", "@types/react": "^16.9.19", "autoprefixer": "^10.2.4", @@ -101,6 +103,7 @@ "s3": "node ./scripts/s3.js", "ship": "yarn surge", "theme": "npx gulp less", - "watch": "node ./scripts/watch.js" + "watch": "node ./scripts/watch.js", + "translations-extract": "formatjs extract 'src/**/*.js*' --ignore='**/*.json' --out-file src/lang/extractions.json --flatten --id-interpolation-pattern '[sha512:contenthash:base64:6]' && formatjs compile 'src/lang/extractions.json' --out-file src/lang/en.json && rm src/lang/extractions.json" } } diff --git a/packages/react-app/src/App.jsx b/packages/react-app/src/App.jsx index b4c2906f..eab64028 100644 --- a/packages/react-app/src/App.jsx +++ b/packages/react-app/src/App.jsx @@ -129,7 +129,7 @@ function App() { const logoutOfWeb3Modal = async () => { await web3Modal.clearCachedProvider(); - if (injectedProvider && injectedProvider.provider && typeof injectedProvider.provider.disconnect == "function") { + if (injectedProvider && injectedProvider.provider && typeof injectedProvider.provider.disconnect === "function") { await injectedProvider.provider.disconnect(); } setTimeout(() => { diff --git a/packages/react-app/src/components/Account.jsx b/packages/react-app/src/components/Account.jsx index 3fa96300..2d83fd80 100644 --- a/packages/react-app/src/components/Account.jsx +++ b/packages/react-app/src/components/Account.jsx @@ -19,6 +19,7 @@ import { LinkBox, LinkOverlay, } from "@chakra-ui/react"; +import { FormattedMessage } from "react-intl"; import QRPunkBlockie from "./QrPunkBlockie"; import useDisplayAddress from "../hooks/useDisplayAddress"; import useCustomColorModes from "../hooks/useCustomColorModes"; @@ -63,7 +64,6 @@ import SignatureSignUp from "./SignatureSignUp"; export default function Account({ address, - connectText, ensProvider, isWalletConnected, loadWeb3Modal, @@ -92,7 +92,7 @@ export default function Account({ const connectWallet = ( ); diff --git a/packages/react-app/src/components/AnnouncementBanner.jsx b/packages/react-app/src/components/AnnouncementBanner.jsx index bbd47926..1eaf116a 100644 --- a/packages/react-app/src/components/AnnouncementBanner.jsx +++ b/packages/react-app/src/components/AnnouncementBanner.jsx @@ -1,17 +1,28 @@ import React from "react"; import { chakra, useColorModeValue, Link } from "@chakra-ui/react"; +import { FormattedMessage } from "react-intl"; export default function AnnouncementBanner() { const bannerBg = useColorModeValue("#fbf7f6", "whiteAlpha.300"); return ( - Hey builder!! The BuidlGuidl is hosting a{" "} - - πŸ— Scaffold-Eth 2 hackathon - - . We are giving 10 ETH away to the best projects. -
Come join the fun and learn the latest scaffold-eth techniques! Let's build a bunch of apps! + πŸ— Scaffold-Eth 2 hackathon. + We are giving 10 ETH away to the best projects. + {br}Come join the fun and learn the latest scaffold-eth techniques! + Let's build a bunch of apps!`} + values={{ + Link: chunks => ( + + {chunks} + + ), + br:
, + }} + />
); } diff --git a/packages/react-app/src/components/ChallengeExpandedCard.jsx b/packages/react-app/src/components/ChallengeExpandedCard.jsx index 216d044b..94c1b59f 100644 --- a/packages/react-app/src/components/ChallengeExpandedCard.jsx +++ b/packages/react-app/src/components/ChallengeExpandedCard.jsx @@ -15,6 +15,7 @@ import { useColorModeValue, VStack, } from "@chakra-ui/react"; +import { FormattedMessage } from "react-intl"; import useCustomColorModes from "../hooks/useCustomColorModes"; import { CHALLENGE_SUBMISSION_STATUS } from "../helpers/constants"; @@ -137,7 +138,7 @@ const ChallengeExpandedCard = ({ fontSize={{ base: "xl", lg: "lg" }} border="2px" backgroundColor="sreDark.default" - disabled={true} + disabled borderColor="sre.default" py="1rem" px={4} @@ -145,7 +146,7 @@ const ChallengeExpandedCard = ({ - Locked + @@ -285,7 +286,7 @@ const ChallengeExpandedCard = ({ - Locked + )} @@ -316,14 +317,14 @@ const ChallengeExpandedCard = ({ - Quest + ) : ( - Locked + )} diff --git a/packages/react-app/src/components/ChallengeReviewRow.jsx b/packages/react-app/src/components/ChallengeReviewRow.jsx index 42cd5cd1..1dffef9a 100644 --- a/packages/react-app/src/components/ChallengeReviewRow.jsx +++ b/packages/react-app/src/components/ChallengeReviewRow.jsx @@ -36,9 +36,10 @@ import { import { useUserAddress } from "eth-hooks"; import ReactMarkdown from "react-markdown"; import ChakraUIRenderer from "chakra-ui-markdown-renderer"; +import { useIntl } from "react-intl"; import Address from "./Address"; import DateWithTooltip from "./DateWithTooltip"; -import { challengeInfo } from "../data/challenges"; +import { getChallengeInfo } from "../data/challenges"; import { chakraMarkdownComponents } from "../helpers/chakraMarkdownTheme"; import { runAutograderTest } from "../data/api"; import { isBoolean } from "../helpers/strings"; @@ -52,6 +53,9 @@ export default function ChallengeReviewRow({ challenge, isLoading, approveClick, const address = useUserAddress(userProvider); const { linkColor } = useCustomColorModes(); + const intl = useIntl(); + const challengeInfo = getChallengeInfo(intl); + if (!challengeInfo[challenge.id]) { return null; } diff --git a/packages/react-app/src/components/ChallengeStatusTag.jsx b/packages/react-app/src/components/ChallengeStatusTag.jsx index 81441586..b37236fa 100644 --- a/packages/react-app/src/components/ChallengeStatusTag.jsx +++ b/packages/react-app/src/components/ChallengeStatusTag.jsx @@ -18,11 +18,13 @@ import { QuestionOutlineIcon } from "@chakra-ui/icons"; import ReactMarkdown from "react-markdown"; import remarkBreaks from "remark-breaks"; import ChakraUIRenderer from "chakra-ui-markdown-renderer"; +import { FormattedMessage, useIntl } from "react-intl"; import { CHALLENGE_SUBMISSION_STATUS } from "../helpers/constants"; import { chakraMarkdownComponents } from "../helpers/chakraMarkdownTheme"; const ChallengeStatusTag = ({ status, comment, autograding }) => { const { isOpen, onOpen, onClose } = useDisclosure(); + const intl = useIntl(); let colorScheme; let label; @@ -30,16 +32,19 @@ const ChallengeStatusTag = ({ status, comment, autograding }) => { switch (status) { case CHALLENGE_SUBMISSION_STATUS.ACCEPTED: { colorScheme = "green"; - label = "Accepted"; + label = intl.formatMessage({ + id: "general.accepted", + defaultMessage: "Accepted", + }); break; } case CHALLENGE_SUBMISSION_STATUS.REJECTED: { colorScheme = "red"; - label = "Rejected"; + label = intl.formatMessage({ id: "general.rejected", defaultMessage: "Rejected" }); break; } case CHALLENGE_SUBMISSION_STATUS.SUBMITTED: { - label = "Submitted"; + label = intl.formatMessage({ id: "general.submitted", defaultMessage: "Submitted" }); break; } default: @@ -56,7 +61,12 @@ const ChallengeStatusTag = ({ status, comment, autograding }) => { {status !== CHALLENGE_SUBMISSION_STATUS.SUBMITTED && comment && ( - + @@ -66,7 +76,9 @@ const ChallengeStatusTag = ({ status, comment, autograding }) => { - Review feedback + + + {autograding ? ( diff --git a/packages/react-app/src/components/ChallengeSubmission.jsx b/packages/react-app/src/components/ChallengeSubmission.jsx index 507ea2c6..8bfbb5b0 100644 --- a/packages/react-app/src/components/ChallengeSubmission.jsx +++ b/packages/react-app/src/components/ChallengeSubmission.jsx @@ -3,6 +3,7 @@ import axios from "axios"; import { useHistory, useParams } from "react-router-dom"; import { Button, Heading, FormControl, FormLabel, Input, Text, Tooltip, useToast } from "@chakra-ui/react"; import { QuestionOutlineIcon } from "@chakra-ui/icons"; +import { FormattedMessage, useIntl } from "react-intl"; import { isValidEtherscanTestnetUrl, isValidUrl } from "../helpers/strings"; const serverPath = "/challenges"; @@ -11,6 +12,7 @@ export default function ChallengeSubmission({ challenge, serverUrl, address, use const { challengeId } = useParams(); const history = useHistory(); const toast = useToast({ position: "top", isClosable: true }); + const intl = useIntl(); const [isSubmitting, setIsSubmitting] = useState(false); const [deployedUrl, setDeployedUrl] = useState(""); const [contractUrl, setContractUrl] = useState(""); @@ -20,7 +22,10 @@ export default function ChallengeSubmission({ challenge, serverUrl, address, use if (!deployedUrl || !contractUrl) { toast({ status: "error", - description: "Both fields are required", + description: intl.formatMessage({ + id: "challengeSubmission.error.both-fields-required", + defaultMessage: "Both fields are required", + }), }); return; } @@ -28,8 +33,14 @@ export default function ChallengeSubmission({ challenge, serverUrl, address, use if (!isValidUrl(deployedUrl) || !isValidUrl(contractUrl)) { toast({ status: "error", - title: "Please provide a valid URL", - description: "Valid URLs start with http:// or https://", + title: intl.formatMessage({ + id: "challengeSubmission.error.invalid-url.title", + defaultMessage: "Please provide a valid URL", + }), + description: intl.formatMessage({ + id: "challengeSubmission.error.invalid-url.description", + defaultMessage: "Valid URLs start with http:// or https://", + }), }); setHasErrorField({ @@ -43,9 +54,15 @@ export default function ChallengeSubmission({ challenge, serverUrl, address, use if (!isValidEtherscanTestnetUrl(contractUrl)) { toast({ status: "error", - title: "Incorrect Etherscan Contract URL", - description: - "Please submit your verified contract’s address on a valid testnet. e.g. https://goerli.etherscan.io/address/**Your Contract Address**", + title: intl.formatMessage({ + id: "challengeSubmission.error.incorrect-contract.title", + defaultMessage: "Incorrect Etherscan Contract URL", + }), + description: intl.formatMessage({ + id: "challengeSubmission.error.incorrect-contract.description", + defaultMessage: + "Please submit your verified contract’s address on a valid testnet. e.g. https://goerli.etherscan.io/address/**Your Contract Address**", + }), }); setHasErrorField({ @@ -70,7 +87,10 @@ export default function ChallengeSubmission({ challenge, serverUrl, address, use signMessage = JSON.stringify(signMessageResponse.data); } catch (error) { toast({ - description: "Can't get the message to sign. Please try again", + description: intl.formatMessage({ + id: "general.error.cant-get-message", + defaultMessage: "Can't get the message to sign. Please try again", + }), status: "error", }); setIsSubmitting(false); @@ -83,7 +103,10 @@ export default function ChallengeSubmission({ challenge, serverUrl, address, use } catch (error) { toast({ status: "error", - description: "The signature was cancelled", + description: intl.formatMessage({ + id: "general.error.signature-cancelled", + defaultMessage: "The signature was cancelled", + }), }); console.error(error); setIsSubmitting(false); @@ -108,7 +131,10 @@ export default function ChallengeSubmission({ challenge, serverUrl, address, use } catch (error) { toast({ status: "error", - description: "Submission Error. Please try again.", + description: intl.formatMessage({ + id: "general.error.submission-error", + defaultMessage: "Submission Error. Please try again.", + }), }); console.error(error); setIsSubmitting(false); @@ -118,7 +144,10 @@ export default function ChallengeSubmission({ challenge, serverUrl, address, use toast({ status: "success", - description: "Challenge submitted!", + description: intl.formatMessage({ + id: "challengeSubmission.challenge-submitted", + defaultMessage: "Challenge submitted!", + }), }); setIsSubmitting(false); history.push("/portfolio"); @@ -127,7 +156,10 @@ export default function ChallengeSubmission({ challenge, serverUrl, address, use if (!address) { return ( - Connect your wallet to submit this Challenge. + ); } @@ -139,14 +171,21 @@ export default function ChallengeSubmission({ challenge, serverUrl, address, use {challenge.isDisabled ? ( - This challenge is disabled. + ) : (
- Deployed URL{" "} - + {" "} + + } + > @@ -170,8 +209,15 @@ export default function ChallengeSubmission({ challenge, serverUrl, address, use - Etherscan Contract URL{" "} - + {" "} + + } + > @@ -195,7 +241,7 @@ export default function ChallengeSubmission({ challenge, serverUrl, address, use
diff --git a/packages/react-app/src/components/Header.jsx b/packages/react-app/src/components/Header.jsx index 4006d630..bdb8e255 100644 --- a/packages/react-app/src/components/Header.jsx +++ b/packages/react-app/src/components/Header.jsx @@ -1,6 +1,7 @@ import React from "react"; import { NavLink, useLocation } from "react-router-dom"; import { chakra, useColorModeValue, Box, Flex, HStack, Spacer } from "@chakra-ui/react"; +import { FormattedMessage } from "react-intl"; import { Account } from "./index"; import { USER_ROLES } from "../helpers/constants"; import { ENVIRONMENT } from "../constants"; @@ -83,7 +84,7 @@ export default function Header({ color: primaryColorString, }} > - Portfolio + )} @@ -98,7 +99,7 @@ export default function Header({ color: primaryColorString, }} > - Builders + @@ -113,7 +114,7 @@ export default function Header({ color: primaryColorString, }} > - Review Submissions + @@ -124,7 +125,7 @@ export default function Header({ color: primaryColorString, }} > - Activity + @@ -134,7 +135,6 @@ export default function Header({ - In order to join the BuildGuidl you need to set your socials in{" "} - - your portfolio - - . It's our way to contact you. - + description: intl.formatMessage( + { + id: "joinBg.missing-socials.description", + defaultMessage: `In order to join the BuildGuidl you need to set + your socials in your portfolio. It's our way to + contact you.`, + }, + { + Link: chunks => ( + + {chunks} + + ), + }, ), status: "error", @@ -48,7 +59,10 @@ export default function JoinBG({ text, connectedBuilder, isChallengeLocked, user signMessage = signMessageResponse.data; } catch (error) { toast({ - description: "Can't get the message to sign. Please try again", + description: intl.formatMessage({ + id: "general.error.cant-get-message", + defaultMessage: "Can't get the message to sign. Please try again", + }), status: "error", }); setIsJoining(false); @@ -61,7 +75,10 @@ export default function JoinBG({ text, connectedBuilder, isChallengeLocked, user } catch (error) { toast({ status: "error", - description: "The signature was cancelled", + description: intl.formatMessage({ + id: "general.error.signature-cancelled", + defaultMessage: "The signature was cancelled", + }), }); console.error(error); setIsJoining(false); @@ -83,7 +100,12 @@ export default function JoinBG({ text, connectedBuilder, isChallengeLocked, user } catch (error) { toast({ status: "error", - description: error?.response?.data ?? "Submission Error. Please try again.", + description: + error?.response?.data ?? + intl.formatMessage({ + id: "general.error.submission-error", + defaultMessage: "Submission Error. Please try again.", + }), }); console.error(error); setIsJoining(false); @@ -94,15 +116,21 @@ export default function JoinBG({ text, connectedBuilder, isChallengeLocked, user toast({ status: "success", duration: 10000, - title: "Welcome to the BuildGuidl :)", - description: ( - <> - Visit{" "} - - BuidlGuidl - {" "} - and start crafting your Web3 portfolio by submitting your DEX, Multisig or SVG NFT build. - + title: intl.formatMessage({ id: "joinBg.success.title", defaultMessage: "Welcome to the BuildGuidl :)" }), + description: intl.formatMessage( + { + id: "joinBg.success.description", + defaultMessage: `Visit BuidlGuidl and start crafting + your Web3 portfolio by submitting your DEX, Multisig or SVG NFT + build.`, + }, + { + Link: chunks => ( + + {chunks} + + ), + }, ), }); setIsJoining(false); @@ -125,7 +153,13 @@ export default function JoinBG({ text, connectedBuilder, isChallengeLocked, user isFullWidth isExternal > - {builderAlreadyJoined || joined ? "Already joined" : text} + + {builderAlreadyJoined || joined ? ( + + ) : ( + text + )} + ); } diff --git a/packages/react-app/src/components/SignatureSignUp.jsx b/packages/react-app/src/components/SignatureSignUp.jsx index 8ac486a9..b4f33f95 100644 --- a/packages/react-app/src/components/SignatureSignUp.jsx +++ b/packages/react-app/src/components/SignatureSignUp.jsx @@ -1,11 +1,13 @@ import React, { useState } from "react"; import axios from "axios"; import { forwardRef, chakra, Button, useToast } from "@chakra-ui/react"; +import { useIntl } from "react-intl"; import { SERVER_URL as serverUrl } from "../constants"; import { USER_ROLES } from "../helpers/constants"; const SignatureSignUp = forwardRef(({ address, userProvider, onSuccess, setUserRole }, ref) => { const [loading, setLoading] = useState(false); + const intl = useIntl(); const toast = useToast({ position: "top", isClosable: true }); const handleLoginSigning = async () => { @@ -68,7 +70,13 @@ const SignatureSignUp = forwardRef(({ address, userProvider, onSuccess, setUserR return ( ); }); diff --git a/packages/react-app/src/components/SiteFooter.jsx b/packages/react-app/src/components/SiteFooter.jsx index 4dcebdae..a33ca11c 100644 --- a/packages/react-app/src/components/SiteFooter.jsx +++ b/packages/react-app/src/components/SiteFooter.jsx @@ -1,6 +1,7 @@ import React from "react"; import { Box, HStack, Link } from "@chakra-ui/react"; import { GithubFilled, HeartFilled } from "@ant-design/icons"; +import { FormattedMessage } from "react-intl"; import useCustomColorModes from "../hooks/useCustomColorModes"; const SiteFooter = () => { @@ -12,17 +13,27 @@ const SiteFooter = () => { {" "} - Fork me +

|

- - Built with at - - - BuidlGuidl - + + + + ), + Link: chunks => ( + + {chunks} + + ), + }} + />
diff --git a/packages/react-app/src/components/builder/BuilderChallenges.jsx b/packages/react-app/src/components/builder/BuilderChallenges.jsx index 8ebd33cf..7165cfe1 100644 --- a/packages/react-app/src/components/builder/BuilderChallenges.jsx +++ b/packages/react-app/src/components/builder/BuilderChallenges.jsx @@ -16,8 +16,9 @@ import { Thead, Tr, } from "@chakra-ui/react"; +import { FormattedMessage, useIntl } from "react-intl"; import BuilderProfileChallengesTableSkeleton from "../skeletons/BuilderProfileChallengesTableSkeleton"; -import { challengeInfo } from "../../data/challenges"; +import { getChallengeInfo } from "../../data/challenges"; import DateWithTooltip from "../DateWithTooltip"; import ChallengeStatusTag from "../ChallengeStatusTag"; import useCustomColorModes from "../../hooks/useCustomColorModes"; @@ -30,12 +31,14 @@ export const BuilderChallenges = ({ isLoadingTimestamps, }) => { const { primaryFontColor, secondaryFontColor, borderColor, linkColor } = useCustomColorModes(); + const intl = useIntl(); + const challengeInfo = getChallengeInfo(intl); return ( <> - Challenges + @@ -47,17 +50,27 @@ export const BuilderChallenges = ({ {isMyProfile && ( )} - Name - Contract - Live Demo - Updated - Status + + + + + + + + + + + + + + + @@ -84,7 +97,7 @@ export const BuilderChallenges = ({ target="_blank" rel="noopener noreferrer" > - Code + @@ -94,7 +107,7 @@ export const BuilderChallenges = ({ target="_blank" rel="noopener noreferrer" > - Demo + @@ -130,19 +143,25 @@ export const BuilderChallenges = ({ {isMyProfile ? ( - Start a new challenge + - Show off your skills. Learn everything you need to build on Ethereum! + ) : ( - This builder hasn't completed any challenges. + )} diff --git a/packages/react-app/src/components/builder/BuilderProfileCard.jsx b/packages/react-app/src/components/builder/BuilderProfileCard.jsx index 9537cc82..c63c0e05 100644 --- a/packages/react-app/src/components/builder/BuilderProfileCard.jsx +++ b/packages/react-app/src/components/builder/BuilderProfileCard.jsx @@ -28,6 +28,7 @@ import { useClipboard, } from "@chakra-ui/react"; import { CopyIcon, QuestionOutlineIcon } from "@chakra-ui/icons"; +import { FormattedMessage, useIntl } from "react-intl"; import QRPunkBlockie from "../QrPunkBlockie"; import SocialLink from "../SocialLink"; import useDisplayAddress from "../../hooks/useDisplayAddress"; @@ -56,6 +57,7 @@ const BuilderProfileCard = ({ builder, mainnetProvider, isMyProfile, userProvide const { isOpen, onOpen, onClose } = useDisclosure(); const { hasCopied, onCopy } = useClipboard(builder?.id); const { borderColor, secondaryFontColor } = useCustomColorModes(); + const intl = useIntl(); const shortAddress = ellipsizedAddress(builder?.id); const hasEns = ens !== shortAddress; @@ -85,9 +87,15 @@ const BuilderProfileCard = ({ builder, mainnetProvider, isMyProfile, userProvide const invalidSocials = validateSocials(socialLinkCleaned); if (invalidSocials.length !== 0) { toast({ - description: `The usernames for the following socials are not correct: ${invalidSocials - .map(([social]) => social) - .join(", ")}`, + description: intl.formatMessage( + { + id: "builderProfileCard.error.invalid-socials", + defaultMessage: "The usernames for the following socials are not correct: {invalidSocials}", + }, + { + invalidSocials: invalidSocials.map(([social]) => social).join(", "), + }, + ), status: "error", variant: toastVariant, }); @@ -100,7 +108,10 @@ const BuilderProfileCard = ({ builder, mainnetProvider, isMyProfile, userProvide signMessage = await getUpdateSocialsSignMessage(address); } catch (error) { toast({ - description: " Sorry, the server is overloaded. πŸ§―πŸš’πŸ”₯", + description: intl.formatMessage({ + id: "error.server-overloaded", + defaultMessage: "Sorry, the server is overloaded. πŸ§―πŸš’πŸ”₯", + }), status: "error", variant: toastVariant, }); @@ -113,7 +124,10 @@ const BuilderProfileCard = ({ builder, mainnetProvider, isMyProfile, userProvide signature = await userProvider.send("personal_sign", [signMessage, address]); } catch (error) { toast({ - description: "Couldn't get a signature from the Wallet", + description: intl.formatMessage({ + id: "error.signature-from-wallet", + defaultMessage: "Couldn't get a signature from the Wallet", + }), status: "error", variant: toastVariant, }); @@ -127,7 +141,10 @@ const BuilderProfileCard = ({ builder, mainnetProvider, isMyProfile, userProvide if (error.status === 401) { toast({ status: "error", - description: "Access error", + description: intl.formatMessage({ + id: "error.access-error", + defaultMessage: "Access error", + }), variant: toastVariant, }); setIsUpdatingSocials(false); @@ -135,7 +152,10 @@ const BuilderProfileCard = ({ builder, mainnetProvider, isMyProfile, userProvide } toast({ status: "error", - description: "Can't update your socials. Please try again.", + description: intl.formatMessage({ + id: "builderProfileCard.error.updating-socials", + defaultMessage: "Can't update your socials. Please try again.", + }), variant: toastVariant, }); setIsUpdatingSocials(false); @@ -143,7 +163,10 @@ const BuilderProfileCard = ({ builder, mainnetProvider, isMyProfile, userProvide } toast({ - description: "Your social links have been updated", + description: intl.formatMessage({ + id: "builderProfileCard.success.updating-socials", + defaultMessage: "Your social links have been updated", + }), status: "success", variant: toastVariant, }); @@ -289,8 +312,18 @@ const BuilderProfileCard = ({ builder, mainnetProvider, isMyProfile, userProvide isMyProfile && ( - You haven't set your socials{" "} - + {" "} + + } + > @@ -299,11 +332,15 @@ const BuilderProfileCard = ({ builder, mainnetProvider, isMyProfile, userProvide )} {isMyProfile && ( )} - Joined {joinedDateDisplay} + @@ -312,7 +349,9 @@ const BuilderProfileCard = ({ builder, mainnetProvider, isMyProfile, userProvide - Update your socials + + + {Object.entries(socials).map(([socialId, socialData]) => ( @@ -336,7 +375,7 @@ const BuilderProfileCard = ({ builder, mainnetProvider, isMyProfile, userProvide
))}
diff --git a/packages/react-app/src/components/builder/BuilderProfileHeader.jsx b/packages/react-app/src/components/builder/BuilderProfileHeader.jsx index 4f78cec7..532c0b78 100644 --- a/packages/react-app/src/components/builder/BuilderProfileHeader.jsx +++ b/packages/react-app/src/components/builder/BuilderProfileHeader.jsx @@ -1,6 +1,7 @@ import React from "react"; import { Flex, HStack, Tag, Text } from "@chakra-ui/react"; import { InfoOutlineIcon } from "@chakra-ui/icons"; +import { FormattedMessage } from "react-intl"; import { userFunctionDescription } from "../../helpers/constants"; import useCustomColorModes from "../../hooks/useCustomColorModes"; @@ -18,7 +19,7 @@ export const BuilderProfileHeader = ({ acceptedChallenges, builder }) => { {acceptedChallenges.length} - challenges completed + @@ -37,7 +38,7 @@ export const BuilderProfileHeader = ({ acceptedChallenges, builder }) => { )} - Role + diff --git a/packages/react-app/src/components/builder/JoinedBuidlGuidlBanner.jsx b/packages/react-app/src/components/builder/JoinedBuidlGuidlBanner.jsx index 57079db2..c06d5334 100644 --- a/packages/react-app/src/components/builder/JoinedBuidlGuidlBanner.jsx +++ b/packages/react-app/src/components/builder/JoinedBuidlGuidlBanner.jsx @@ -1,5 +1,6 @@ import React from "react"; import { Button, Center, Flex, Image, Link, Text, VStack, chakra, useColorModeValue } from "@chakra-ui/react"; +import { FormattedMessage } from "react-intl"; import CrossedSwordsIcon from "../icons/CrossedSwordsIcon"; const BG_FRONTEND_URL = "https://buidlguidl.com"; @@ -36,7 +37,10 @@ export const JoinedBuidlGuidlBanner = ({ builderAddress }) => { fontWeight="extrabold" px="20px" > - This builder has upgraded to BuidlGuidl + diff --git a/packages/react-app/src/data/api.js b/packages/react-app/src/data/api.js index 2915f16f..bf93e13c 100644 --- a/packages/react-app/src/data/api.js +++ b/packages/react-app/src/data/api.js @@ -197,9 +197,9 @@ export const getDraftBuilds = async address => { } }; -export const getChallengeReadme = async (challengeId, version) => { +export const getChallengeReadme = async (challengeId, version, intl) => { try { - const response = await axios.get(getGithubChallengeReadmeUrl(challengeId, version)); + const response = await axios.get(getGithubChallengeReadmeUrl(challengeId, version, intl)); return response.data; } catch (err) { console.log("error fetching challenge README", err); diff --git a/packages/react-app/src/data/challenges.js b/packages/react-app/src/data/challenges.js index a5217afd..3ea4c9ac 100644 --- a/packages/react-app/src/data/challenges.js +++ b/packages/react-app/src/data/challenges.js @@ -1,45 +1,69 @@ -export const challengeInfo = { +export const getChallengeInfo = intl => ({ "simple-nft-example": { id: 0, branchName: "challenge-0-simple-nft", - label: "🚩 Challenge 0: 🎟 Simple NFT Example", + label: intl.formatMessage({ + id: "challenges.challenge-0-simple-nft.label", + defaultMessage: "🚩 Challenge 0: 🎟 Simple NFT Example", + }), disabled: false, - description: - "🎫 Create a simple NFT to learn basics of πŸ— scaffold-eth. You'll use πŸ‘·β€β™€οΈ HardHat to compile and deploy smart contracts. Then, you'll use a template React app full of important Ethereum components and hooks. Finally, you'll deploy an NFT to a public network to share with friends! πŸš€", + description: intl.formatMessage({ + id: "challenges.challenge-0-simple-nft.description", + defaultMessage: + "🎫 Create a simple NFT to learn basics of πŸ— scaffold-eth. You'll use πŸ‘·β€β™€οΈ HardHat to compile and deploy smart contracts. Then, you'll use a template React app full of important Ethereum components and hooks. Finally, you'll deploy an NFT to a public network to share with friends! πŸš€", + }), previewImage: "/assets/challenges/simpleNFT.svg", dependencies: [], }, "decentralized-staking": { id: 1, branchName: "challenge-1-decentralized-staking", - label: "🚩 Challenge 1: πŸ₯© Decentralized Staking App ", + label: intl.formatMessage({ + id: "challenges.challenge-1-decentralized-staking.label", + defaultMessage: "🚩 Challenge 1: πŸ₯© Decentralized Staking App ", + }), disabled: false, - description: - "🦸 A superpower of Ethereum is allowing you, the builder, to create a simple set of rules that an adversarial group of players can use to work together. In this challenge, you create a decentralized application where users can coordinate a group funding effort. The users only have to trust the code.", + description: intl.formatMessage({ + id: "challenges.challenge-1-decentralized-staking.description", + defaultMessage: + "🦸 A superpower of Ethereum is allowing you, the builder, to create a simple set of rules that an adversarial group of players can use to work together. In this challenge, you create a decentralized application where users can coordinate a group funding effort. The users only have to trust the code.", + }), previewImage: "/assets/challenges/stakingToken.svg", dependencies: [], }, "token-vendor": { id: 2, branchName: "challenge-2-token-vendor", - label: "🚩 Challenge 2: 🏡 Token Vendor", + label: intl.formatMessage({ + id: "challenges.challenge-2-token-vendor.label", + defaultMessage: "🚩 Challenge 2: 🏡 Token Vendor", + }), icon: "/assets/key_icon.svg", disabled: false, - description: - 'πŸ€– Smart contracts are kind of like "always on" vending machines that anyone can access. Let\'s make a decentralized, digital currency (an ERC20 token). Then, let\'s build an unstoppable vending machine that will buy and sell the currency. We\'ll learn about the "approve" pattern for ERC20s and how contract to contract interactions work.', + description: intl.formatMessage({ + id: "challenges.challenge-2-token-vendor.description", + defaultMessage: + 'πŸ€– Smart contracts are kind of like "always on" vending machines that anyone can access. Let\'s make a decentralized, digital currency (an ERC20 token). Then, let\'s build an unstoppable vending machine that will buy and sell the currency. We\'ll learn about the "approve" pattern for ERC20s and how contract to contract interactions work.', + }), previewImage: "/assets/challenges/tokenVendor.svg", dependencies: [], }, "buidl-guidl": { id: 4, branchName: "", - label: "Eligible to join 🏰️ BuidlGuidl", + label: intl.formatMessage({ + id: "challenges.buidl-guidl.label", + defaultMessage: "Eligible to join 🏰️ BuidlGuidl", + }), icon: "/assets/vault_icon.svg", // Not a challenge, just a checkpoint in the Challenge timeline. checkpoint: true, disabled: false, - description: - "The BuidlGuidl is a curated group of Ethereum builders creating products, prototypes, and tutorials to enrich the web3 ecosystem. A place to show off your builds and meet other builders. Start crafting your Web3 portfolio by submitting your DEX, Multisig or SVG NFT build.", + description: intl.formatMessage({ + id: "challenges.buidl-guidl.description", + defaultMessage: + "The BuidlGuidl is a curated group of Ethereum builders creating products, prototypes, and tutorials to enrich the web3 ecosystem. A place to show off your builds and meet other builders. Start crafting your Web3 portfolio by submitting your DEX, Multisig or SVG NFT build.", + }), previewImage: "assets/bg.png", dependencies: ["simple-nft-example", "decentralized-staking", "token-vendor"], externalLink: { @@ -50,40 +74,64 @@ export const challengeInfo = { "dice-game": { id: 3, branchName: "challenge-3-dice-game", - label: "🚩 Challenge 3: 🎲 Dice Game", + label: intl.formatMessage({ + id: "challenges.challenge-3-dice-game.label", + defaultMessage: "🚩 Challenge 3: 🎲 Dice Game", + }), disabled: false, - description: - "🎰 Randomness is tricky on a public deterministic blockchain. The block hash is the result proof-of-work (for now) and some builders use this as a weak form of randomness. In this challenge you will take advantage of a Dice Game contract by predicting the randomness in order to only roll winning dice!", + description: intl.formatMessage({ + id: "challenges.challenge-3-dice-game.description", + defaultMessage: + "🎰 Randomness is tricky on a public deterministic blockchain. The block hash is the result proof-of-work (for now) and some builders use this as a weak form of randomness. In this challenge you will take advantage of a Dice Game contract by predicting the randomness in order to only roll winning dice!", + }), previewImage: "/assets/challenges/diceGame.svg", dependencies: ["simple-nft-example", "decentralized-staking", "token-vendor"], }, "minimum-viable-exchange": { id: 5, branchName: "challenge-4-dex", - label: "βš–οΈ Build a DEX Challenge", + label: intl.formatMessage({ + id: "challenges.challenge-4-dex.label", + defaultMessage: "βš–οΈ Build a DEX Challenge", + }), disabled: false, - description: - "πŸ’΅ Build an exchange that swaps ETH to tokens and tokens to ETH. πŸ’° This is possible because the smart contract holds reserves of both assets and has a price function based on the ratio of the reserves. Liquidity providers are issued a token that represents their share of the reserves and fees...", + description: intl.formatMessage({ + id: "challenges.challenge-4-dex.description", + defaultMessage: + "πŸ’΅ Build an exchange that swaps ETH to tokens and tokens to ETH. πŸ’° This is possible because the smart contract holds reserves of both assets and has a price function based on the ratio of the reserves. Liquidity providers are issued a token that represents their share of the reserves and fees...", + }), previewImage: "assets/challenges/dex.svg", dependencies: ["simple-nft-example", "decentralized-staking", "token-vendor", "dice-game"], }, "state-channels": { id: 9, branchName: "challenge-9-state-channels", - label: "πŸ“Ί A State Channel Application Challenge", + label: intl.formatMessage({ + id: "challenges.challenge-9-state-channels.label", + defaultMessage: "πŸ“Ί A State Channel Application Challenge", + }), disabled: false, - description: - "πŸ›£οΈ The Ethereum blockchain has great decentralization & security properties but these properties come at a price: transaction throughput is low, and transactions can be expensive. This makes many traditional web applications infeasible on a blockchain... or does it? State channels look to solve these problems by allowing participants to securely transact off-chain while keeping interaction with Ethereum Mainnet at a minimum.", + description: intl.formatMessage({ + id: "challenges.challenge-9-state-channels.description", + defaultMessage: + "πŸ›£οΈ The Ethereum blockchain has great decentralization & security properties but these properties come at a price: transaction throughput is low, and transactions can be expensive. This makes many traditional web applications infeasible on a blockchain... or does it? State channels look to solve these problems by allowing participants to securely transact off-chain while keeping interaction with Ethereum Mainnet at a minimum.", + }), previewImage: "assets/challenges/state.svg", dependencies: ["simple-nft-example", "decentralized-staking", "token-vendor", "dice-game"], }, "learn-multisig": { id: 6, branchName: "challenge-3-multi-sig", - label: "πŸ‘› Multisig Wallet Challenge", + label: intl.formatMessage({ + id: "challenges.challenge-3-multi-sig.label", + defaultMessage: "πŸ‘› Multisig Wallet Challenge", + }), disabled: false, - description: - 'πŸ‘©β€πŸ‘©β€πŸ‘§β€πŸ‘§ Using a smart contract as a wallet we can secure assets by requiring multiple accounts to "vote" on transactions. The contract will keep track of transactions in an array of structs and owners will confirm or reject each one. Any transaction with enough confirmations can "execute".', + description: intl.formatMessage({ + id: "challenges.challenge-3-multi-sig.description", + defaultMessage: + 'πŸ‘©β€πŸ‘©β€πŸ‘§β€πŸ‘§ Using a smart contract as a wallet we can secure assets by requiring multiple accounts to "vote" on transactions. The contract will keep track of transactions in an array of structs and owners will confirm or reject each one. Any transaction with enough confirmations can "execute".', + }), previewImage: "assets/challenges/multiSig.svg", // Challenge locked until the builder completed these challenges dependencies: ["simple-nft-example", "decentralized-staking", "token-vendor", "dice-game"], @@ -97,10 +145,16 @@ export const challengeInfo = { "nft-cohort": { id: 7, branchName: "challenge-5-svg-nft-cohort", - label: "🎁 SVG NFT 🎫 Building Cohort Challenge", + label: intl.formatMessage({ + id: "challenges.challenge-5-svg-nft-cohort.label", + defaultMessage: "🎁 SVG NFT 🎫 Building Cohort Challenge", + }), disabled: false, - description: - "πŸ§™ Tinker around with cutting edge smart contracts that render SVGs in Solidity. 🧫 We quickly discovered that the render function needs to be public... πŸ€” This allows NFTs that own other NFTs to render their stash. Just wait until you see an Optimistic Loogie and a Fancy Loogie swimming around in the same Loogie Tank!", + description: intl.formatMessage({ + id: "challenges.challenge-5-svg-nft-cohort.description", + defaultMessage: + "πŸ§™ Tinker around with cutting edge smart contracts that render SVGs in Solidity. 🧫 We quickly discovered that the render function needs to be public... πŸ€” This allows NFTs that own other NFTs to render their stash. Just wait until you see an Optimistic Loogie and a Fancy Loogie swimming around in the same Loogie Tank!", + }), previewImage: "assets/challenges/dynamicSvgNFT.svg", // Challenge locked until the builder completed these challenges dependencies: ["simple-nft-example", "decentralized-staking", "token-vendor", "dice-game"], @@ -111,12 +165,12 @@ export const challengeInfo = { claim: "Join the 🎁 SVG NFT 🎫 Building Cohort", }, }, -}; +}); const githubChallengesRepoBaseRawUrl = { js: "https://raw.githubusercontent.com/scaffold-eth/scaffold-eth-challenges", ts: "https://raw.githubusercontent.com/scaffold-eth/scaffold-eth-typescript-challenges", }; -export const getGithubChallengeReadmeUrl = (challengeId, version) => - `${githubChallengesRepoBaseRawUrl[version]}/${challengeInfo[challengeId].branchName}/README.md`; +export const getGithubChallengeReadmeUrl = (challengeId, version, intl) => + `${githubChallengesRepoBaseRawUrl[version]}/${getChallengeInfo(intl)[challengeId].branchName}/README.md`; diff --git a/packages/react-app/src/index.jsx b/packages/react-app/src/index.jsx index 846a703c..64033624 100644 --- a/packages/react-app/src/index.jsx +++ b/packages/react-app/src/index.jsx @@ -1,19 +1,33 @@ -import React from "react"; +import React, { useState } from "react"; import ReactDOM from "react-dom"; import { BrowserRouter } from "react-router-dom"; import "@fontsource/space-grotesk/400.css"; import "@fontsource/space-grotesk/500.css"; import "./index.css"; import { ChakraProvider, ColorModeScript } from "@chakra-ui/react"; +import { IntlProvider } from "react-intl"; import theme from "./theme"; import App from "./App"; -ReactDOM.render( - - - - - - , - document.getElementById("root"), +import translationEn from "./lang/en.json"; +import translationEs from "./lang/es.json"; + +const translations = { + en: translationEn, + es: translationEs, +}; + +// TODO: change from ui +const userLocale = "en"; + +const Root = () => ( + + + + + + + + ); +ReactDOM.render(, document.getElementById("root")); diff --git a/packages/react-app/src/lang/en.json b/packages/react-app/src/lang/en.json new file mode 100644 index 00000000..bc63d4f7 --- /dev/null +++ b/packages/react-app/src/lang/en.json @@ -0,0 +1,98 @@ +{ + "account.connect-wallet": "Connect Wallet", + "announcementBanner": "Hey builder!! The BuidlGuidl is hosting a πŸ— Scaffold-Eth 2 hackathon. We are giving 10 ETH away to the best projects. {br}Come join the fun and learn the latest scaffold-eth techniques! Let's build a bunch of apps!", + "builderChallenges.challenges": "Challenges", + "builderChallenges.empty-state.button": "Start a challenge", + "builderChallenges.empty-state.description": "Show off your skills. Learn everything you need to build on Ethereum!", + "builderChallenges.empty-state.other-profile": "This builder hasn't completed any challenges.", + "builderChallenges.empty-state.title": "Start a new challenge", + "builderChallenges.start-challenge": "Start a challenge", + "builderChallenges.table.contract": "Contract", + "builderChallenges.table.live-demo": "Live Demo", + "builderChallenges.table.name": "Name", + "builderChallenges.table.status": "Status", + "builderChallenges.table.updated": "Updated", + "builderProfileCard.error.invalid-socials": "The usernames for the following socials are not correct: {invalidSocials}", + "builderProfileCard.error.updating-socials": "Can't update your socials. Please try again.", + "builderProfileCard.joined": "Joined {date}", + "builderProfileCard.modal-socials.header": "Update your socials", + "builderProfileCard.set-socials.tooltip": "It's our way of reaching out to you. We could sponsor you an ENS, offer to be part of a build or set up an ETH stream for you.", + "builderProfileCard.set-socials.warning": "You haven't set your socials", + "builderProfileCard.success.updating-socials": "Your social links have been updated", + "builderProfileCard.update-socials": "Update socials", + "builderProfileHeader.challenges": "challenges completed", + "builderProfileView.error-getting-challenges": "Can't get challenges metadata. Please try again", + "challengeDetailView.github-button": "View it on Github", + "challengeDetailView.modal.header": "Submit Challenge", + "challengeDetailView.submit-button": "Submit challenge", + "challengeDetailView.submit-button.tooltip.register": "You need to register as a builder", + "challengeStatusTag.modal.header": "Review feedback", + "challengeStatusTag.see-comments": "See comments", + "challengeSubmission.challenge-submitted": "Challenge submitted!", + "challengeSubmission.connect-wallet": "Connect your wallet to submit this Challenge.", + "challengeSubmission.deployed-url": "Deployed URL", + "challengeSubmission.deployed-url.tooltip": "Your deployed challenge URL on surge / s3 / ipfs", + "challengeSubmission.disabled": "This challenge is disabled.", + "challengeSubmission.error.both-fields-required": "Both fields are required", + "challengeSubmission.error.incorrect-contract.description": "Please submit your verified contract’s address on a valid testnet. e.g. https://goerli.etherscan.io/address/**Your Contract Address**", + "challengeSubmission.error.incorrect-contract.title": "Incorrect Etherscan Contract URL", + "challengeSubmission.error.invalid-url.description": "Valid URLs start with http:// or https://", + "challengeSubmission.error.invalid-url.title": "Please provide a valid URL", + "challengeSubmission.etherscan-url": "Etherscan Contract URL", + "challengeSubmission.etherscan-url.tooltip": "Your verified contract URL on Etherscan", + "challenges.buidl-guidl.description": "The BuidlGuidl is a curated group of Ethereum builders creating products, prototypes, and tutorials to enrich the web3 ecosystem. A place to show off your builds and meet other builders. Start crafting your Web3 portfolio by submitting your DEX, Multisig or SVG NFT build.", + "challenges.buidl-guidl.label": "Eligible to join 🏰️ BuidlGuidl", + "challenges.challenge-0-simple-nft.description": "🎫 Create a simple NFT to learn basics of πŸ— scaffold-eth. You'll use πŸ‘·β€β™€οΈ HardHat to compile and deploy smart contracts. Then, you'll use a template React app full of important Ethereum components and hooks. Finally, you'll deploy an NFT to a public network to share with friends! πŸš€", + "challenges.challenge-0-simple-nft.label": "🚩 Challenge 0: 🎟 Simple NFT Example", + "challenges.challenge-1-decentralized-staking.description": "🦸 A superpower of Ethereum is allowing you, the builder, to create a simple set of rules that an adversarial group of players can use to work together. In this challenge, you create a decentralized application where users can coordinate a group funding effort. The users only have to trust the code.", + "challenges.challenge-1-decentralized-staking.label": "🚩 Challenge 1: πŸ₯© Decentralized Staking App", + "challenges.challenge-2-token-vendor.description": "πŸ€– Smart contracts are kind of like \"always on\" vending machines that anyone can access. Let's make a decentralized, digital currency (an ERC20 token). Then, let's build an unstoppable vending machine that will buy and sell the currency. We'll learn about the \"approve\" pattern for ERC20s and how contract to contract interactions work.", + "challenges.challenge-2-token-vendor.label": "🚩 Challenge 2: 🏡 Token Vendor", + "challenges.challenge-3-dice-game.description": "🎰 Randomness is tricky on a public deterministic blockchain. The block hash is the result proof-of-work (for now) and some builders use this as a weak form of randomness. In this challenge you will take advantage of a Dice Game contract by predicting the randomness in order to only roll winning dice!", + "challenges.challenge-3-dice-game.label": "🚩 Challenge 3: 🎲 Dice Game", + "challenges.challenge-3-multi-sig.description": "πŸ‘©β€πŸ‘©β€πŸ‘§β€πŸ‘§ Using a smart contract as a wallet we can secure assets by requiring multiple accounts to \"vote\" on transactions. The contract will keep track of transactions in an array of structs and owners will confirm or reject each one. Any transaction with enough confirmations can \"execute\".", + "challenges.challenge-3-multi-sig.label": "πŸ‘› Multisig Wallet Challenge", + "challenges.challenge-4-dex.description": "πŸ’΅ Build an exchange that swaps ETH to tokens and tokens to ETH. πŸ’° This is possible because the smart contract holds reserves of both assets and has a price function based on the ratio of the reserves. Liquidity providers are issued a token that represents their share of the reserves and fees...", + "challenges.challenge-4-dex.label": "βš–οΈ Build a DEX Challenge", + "challenges.challenge-5-svg-nft-cohort.description": "πŸ§™ Tinker around with cutting edge smart contracts that render SVGs in Solidity. 🧫 We quickly discovered that the render function needs to be public... πŸ€” This allows NFTs that own other NFTs to render their stash. Just wait until you see an Optimistic Loogie and a Fancy Loogie swimming around in the same Loogie Tank!", + "challenges.challenge-5-svg-nft-cohort.label": "🎁 SVG NFT 🎫 Building Cohort Challenge", + "challenges.challenge-9-state-channels.description": "πŸ›£οΈ The Ethereum blockchain has great decentralization & security properties but these properties come at a price: transaction throughput is low, and transactions can be expensive. This makes many traditional web applications infeasible on a blockchain... or does it? State channels look to solve these problems by allowing participants to securely transact off-chain while keeping interaction with Ethereum Mainnet at a minimum.", + "challenges.challenge-9-state-channels.label": "πŸ“Ί A State Channel Application Challenge", + "error.access-error": "Access error", + "error.server-overloaded": "Sorry, the server is overloaded. πŸ§―πŸš’πŸ”₯", + "error.signature-from-wallet": "Couldn't get a signature from the Wallet", + "footer.built-with-love-at-buidlguidl": "Built with {heartIcon} at BuidlGuidl", + "footer.fork-me": "Fork me", + "general.Submit": "Submit", + "general.accepted": "Accepted", + "general.code": "Code", + "general.demo": "Demo", + "general.error.cant-get-message": "Can't get the message to sign. Please try again", + "general.error.signature-cancelled": "The signature was cancelled", + "general.error.submission-error": "Submission Error. Please try again.", + "general.locked": "Locked", + "general.quest": "Quest", + "general.rejected": "Rejected", + "general.role": "Role", + "general.submitted": "Submitted", + "general.update": "Update", + "header.activity": "Activity", + "header.builders": "Builders", + "header.portfolio": "Portfolio", + "header.review-submissions": "Review Submissions", + "index.learn-ethereum": "Learn how to build on Ethereum; the superpowers and the gotchas.", + "index.step-1": "Watch this quick video as an Intro to Ethereum Development.", + "index.step-2.1": "Then use πŸ— Scaffold-ETH to copy/paste each Solidity concept and tinker:", + "index.step-2.2": "global units, primitives, mappings, structs, modifiers, events,", + "index.step-2.3": "inheritance, sending eth, and payable/fallback functions.", + "index.step-3": "Watch this getting started playlist to become a power user and eth scripter.", + "index.step-4": "When you are ready to test your knowledge, Speed Run Ethereum:", + "joinBg.button.already-joined": "Already joined", + "joinBg.missing-socials.description": "In order to join the BuildGuidl you need to set your socials in your portfolio. It's our way to contact you.", + "joinBg.missing-socials.title": "Can't join the BuidlGuidl", + "joinBg.success.description": "Visit BuidlGuidl and start crafting your Web3 portfolio by submitting your DEX, Multisig or SVG NFT build.", + "joinBg.success.title": "Welcome to the BuildGuidl :)", + "joinedBuidlGuidlBanner.button": "View their profile on Buidlguidl", + "joinedBuidlGuidlBanner.label": "This builder has upgraded to BuidlGuidl", + "signatureSignUp.write-icon": "write icon" +} \ No newline at end of file diff --git a/packages/react-app/src/lang/es.json b/packages/react-app/src/lang/es.json new file mode 100644 index 00000000..69d21ebc --- /dev/null +++ b/packages/react-app/src/lang/es.json @@ -0,0 +1,5 @@ +{ + "account.connect-wallet": "Conectar Wallet", + "index.learn-ethereum": "Aprender a desarrollar en Ethereum; los super poderes y los trucos.", + "challenges.simple-nft-example.label": "🚩 Reto 0: 🎟 Ejemplo de NFT Simple" +} diff --git a/packages/react-app/src/views/BuilderProfileView.jsx b/packages/react-app/src/views/BuilderProfileView.jsx index b21f322a..ddc9e5a6 100644 --- a/packages/react-app/src/views/BuilderProfileView.jsx +++ b/packages/react-app/src/views/BuilderProfileView.jsx @@ -2,8 +2,9 @@ import React, { useEffect, useState } from "react"; import { useParams } from "react-router-dom"; import axios from "axios"; import { useToast, useColorModeValue, Container, SimpleGrid, GridItem, Box } from "@chakra-ui/react"; +import { useIntl } from "react-intl"; import BuilderProfileCard from "../components/builder/BuilderProfileCard"; -import { challengeInfo } from "../data/challenges"; +import { getChallengeInfo } from "../data/challenges"; import { BG_BACKEND_URL as bgBackendUrl } from "../constants"; import { getAcceptedChallenges } from "../helpers/builders"; import { getChallengeEventsForUser } from "../data/api"; @@ -26,6 +27,8 @@ export default function BuilderProfileView({ const [isLoadingBuilder, setIsLoadingBuilder] = useState(false); const [isBuilderOnBg, setIsBuilderOnBg] = useState(false); const [isLoadingTimestamps, setIsLoadingTimestamps] = useState(false); + const intl = useIntl(); + const challengeInfo = getChallengeInfo(intl); const toast = useToast({ position: "top", isClosable: true }); const toastVariant = useColorModeValue("subtle", "solid"); const bgColor = useColorModeValue("sre.cardBackground", "sreDark.cardBackground"); @@ -75,7 +78,10 @@ export default function BuilderProfileView({ setIsLoadingTimestamps(false); } catch (error) { toast({ - description: "Can't get challenges metadata. Please try again", + description: intl.formatMessage({ + id: "builderProfileView.error-getting-challenges", + defaultMessage: "Can't get challenges metadata. Please try again", + }), status: "error", variant: toastVariant, }); diff --git a/packages/react-app/src/views/ChallengeDetailView.jsx b/packages/react-app/src/views/ChallengeDetailView.jsx index 5619aaf5..db3fe5b9 100644 --- a/packages/react-app/src/views/ChallengeDetailView.jsx +++ b/packages/react-app/src/views/ChallengeDetailView.jsx @@ -26,7 +26,8 @@ import ReactMarkdown from "react-markdown"; import ChakraUIRenderer from "chakra-ui-markdown-renderer"; import rehypeRaw from "rehype-raw"; -import { challengeInfo } from "../data/challenges"; +import { FormattedMessage, useIntl } from "react-intl"; +import { getChallengeInfo } from "../data/challenges"; import ChallengeSubmission from "../components/ChallengeSubmission"; import { chakraMarkdownComponents } from "../helpers/chakraMarkdownTheme"; import { USER_ROLES, JS_CHALLENGE_REPO, TS_CHALLENGE_REPO } from "../helpers/constants"; @@ -42,6 +43,8 @@ export default function ChallengeDetailView({ serverUrl, address, userProvider, const [openModalOnLoad, setOpenModalOnLoad] = useState(false); const bgColor = useColorModeValue("sre.cardBackground", "sreDark.cardBackground"); + const intl = useIntl(); + const challengeInfo = getChallengeInfo(intl); const challenge = challengeInfo[challengeId]; const isWalletConnected = !!userRole; const isAnonymous = userRole && USER_ROLES.anonymous === userRole; @@ -50,14 +53,14 @@ export default function ChallengeDetailView({ serverUrl, address, userProvider, // In the future, this might be a fetch to the repos/branchs README // (Ideally fetched at build time) useEffect(() => { - getChallengeReadme(challengeId, "js") + getChallengeReadme(challengeId, "js", intl) .then(text => setDescriptionJs(parseGithubReadme(text))) .catch(() => setDescriptionJs(null)); - getChallengeReadme(challengeId, "ts") + getChallengeReadme(challengeId, "ts", intl) .then(text => setDescriptionTs(parseGithubReadme(text))) .catch(() => setDescriptionTs(null)); - }, [challengeId, challenge]); + }, [challengeId, challenge, intl]); useEffect(() => { if (!isWalletConnected || isAnonymous) return; @@ -98,13 +101,26 @@ export default function ChallengeDetailView({ serverUrl, address, userProvider, target="_blank" rel="noopener noreferrer" > - View it on Github + {" "} + - + + ) : ( + + ) + } + shouldWrapChildren + > @@ -146,7 +162,9 @@ export default function ChallengeDetailView({ serverUrl, address, userProvider, - Submit Challenge + + + ( export default function HomeView({ connectedBuilder, userProvider }) { const { primaryFontColor, bgColor } = useCustomColorModes(); const cardBgColor = useColorModeValue("sre.cardBackground", "sreDark.cardBackground"); - + const intl = useIntl(); + const challengeInfo = getChallengeInfo(intl); const builderAttemptedChallenges = useMemo(() => { if (!connectedBuilder?.challenges) { return []; @@ -60,7 +62,13 @@ export default function HomeView({ connectedBuilder, userProvider }) { }} textAlign="center" > - Learn how to build on Ethereum; the superpowers and the gotchas. + {chunks}, + }} + />
- Watch this{" "} - - quick video - {" "} - as an Intro to Ethereum Development. + ( + + {chunks} + + ), + }} + /> @@ -110,19 +124,27 @@ export default function HomeView({ connectedBuilder, userProvider }) { }} textAlign="center" > - Then use{" "} - - - πŸ— - {" "} - Scaffold-ETH - {" "} - to copy/paste each Solidity concept and tinker: + ( + + {chunks} + + ), + span: chunks => ( + + {chunks} + + ), + }} + /> - {" "} - - inheritance - - ,{" "} - - sending eth - - , and{" "} - - payable - - / - - fallback - {" "} - functions. + global units, primitives, mappings, structs, modifiers, events, + `} + values={{ + a_globalUnits: chunks => ( + + {chunks} + + ), + a_primitives: chunks => ( + + {chunks} + + ), + a_mappings: chunks => ( + + {chunks} + + ), + a_structs: chunks => ( + + {chunks} + + ), + a_modifiers: chunks => ( + + {chunks} + + ), + a_events: chunks => ( + + {chunks} + + ), + }} + />{" "} + inheritance, sending eth, and payable/fallback functions. + `} + values={{ + a_inheritance: chunks => ( + + {chunks} + + ), + a_sendingEth: chunks => ( + + {chunks} + + ), + a_payable: chunks => ( + + {chunks} + + ), + a_fallback: chunks => ( + + {chunks} + + ), + }} + /> @@ -194,16 +247,22 @@ export default function HomeView({ connectedBuilder, userProvider }) { }} textAlign="center" > - Watch this{" "} - - getting started playlist - {" "} - to become a power user and eth scripter. + ( + + {chunks} + + ), + }} + /> @@ -218,7 +277,10 @@ export default function HomeView({ connectedBuilder, userProvider }) { }} textAlign="center" > - When you are ready to test your knowledge, Speed Run Ethereum: + {Object.entries(challengeInfo).map(([challengeId, challenge], index, { length }) => (