diff --git a/api-server/server/utils/get-curriculum.js b/api-server/server/utils/get-curriculum.js index dfc0ae415c..f1fdcb6822 100644 --- a/api-server/server/utils/get-curriculum.js +++ b/api-server/server/utils/get-curriculum.js @@ -12,7 +12,7 @@ let curriculum; export async function getCurriculum() { curriculum = curriculum ? curriculum - : getChallengesForLang(process.env.LOCALE); + : getChallengesForLang(process.env.CURRICULUM_LOCALE); return curriculum; } diff --git a/client/gatsby-browser.js b/client/gatsby-browser.js index 32aa254ac2..97bb014d75 100644 --- a/client/gatsby-browser.js +++ b/client/gatsby-browser.js @@ -1,7 +1,9 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Provider } from 'react-redux'; +import { I18nextProvider } from 'react-i18next'; +import i18n from './i18n/config'; import { createStore } from './src/redux/createStore'; import AppMountNotifier from './src/components/AppMountNotifier'; import layoutSelector from './utils/gatsby/layoutSelector'; @@ -11,7 +13,9 @@ const store = createStore(); export const wrapRootElement = ({ element }) => { return ( - element} /> + + element} /> + ); }; diff --git a/client/gatsby-config.js b/client/gatsby-config.js index f1f3f1ca1b..2e9d5fc7ac 100644 --- a/client/gatsby-config.js +++ b/client/gatsby-config.js @@ -34,7 +34,7 @@ module.exports = { options: { name: 'challenges', source: buildChallenges, - onSourceChange: replaceChallengeNode(config.locale), + onSourceChange: replaceChallengeNode(config.curriculumLocale), curriculumPath: localeChallengesRootDir } }, diff --git a/client/i18n/allLangs.js b/client/i18n/allLangs.js new file mode 100644 index 0000000000..cdfaaf35ca --- /dev/null +++ b/client/i18n/allLangs.js @@ -0,0 +1,38 @@ +/* An error will be thrown if the CLIENT_LOCALE and CURRICULUM_LOCALE variables + * from the .env file aren't found in their respective arrays below + */ +const availableLangs = { + client: ['english', 'espanol'], + curriculum: ['english', 'chinese'] +}; + +// Each client language needs an entry in the rest of the variables below + +/* These strings set the i18next langauge. It needs to be the two character + * string for the language to take advantage of available functionality. + * Use a 639-1 code here https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes + */ +const i18nextCodes = { + english: 'en', + espanol: 'es' +}; + +// These are for the language selector dropdown menu in the footer +const langDisplayNames = { + english: 'English', + espanol: 'Español' +}; + +/* These are for formatting dates and numbers. Used with JS .toLocaleString(). + * There's an example in profile/components/Camper.js + * List: https://github.com/unicode-cldr/cldr-dates-modern/tree/master/main + */ +const langCodes = { + english: 'en-US', + espanol: 'es-419' +}; + +exports.availableLangs = availableLangs; +exports.i18nextCodes = i18nextCodes; +exports.langDisplayNames = langDisplayNames; +exports.langCodes = langCodes; diff --git a/client/i18n/config.js b/client/i18n/config.js new file mode 100644 index 0000000000..e2b38a2120 --- /dev/null +++ b/client/i18n/config.js @@ -0,0 +1,32 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; + +const { environment, clientLocale } = require('../config/env'); +const { i18nextCodes } = require('./allLangs'); + +const i18nextCode = i18nextCodes[clientLocale]; + +i18n.use(initReactI18next).init({ + fallbackLng: i18nextCode, + lng: i18nextCode, + // we only load one language since each language will have it's own server + resources: { + [i18nextCode]: { + translations: require(`./locales/${clientLocale}/translations.json`) + } + }, + ns: ['translations'], + defaultNS: 'translations', + returnObjects: true, + debug: environment === 'development', + interpolation: { + escapeValue: false + }, + react: { + wait: true + } +}); + +i18n.languages = clientLocale; + +export default i18n; diff --git a/client/i18n/configForTests.js b/client/i18n/configForTests.js new file mode 100644 index 0000000000..6a1a5ce6ee --- /dev/null +++ b/client/i18n/configForTests.js @@ -0,0 +1,16 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; + +i18n.use(initReactI18next).init({ + debug: true, + defaultNS: 'translations', + fallbackLng: 'en', + interpolation: { + escapeValue: false + }, + lng: 'en', + ns: ['translations'], + resources: { en: { translations: {} } } +}); + +export default i18n; diff --git a/client/i18n/locales.test.js b/client/i18n/locales.test.js new file mode 100644 index 0000000000..20d697fb0d --- /dev/null +++ b/client/i18n/locales.test.js @@ -0,0 +1,60 @@ +/* global expect */ +import { translationsSchema } from './translations-schema'; +import { motivationSchema } from './motivation-schema'; +import { + availableLangs, + i18nextCodes, + langDisplayNames, + langCodes +} from './allLangs'; + +const fs = require('fs'); +const { expectToMatchSchema, setup } = require('jest-json-schema-extended'); + +setup(); + +const filesThatShouldExist = [ + { + name: 'translations.json', + schema: translationsSchema + }, + { + name: 'motivation.json', + schema: motivationSchema + } +]; + +const path = `${process.cwd()}/i18n/locales`; + +describe('Locale tests:', () => { + availableLangs.client.forEach(lang => { + describe(`-- ${lang} --`, () => { + filesThatShouldExist.forEach(file => { + // check that each json file exists + test(`${file.name} file exists`, () => { + const exists = fs.existsSync(`${path}/${lang}/${file.name}`); + expect(exists).toBeTruthy(); + }); + + // check that each of the json files match the schema + test(`${file.name} has correct schema`, async () => { + const jsonFile = fs.readFileSync(`${path}/${lang}/${file.name}`); + let json = await JSON.parse(jsonFile); + expectToMatchSchema(json, file.schema); + }); + }); + + test(`has a two character entry in the i18nextCodes variable`, () => { + expect(i18nextCodes[lang].length).toBe(2); + }); + + test(`has an entry in the langDisplayNames variable`, () => { + expect(langDisplayNames[lang].length).toBeGreaterThan(0); + }); + + test(`has an entry in the langCodes variable`, () => { + expect(langCodes[lang].length).toBeGreaterThan(0); + }); + }); + }); +}); diff --git a/config/motivational-quotes.json b/client/i18n/locales/english/motivation.json similarity index 99% rename from config/motivational-quotes.json rename to client/i18n/locales/english/motivation.json index 5a572b5304..d0cf5c66ae 100644 --- a/config/motivational-quotes.json +++ b/client/i18n/locales/english/motivation.json @@ -1,6 +1,6 @@ { "compliments": [ - "Over the top!", + "Over the top!", "Down the rabbit hole we go!", "Bring that rain!", "Target acquired.", diff --git a/client/i18n/locales/english/translations.json b/client/i18n/locales/english/translations.json new file mode 100644 index 0000000000..a5dfd950be --- /dev/null +++ b/client/i18n/locales/english/translations.json @@ -0,0 +1,466 @@ +{ + "meta": { + "title": "Learn to Code — For Free — Coding Courses for Busy People", + "description": "Learn to code — for free.", + "keywords": [ + "javascript", + "js", + "website", + "web", + "development", + "free", + "code", + "camp", + "course", + "courses", + "html", + "css", + "react", + "redux", + "api", + "front", + "back", + "end", + "learn", + "tutorial", + "programming" + ], + "youre-unsubscribed": "You have been unsubscribed" + }, + "buttons": { + "logged-in-cta-btn": "Get started (it's free)", + "logged-out-cta-btn": "Sign in to save your progress (it's free)", + "view-curriculum": "View the Curriculum", + "first-lesson": "Go to the first lesson", + "close": "Close", + "edit": "Edit", + "show-code": "Show Code", + "show-solution": "Show Solution", + "frontend": "Front End", + "backend": "Back End", + "view": "View", + "show-cert": "Show Certification", + "claim-cert": "Claim Certification", + "save-progress": "Save Progress", + "accepted-honesty": "You have accepted our Academic Honesty Policy.", + "agree": "Agree", + "save-portfolio": "Save this portfolio item", + "remove-portfolio": "Remove this portfolio item", + "add-portfolio": "Add a new portfolio Item", + "download-data": "Download your data", + "public": "Public", + "private": "Private", + "off": "Off", + "on": "On", + "sign-in": "Sign in", + "sign-out": "Sign out", + "curriculum": "Curriculum", + "forum": "Forum", + "profile": "Profile", + "update-settings": "Update my account settings", + "sign-me-out": "Sign me out of freeCodeCamp", + "flag-user": "Flag This User's Account for Abuse", + "current-challenge": "Go to current challenge", + "try-again": "Try again", + "menu": "Menu", + "settings": "Settings", + "take-me": "Take me to the Challenges", + "check-answer": "Check your answer", + "get-hint": "Get a Hint", + "ask-for-help": "Ask for Help", + "create-post": "Create a help post on the forum", + "cancel": "Cancel", + "reset-lesson": "Reset this lesson", + "run": "Run", + "run-test": "Run the Tests", + "reset": "Reset", + "reset-code": "Reset All Code", + "help": "Help", + "get-help": "Get Help", + "watch-video": "Watch a Video", + "resubscribe": "You can click here to resubscribe", + "click-here": "Click here to sign in", + "save": "Save", + "no-thanks": "No thanks", + "yes-please": "Yes please", + "update-email": "Update my Email", + "verify-email": "Verify Email", + "submit-and-go": "Submit and go to next challenge", + "go-to-next": "Go to next challenge", + "ask-later": "Ask me later" + }, + "landing": { + "big-heading-1": "Learn to code — for free.", + "big-heading-2": "Build projects.", + "big-heading-3": "Earn certifications.", + "h2-heading": "Since 2014, more than 40,000 freeCodeCamp.org graduates have gotten jobs at tech companies including:", + "hero-img-description": "freeCodeCamp students at a local study group in South Korea.", + "as-seen-in": "As seen in:", + "testimonials": { + "heading": "Here is what our alumni say about freeCodeCamp:", + "shawn": { + "location": "Shawn Wang in Singapore", + "occupation": "Software Engineer at Amazon", + "testimony": "\"It's scary to change careers. I only gained confidence that I could code by working through the hundreds of hours of free lessons on freeCodeCamp. Within a year I had a six-figure job as a Software Engineer. freeCodeCamp changed my life.\"" + }, + "sarah": { + "location": "Sarah Chima in Nigeria", + "occupation": "Software Engineer at ChatDesk", + "testimony": "\"freeCodeCamp was the gateway to my career as a software developer. The well-structured curriculum took my coding knowledge from a total beginner level to a very confident level. It was everything I needed to land my first dev job at an amazing company.\"" + }, + "emma": { + "location": "Emma Bostian in Sweden", + "occupation": "Software Engineer at Spotify", + "testimony": "\"I've always struggled with learning JavaScript. I've taken many courses but freeCodeCamp's course was the one which stuck. Studying JavaScript as well as data structures and algorithms on freeCodeCamp gave me the skills and confidence I needed to land my dream job as a software engineer at Spotify.\"" + } + }, + "certification-heading": "Earn free verified certifications in:" + }, + "settings": { + "share-projects": "Share your non-freeCodeCamp projects, articles or accepted pull requests.", + "privacy": "The settings in this section enable you to control what is shown on your freeCodeCamp public portfolio.", + "data": "To see what data we hold on your account, click the \"Download your data\" button below", + "disabled": "Your certifications will be disabled, if set to private.", + "claim-legacy": "Once you've earned the following freeCodeCamp certifications, you'll be able to claim the {{cert}}:", + "for": "Account Settings for {{username}}", + "username": { + "contains invalid characters": "Username \"{{username}}\" contains invalid characters", + "is too short": "Username \"{{username}}\" is too short", + "is a reserved error code": "Username \"{{username}}\" is a reserved error code", + "unavailable": "Username not available", + "validating": "Validating username...", + "available": "Username is available", + "change": "Please note, changing your username will also change the URL to your profile and your certifications." + }, + "labels": { + "username": "Username", + "name": "Name", + "location": "Location", + "picture": "Picture", + "about": "About", + "personal": "Personal Website", + "title": "Title", + "url": "URL", + "image": "Image", + "description": "Description", + "project-name": "Project Name", + "solution": "Solution", + "solution-for": "Solution for {{projectTitle}}", + "my-profile": "My profile", + "my-name": "My name", + "my-location": "My location", + "my-about": "My about", + "my-points": "My points", + "my-heatmap": "My heatmap", + "my-certs": "My certifications", + "my-portfolio": "My portfolio", + "my-timeline": "My timeline", + "my-donations": "My donations", + "night-mode": "Night Mode" + }, + "headings": { + "certs": "Certifications", + "legacy-certs": "Legacy Certifications", + "honesty": "Academic Honesty Policy", + "internet": "Your Internet Presence", + "portfolio": "Portfolio Settings", + "privacy": "Privacy Settings" + }, + "danger": { + "heading": "Danger Zone", + "be-careful": "Please be careful. Changes in this section are permanent.", + "reset": "Reset all of my progress", + "delete": "Delete my account", + "delete-title": "Delete My Account", + "delete-p1": "This will really delete all your data, including all your progress and account information.", + "delete-p2": "We won't be able to recover any of it for you later, even if you change your mind.", + "delete-p3": "If there's something we could do better, send us an email instead and we'll do our best: <0>{{email}}", + "nevermind": "Nevermind, I don't want to delete my account", + "certain": "I am 100% certain. Delete everything related to this account", + "reset-heading": "Reset My Progress", + "reset-p1": "This will really delete all of your progress, points, completed challenges, our records of your projects, any certifications you have, everything.", + "reset-p2": "We won't be able to recover any of it for you later, even if you change your mind.", + "nevermind-2": "Nevermind, I don't want to delete all of my progress", + "reset-confirm": "Reset everything. I want to start from the beginning" + }, + "email": { + "missing": "You do not have an email associated with this account.", + "heading": "Email Settings", + "not-verified": "Your email has not been verified.", + "check": "Please check your email, or <0>request a new verification email here.", + "current": "Current Email", + "new": "New Email", + "confirm": "Confirm New Email", + "weekly": "Send me Quincy's weekly email" + }, + "honesty": { + "p1": "Before you can claim a verified certification, you must accept our Academic Honesty Pledge, which reads:", + "p2": "\"I understand that plagiarism means copying someone else’s work and presenting the work as if it were my own, without clearly attributing the original author.\"", + "p3": "\"I understand that plagiarism is an act of intellectual dishonesty, and that people usually get kicked out of university or fired from their jobs if they get caught plagiarizing.\"", + "p4": "\"Aside from using open source libraries such as jQuery and Bootstrap, and short snippets of code which are clearly attributed to their original author, 100% of the code in my projects was written by me, or along with another person going through the freeCodeCamp curriculum with whom I was pair programming in real time.\"", + "p5": "\"I pledge that I did not plagiarize any of my freeCodeCamp.org work. I understand that freeCodeCamp.org’s team will audit my projects to confirm this.\"", + "p6": "In the situations where we discover instances of unambiguous plagiarism, we will replace the person in question’s certification with a message that \"Upon review, this account has been flagged for academic dishonesty.\"", + "p7": "As an academic institution that grants achievement-based certifications, we take academic honesty very seriously. If you have any questions about this policy, or suspect that someone has violated it, you can email <0>{{email}} and we will investigate." + } + }, + "profile": { + "you-not-public": "You have not made your portfolio public.", + "username-not-public": "{{username}} has not made their portfolio public.", + "you-change-privacy": "You need to change your privacy setting in order for your portfolio to be seen by others. This is a preview of how your portfolio will look when made public.", + "username-change-privacy": "{{username}} needs to change their privacy setting in order for you to view their portfolio.", + "supporter": "Supporter", + "contributor": "Top Contributor", + "no-certs": "No certifications have been earned under the current curriculum", + "fcc-certs": "freeCodeCamp Certifications", + "longest-streak": "Longest Streak:", + "current-streak": "Current Streak:", + "portfolio": "Portfolio", + "timeline": "Timeline", + "none-completed": "No challenges have been completed yet.", + "get-started": "Get started here.", + "challenge": "Challenge", + "completed": "Completed", + "add-linkedin": "Add this certification to my LinkedIn profile", + "add-twitter": "Share this certification on Twitter", + "tweet": "I just earned the {{certTitle}} certification @freeCodeCamp! Check it out here: {{certURL}}", + "avatar": "{{username}}'s avatar", + "joined": "Joined {{date}}", + "total-points": "{{count}} total point", + "total-points_plural": "{{count}} total points", + "points": "{{count}} point on {{date}}", + "points_plural": "{{count}} points on {{date}}", + "screen-shot": "A screen shot of {{title}}", + "page-number": "{{pageNumber}} of {{totalPages}}" + }, + "footer": { + "tax-exempt-status": "freeCodeCamp is a donor-supported tax-exempt 501(c)(3) nonprofit organization (United States Federal Tax Identification Number: 82-0779546)", + "mission-statement": "Our mission: to help people learn to code for free. We accomplish this by creating thousands of videos, articles, and interactive coding lessons - all freely available to the public. We also have thousands of freeCodeCamp study groups around the world.", + "donation-initiatives": "Donations to freeCodeCamp go toward our education initiatives, and help pay for servers, services, and staff.", + "donate-text": "You can", + "donate-link": "make a tax-deductible donation here", + "trending-guides": "Trending Guides", + "our-nonprofit": "Our Nonprofit", + "links": { + "about": "About", + "alumni": "Alumni Network", + "open-source": "Open Source", + "shop": "Shop", + "support": "Support", + "sponsors": "Sponsors", + "honesty": "Academic Honesty", + "coc": "Code of Conduct", + "privacy": "Privacy Policy", + "tos": "Terms of Service", + "copyright": "Copyright Policy" + }, + "language": "Language:" + }, + "learn": { + "heading": "Welcome to freeCodeCamp's curriculum.", + "welcome-1": "Welcome back, {{name}}.", + "welcome-2": "Welcome to freeCodeCamp.org", + "start-at-beginning": "If you are new to coding, we recommend you <0>start at the beginning.", + "read-this": { + "heading": "Please slow down and read this.", + "p1": "freeCodeCamp is a proven path to your first software developer job.", + "p2": "More than 40,000 people have gotten developer jobs after completing this – including at big companies like Google and Microsoft.", + "p3": "If you are new to programming, we recommend you start at the beginning and earn these certifications in order.", + "p4": "To earn each certification, build its 5 required projects and get all their tests to pass.", + "p5": "You can add these certifications to your résumé or LinkedIn. But more important than the certifications is the practice you get along the way.", + "p6": "If you feel overwhelmed, that is normal. Programming is hard.", + "p7": "Practice is the key. Practice, practice, practice.", + "p8": "And this curriculum will give you thousands of hours of hands-on programming practice.", + "p9": "And if you want to learn more math and computer science theory, we also have thousands of hours of video courses on <0>freeCodeCamp's YouTube channel.", + "p10": "If you want to get a developer job or freelance clients, programming skills will be just part of the puzzle. You also need to build your personal network and your reputation as a developer.", + "p11": "You can do this on Twitter and GitHub, and also on <0>the freeCodeCamp forum.", + "p12": "Happy coding!" + }, + "upcoming-lessons": "Upcoming Lessons", + "learn": "Learn", + "add-subtitles": "Help improve or add subtitles", + "wrong-answer": "Sorry, that's not the right answer. Give it another try?", + "check-answer": "Click the button below to check your answer.", + "solution-link": "Solution Link", + "github-link": "GitHub Link", + "submit-and-go": "Submit and go to my next challenge", + "i-completed": "I've completed this challenge", + "test-output": "Your test output will go here", + "running-tests": "// running tests", + "tests-completed": "// tests completed", + "console-output": "// console output", + "sign-in-save": "Sign in to save your progress", + "download-solution": "Download my solution", + "percent-complete": "{{percent}}% complete", + "tried-rsa": "If you've already tried the <0>Read-Search-Ask method, then you can ask for help on the freeCodeCamp forum.", + "rsa": "Read, search, ask", + "reset": "Reset this lesson?", + "reset-warn": "Are you sure you wish to reset this lesson? The editors and tests will be reset.", + "reset-warn-2": "This cannot be undone", + "scrimba-tip": "Tip: If the mini-browser is covering the code, click and drag to move it. Also, feel free to stop and edit the code in the video at any time.", + "chal-preview": "Challenge Preview" + }, + "donate": { + "title": "Support our nonprofit", + "processing": "We are processing your donation.", + "thank-you": "Thank you for being a supporter.", + "thank-you-2": "Thank you for being a supporter of freeCodeCamp. You currently have a recurring donation.", + "additional": "You can make an additional one-time donation of any amount using this link: <0>{{url}}", + "help-more": "Help us do more", + "error": "Something went wrong with your donation.", + "free-tech": "Your donations will support free technology education for people all over the world.", + "gift-frequency": "Select gift frequency:", + "gift-amount": "Select gift amount:", + "confirm": "Confirm your donation", + "confirm-2": "Confirm your one-time donation of ${{usd}}", + "confirm-3": "Confirm your donation of ${{usd}} / month", + "confirm-4": "Confirm your donation of ${{usd}} / year", + "your-donation": "Your ${{usd}} donation will provide {{hours}} hours of learning to people around the world.", + "your-donation-2": "Your ${{usd}} donation will provide {{hours}} hours of learning to people around the world each month.", + "your-donation-3": "Your ${{usd}} donation will provide {{hours}} hours of learning to people around the world each year.", + "duration": "Become a one-time supporter of our nonprofit.", + "duration-2": "Become a monthly supporter of our nonprofit.", + "duration-3": "Become an annual supporter of our nonprofit", + "duration-4": "Become a supporter of our nonprofit", + "nicely-done": "Nicely done. You just completed {{block}}.", + "credit-card": "Credit Card", + "credit-card-2": "Or donate with a credit card:", + "paypal": "with PayPal:", + "need-email": "We need a valid email address to which we can send your donation tax receipt.", + "went-wrong": "Something went wrong processing your donation. Your card has not been charged.", + "valid-info": "Please enter valid email address, credit card number, and expiration date.", + "valid-email": "Please enter a valid email address.", + "valid-card": "Please enter valid credit card number and expiration date.", + "email-receipt": "Email (we'll send you a tax-deductible donation receipt):", + "need-help": "Need help with your current or past donations?", + "forward-receipt": "Forward a copy of your donation receipt to donors@freecodecamp.org and tell us how we can help.", + "efficiency": "freeCodeCamp is a highly efficient education nonprofit.", + "why-donate-1": "When you donate to freeCodeCamp, you help people learn new skills and provide for their families.", + "why-donate-2": "You also help us create new resources for you to use to expand your own technology skills.", + "failed-pay": "Uh - oh. It looks like your transaction didn't go through. Could you please try again?", + "try-again": "Please try again.", + "card-number": "Your Card Number:", + "expiration": "Expiration Date:", + "only-you": "Only you can see this message. Congratulations on earning this certification. It’s no easy task. Running freeCodeCamp isn’t easy either. Nor is it cheap. Help us help you and many other people around the world. Make a tax-deductible supporting donation to our nonprofit today." + }, + "report": { + "sign-in": "You need to be signed in to report a user", + "details": "Please provide as much detail as possible about the account or behavior you are reporting.", + "portfolio": "Report a users portfolio", + "portfolio-2": "Do you want to report {{username}}'s portfolio for abuse?", + "notify-1": "We will notify the community moderators' team, and a send copy of this report to your email: {{email}}", + "notify-2": "We may get back to you for more information, if required.", + "what": "What would you like to report?", + "submit": "Submit the report" + }, + "404": { + "page-not-found": "Page not found", + "not-found": "404 Not Found:", + "heres-a-quote": "We couldn't find what you were looking for, but here is a quote:" + }, + "search": { + "label": "Search", + "placeholder": "Search 6,000+ tutorial", + "see-results": "See all results for {{searchQuery}}", + "no-tutorials": "No tutorials found", + "try": "Looking for something? Try the search bar on this page.", + "no-results": "We could not find anything relating to <0>{{query}}" + }, + "misc": { + "offline": "You appear to be offline, your progress may not be saved", + "unsubscribed": "You have successfully been unsubscribed", + "keep-coding": "Whatever you go on to, keep coding!", + "email-signup": "Email Sign Up", + "quincy": "- Quincy Larson, the teacher who founded freeCodeCamp.org", + "email-blast": "By the way, each Friday I send an email with 5 links about programming and computer science. I send these to about 4 million people. Would you like me to send this to you, too?", + "update-email-1": "Update your email address", + "update-email-2": "Update your email address here:", + "email": "Email", + "and": "and" + }, + "icons": { + "gold-cup": "Gold Cup", + "avatar": "Default Avatar", + "avatar-2": "An avatar coding with a laptop", + "donate": "Donate with PayPal", + "fail": "Test Failed", + "not-passed": "Not Passed", + "passed": "Passed", + "heart": "Heart", + "initial": "Initial", + "info": "Intro Information", + "spacer": "Spacer", + "toggle": "Toggle Checkmark" + }, + "aria": { + "fcc-logo": "freeCodeCamp Logo", + "answer": "Answer", + "linkedin": "Link to {{username}}'s LinkedIn", + "github": "Link to {{username}}'s GitHub", + "website": "Link to {{username}}'s website", + "twitter": "Link to {{username}}'s Twitter", + "first-page": "Go to first page", + "previous-page": "Go to previous page", + "next-page": "Go to next page", + "last-page": "Go to last page" + }, + "flash": { + "msg-1": "To claim a certification, you must first accept our academic honesty policy", + "msg-2": "Something really weird happened, if it happens again, please consider raising an issue on https://github.com/freeCodeCamp/freeCodeCamp/issues/new", + "msg-3": "Something is not quite right. A report has been generated and the freeCodeCamp.org team have been notified", + "msg-4": "Something went wrong, please check and try again", + "msg-5": "Your account has been successfully deleted", + "msg-6": "Your progress has been reset", + "msg-7": "You are not authorized to continue on this route", + "msg-8": "We couldn't find what you were looking for. Please check and try again", + "msg-9": "Something went wrong updating your account. Please check and try again", + "msg-10": "We have updated your preferences", + "msg-11": "Email format is invalid", + "msg-12": "currentChallengeId is not a valid challenge ID", + "msg-13": "Theme is invalid", + "msg-14": "Theme already set", + "msg-15": "Your theme has been updated!", + "msg-16": "Username is already associated with this account", + "msg-17": "Username is already associated with a different account", + "msg-18": "We have updated your username to {{username}}", + "msg-19": "We could not log you out, please try again in a moment", + "msg-20": "The email encoded in the link is incorrectly formatted", + "msg-21": "Oops, something is not right, please request a fresh link to sign in / sign up", + "msg-22": "Looks like the link you clicked has expired, please request a fresh link, to sign in", + "msg-23": "Success! You have signed in to your account. Happy Coding!", + "msg-24": "We are moving away from social authentication for privacy reasons. Next time we recommend using your email address: {{email}} to sign in instead.", + "msg-25": "We need your name so we can put it on your certification. Add your name to your account settings and click the save button. Then we can issue your certification.", + "msg-26": "It looks like you have not completed the necessary steps. Please complete the required projects to claim the {{name}} Certification.", + "msg-27": "It looks like you already have claimed the {{name}} Certification", + "msg-28": "@{{username}}, you have successfully claimed the {{name}} Certification! Congratulations on behalf of the freeCodeCamp.org team!", + "msg-29": "Something went wrong with the verification of {{name}}, please try again. If you continue to receive this error, you can send a message to support@freeCodeCamp.org to get help.", + "msg-30": "Error claiming {{certName}}", + "msg-31": "We could not find a user with the username \"{{username}}\"", + "msg-32": "This user needs to add their name to their account in order for others to be able to view their certification.", + "msg-33": "This user is not eligible for freeCodeCamp.org certifications at this time.", + "msg-34": "{{username}} has chosen to make their portfolio private. They will need to make their portfolio public in order for others to be able to view their certification.", + "msg-35": "{{username}} has chosen to make their certifications private. They will need to make their certifications public in order for others to be able to view them.", + "msg-36": "{{username}} has not yet agrees to our Academic Honesty Pledge.", + "msg-37": "It looks like user {{username}} is not {{cert}} certified", + "msg-38": "That does not appear to be a valid challenge submission", + "msg-39": "You have not provided the valid links for us to inspect your work.", + "msg-40": "No social account found", + "msg-41": "Invalid social account", + "msg-42": "No {{website}} account associated", + "msg-43": "You've successfully unlinked your {{website}}", + "msg-44": "Check if you have provided a username and a report", + "msg-45": "A report was sent to the team with {{email}} in copy" + }, + "validation": { + "msg-1": "There is a maximum limit of 288 characters, you have {{charsLeft}} left", + "msg-2": "This email is the same as your current email", + "msg-3": "We could not validate your email correctly, please ensure it is correct", + "msg-4": "Both new email addresses must be the same", + "msg-5": "A title is required", + "msg-6": "Title is too short", + "msg-7": "Title is too long", + "msg-8": "We could not validate your URL correctly, please ensure it is correct", + "msg-9": "URL must start with http or https", + "msg-10": "URL must link directly to an image file", + "msg-11": "Please use a valid URL" + } +} diff --git a/client/i18n/locales/espanol/motivation.json b/client/i18n/locales/espanol/motivation.json new file mode 100644 index 0000000000..4c4cc6e711 --- /dev/null +++ b/client/i18n/locales/espanol/motivation.json @@ -0,0 +1,29 @@ +{ + "compliments": [ + "¡Excesivo!", + "¡Por la madriguera del conejo vamos!", + "¡Trae esa lluvia!", + "Objetivo localizado.", + "¡Siente esa necesidad de velocidad!", + "¡Tienes agallas!", + "¡Al infinito y más allá!", + "¡De nuevo!", + "¡Adelante!", + "¡Desafío destruido!", + "¡Estás que ardes!" + ], + "motivationalQuotes": [ + { + "quote": "La acción es la clave fundamental para todo éxito.", + "author": "Pablo Picasso" + }, + { + "quote": "Trabaja duro en silencio, que el éxito sea tu ruido.", + "author": "Frank Ocean" + }, + { + "quote": "No importa lo lento que vayas, siempre y cuando no te detengas.", + "author": "Confucio" + } + ] +} diff --git a/client/i18n/locales/espanol/translations.json b/client/i18n/locales/espanol/translations.json new file mode 100644 index 0000000000..4da1b9ff50 --- /dev/null +++ b/client/i18n/locales/espanol/translations.json @@ -0,0 +1,444 @@ +{ + "meta": { + "title": "Aprenda a codificar gratis - cursos de codificación para personas ocupadas", + "description": "Aprenda a codificar en casa. Construye proyectos. Obtén certificaciones. Desde 2014, más de 40.000 graduados de freeCodeCamp.org han conseguido trabajos en empresas de tecnología como Google, Apple, Amazon y Microsoft.", + "keywords": ["javascript","js","sitio web","web","desarrollo","free","code","camp","curso","cursos","html","css","react","redux","api","front","back","end","aprender","tutorial","programación"], + "youre-unsubscribed": "Se ha dado de baja" + }, + "buttons": { + "logged-in-cta-btn": "Empiece (es gratis)", + "logged-out-cta-btn": "Inicia sesión para guardar tu progreso (es gratis)", + "view-curriculum": "Ver el plan de estudios", + "first-lesson": "Ir a la primera lección", + "close": "Cerrar", + "edit": "Editar", + "show-code": "Mostrar código", + "show-solution": "Mostrar solución", + "frontend": "Interfaz", + "backend": "Backend", + "view": "Ver", + "show-cert": "Mostrar certificación", + "claim-cert": "Certificación de reclamo", + "save-progress": "Guardar progreso", + "accepted-honesty": "Ha aceptado nuestra Política de honestidad académica.", + "agree": "De acuerdo", + "save-portfolio": "Guardar este artículo de cartera", + "remove-portfolio": "Eliminar este artículo de cartera", + "add-portfolio": "Agregar un nuevo artículo de cartera", + "download-data": "Descarga tus datos", + "public": "Pública", + "private": "Privada", + "off": "Apagada", + "on": "En", + "sign-in": "Registrarse", + "sign-out": "desconectar", + "curriculum": "Plan de estudios", + "forum": "Foro", + "profile": "Perfil", + "update-settings": "Actualizar la configuración de mi cuenta", + "sign-me-out": "Cerrar sesión en freeCodeCamp", + "flag-user": "Marcar la cuenta del usuario por abuso", + "current-challenge": "Ir al desafío actual", + "try-again": "Inténtalo de nuevo", + "menu": "Menú", + "settings": "Configuraciones", + "take-me": "Llévame a los desafíos", + "check-answer": "Comprueba tu respuesta", + "get-hint": "Obtén un consejo", + "ask-for-help": "Pedir ayuda", + "create-post": "Crea una publicación de ayuda en el foro", + "cancel": "Cancelar", + "reset-lesson": "Restablecer esta lección", + "run": "correr", + "run-test": "Ejecutar las pruebas", + "reset": "Reiniciar", + "reset-code": "Restablecer todo el código", + "help": "Ayuda", + "get-help": "Consigue ayuda", + "watch-video": "Ver un video", + "resubscribe": "Puede hacer clic aquí para volver a suscribirse", + "click-here": "Clic aquí para ingresar", + "save": "Salvar", + "no-thanks": "No, gracias", + "yes-please": "sí por favor", + "update-email": "Actualizar mi correo electrónico", + "verify-email": "Verificar correo electrónico", + "submit-and-go": "Enviar y pasar al siguiente desafío", + "go-to-next": "Ir al siguiente desafío", + "ask-later": "Pregúntame Luego" + }, + "landing": { + "big-heading-1": "Aprenda a codificar en casa.", + "big-heading-2": "Construye proyectos.", + "big-heading-3": "Obtén certificaciones.", + "h2-heading": "Desde 2014, más de 40.000 graduados de freeCodeCamp.org han conseguido trabajos en empresas de tecnología, entre las que se incluyen:", + "hero-img-description" : "Estudiantes de freeCodeCamp en un grupo de estudio local en Corea del Sur.", + "as-seen-in": "Como se vio en:", + "testimonials": { + "heading": "Esto es lo que dicen nuestros antiguos alumnos sobre freeCodeCamp:", + "shawn": { + "location": "Shawn Wang en Singapur", + "occupation": "Ingeniero de software en Amazon", + "testimony": "\"Da miedo cambiar de carrera. Solo gané la confianza de que podía codificar trabajando con los cientos de horas de lecciones gratuitas en freeCodeCamp. En un año tenía un trabajo de seis cifras como ingeniero de software. freeCodeCamp cambió mi vida.\"" + }, + "sarah": { + "location": "Sarah Chima en Nigeria", + "occupation": "Ingeniera de software en ChatDesk", + "testimony": "\"freeCodeCamp fue la puerta de entrada a mi carrera como desarrollador de software. El plan de estudios bien estructurado llevó mi conocimiento de codificación de un nivel principiante total a un nivel muy seguro. Era todo lo que necesitaba para conseguir mi primer trabajo de desarrollador en una empresa increíble.\"" + }, + "emma": { + "location": "Emma Bostian en Suecia", + "occupation": "Ingeniera de software en Spotify", + "testimony": "\"Siempre he tenido problemas para aprender JavaScript. He tomado muchos cursos, pero el curso de freeCodeCamp fue el que se quedó. Estudiar JavaScript, así como estructuras de datos y algoritmos en freeCodeCamp me dio las habilidades y la confianza que necesitaba para conseguir el trabajo de mis sueños como ingeniero de software en Spotify.\"" + } + }, + "certification-heading": "Obtenga certificaciones verificadas gratuitas en:" + }, + "settings": { + "share-projects": "Comparta sus proyectos, artículos o solicitudes de extracción aceptadas que no sean de freeCodeCamp.", + "privacy": "La configuración de esta sección le permite controlar lo que se muestra en su cartera pública de freeCodeCamp.", + "data": "Para ver qué datos tenemos en su cuenta, haga clic en el botón \"Descarga tus datos\" a continuación", + "disabled": "Sus certificaciones se deshabilitarán si se configuran como privadas.", + "claim-legacy": "Una vez que haya obtenido las siguientes certificaciones de FreeCodeCamp, podrá reclamar las {{cert}}:", + "for": "Configuración de cuenta para {{username}}", + "username": { + "contains invalid characters": "El nombre de usuario \"{{username}}\" contiene caracteres no válidos", + "is too short": "El nombre de usuario \"{{username}}\" es demasiado corto", + "is a reserved error code": "El nombre de usuario \"{{username}}\" es un código de error reservado", + "unavailable": "Nombre de usuario no disponible", + "validating": "Validando nombre de usuario...", + "available": "Nombre de usuario disponible", + "change": "Tenga en cuenta que cambiar su nombre de usuario también cambiará la URL de su perfil y sus certificaciones." + }, + "labels": { + "username": "Nombre de usuario", + "name": "Nombre", + "location": "Ubicación", + "picture": "Imagen", + "about": "Acerca de", + "personal": "Sitio web personal", + "title": "Título", + "url": "URL", + "image": "Imagen", + "description": "Descripción", + "project-name": "Nombre del proyecto", + "solution": "Solución", + "solution-for": "Solución para {{projectTitle}}", + "my-profile": "Mi perfil", + "my-name": "Mi nombre", + "my-location": "Mi ubicacion", + "my-about": "Mi acerca de", + "my-points": "Mis puntos", + "my-heatmap": "Mi mapa de calor", + "my-certs": "Mis certificaciones", + "my-portfolio": "Mi portafolio", + "my-timeline": "Mi cronología", + "my-donations": "Mis donaciones", + "night-mode": "Modo nocturno" + }, + "headings": { + "certs": "Certificaciones", + "legacy-certs": "Certificaciones heredadas", + "honesty": "Política de honestidad académica", + "internet": "Tu presencia en Internet", + "portfolio": "Configuración de la cartera", + "privacy": "La configuración de privacidad" + }, + "danger": { + "heading": "Zona peligrosa", + "be-careful": "Por favor tenga cuidado. Los cambios en esta sección son permanentes.", + "reset": "Restablecer todo mi progreso", + "delete": "Borrar mi cuenta", + "delete-title": "Borrar mi cuenta", + "delete-p1": "Esto realmente eliminará todos sus datos, incluido todo su progreso e información de cuenta.", + "delete-p2": "No podremos recuperar nada más tarde, incluso si cambia de opinión.", + "delete-p3": "Si hay algo que podamos hacer mejor, envíanos un correo electrónico y haremos todo lo posible: <0>{{email}}", + "nevermind": "No importa, no quiero borrar mi cuenta", + "certain": "Estoy 100% seguro. Eliminar todo lo relacionado con esta cuenta", + "reset-heading": "Restablecer mi progreso", + "reset-p1": "Esto realmente eliminará todo su progreso, puntos, desafíos completados, nuestros registros de sus proyectos, cualquier certificación que tenga, todo.", + "reset-p2": "No podremos recuperar nada más tarde, incluso si cambia de opinión.", + "nevermind-2": "No importa, no quiero borrar todo mi progreso", + "reset-confirm": "Reinicia todo. Quiero empezar desde el principio" + }, + "email": { + "missing": "No tiene un correo electrónico asociado con esta cuenta.", + "heading": "Ajustes del correo electrónico", + "not-verified": "Tu email no se ha verificado.", + "check": "Por favor revise su correo electrónico o <0>solicite un nuevo correo electrónico de verificación aquí.", + "current": "Correo electrónico actual", + "new": "Nuevo correo electrónico", + "confirm": "confirme nuevo correo electrónico", + "weekly": "Envíame el correo electrónico semanal de Quincy" + }, + "honesty": { + "p1": "Antes de poder reclamar una certificación verificada, debe aceptar nuestro Compromiso de honestidad académica, que dice:", + "p2": "\"Entiendo que el plagio significa copiar el trabajo de otra persona y presentar el trabajo como si fuera mío, sin atribuir claramente el autor original.\"", + "p3": "\"Entiendo que el plagio es un acto de deshonestidad intelectual, y que la gente suele ser expulsada de la universidad o despedida de sus trabajos si la descubren plagiando.\"", + "p4": "\"Además de usar bibliotecas de código abierto como jQuery y Bootstrap, y pequeños fragmentos de código que se atribuyen claramente a su autor original, el 100% del código de mis proyectos fue escrito por mí, o junto con otra persona que pasa por el plan de estudios freeCodeCamp con con quien estaba programando en pareja en tiempo real.\"", + "p5": "\"Prometo no plagiar ninguno de mis trabajos en freeCodeCamp.org. Entiendo que el equipo de freeCodeCamp.org auditará mis proyectos para confirmar esto.\"", + "p6": "En las situaciones en las que descubramos casos de plagio inequívoco, reemplazaremos la certificación de la persona en cuestión con un mensaje que diga: \"Tras la revisión, esta cuenta ha sido marcada por deshonestidad académica.\"", + "p7": "Como institución académica que otorga certificaciones basadas en logros, nos tomamos muy en serio la honestidad académica. Si tiene alguna pregunta sobre esta política o sospecha que alguien la ha violado, puede enviar un correo electrónico a <0> {{email}} y lo investigaremos." + } + }, + "profile": { + "you-not-public": "No ha hecho pública su cartera.", + "username-not-public": "no ha hecho pública su cartera.", + "you-change-privacy": "Debe cambiar su configuración de privacidad para que otros puedan ver su cartera. Esta es una vista previa de cómo se verá su portafolio cuando se haga público.", + "username-change-privacy": "necesita cambiar su configuración de privacidad para que puedas ver su cartera.", + "supporter": "Seguidora", + "contributor": "Colaboradora principal", + "no-certs": "No se han obtenido certificaciones bajo el plan de estudios actual", + "fcc-certs": "Certificaciones freeCodeCamp", + "longest-streak": "Racha más larga:", + "current-streak": "Racha actual:", + "portfolio": "portafolio", + "timeline": "Cronología", + "none-completed": "Aún no se han completado desafíos.", + "get-started": "Empiece aquí.", + "challenge": "Desafío", + "completed": "Terminada", + "add-linkedin": "Agregar esta certificación a mi perfil de LinkedIn", + "add-twitter": "Comparte esta certificación en Twitter", + "tweet": "¡Acabo de obtener la certificación {{certTitle}} @freeCodeCamp! Compruébalo aquí: {{certURL}}", + "avatar": "Avatar de {{username}}", + "joined": "Se unió en {{date}}", + "total-points": "{{count}} punto total", + "total-points_plural": "{{count}} puntos totales", + "points": "{{count}} punto el {{date}}", + "points_plural": "{{count}} puntos el {{date}}", + "screen-shot": "Una captura de pantalla de {{title}}", + "page-number": "{{pageNumber}} of {{totalPages}}" + }, + "footer": { + "tax-exempt-status": "freeCodeCamp es una organización sin fines de lucro 501 (c) (3) exenta de impuestos respaldada por donantes (Número de identificación fiscal federal de los Estados Unidos: 82-0779546)", + "mission-statement": "Nuestra misión: ayudar a las personas a aprender a codificar de forma gratuita. Logramos esto mediante la creación de miles de videos, artículos y lecciones de codificación interactivas, todos disponibles gratuitamente para el público. También tenemos miles de grupos de estudio de freeCodeCamp en todo el mundo.", + "donation-initiatives": "Las donaciones a freeCodeCamp se destinan a nuestras iniciativas educativas y ayudan a pagar los servidores, los servicios y el personal.", + "donate-text": "Puedes", + "donate-link": "hacer una donación deducible de impuestos aquí", + "trending-guides": "Guías de tendencias", + "our-nonprofit": "Nuestra sin fines de lucro", + "links": { + "about": "Acerca de", + "alumni": "Red de antiguos alumnos", + "open-source": "Fuente abierta", + "shop": "tienda", + "support": "Apoyo", + "sponsors": "Patrocinadoras", + "honesty": "Honestidad académica", + "coc": "Código de Conducta", + "privacy": "Política de privacidad", + "tos": "Términos de servicio", + "copyright": "Política de derechos de autor" + }, + "language": "Idioma:" + }, + "learn": { + "heading": "Bienvenido al plan de estudios de freeCodeCamp.", + "welcome-1": "Bienvenido de nuevo, {{name}}.", + "welcome-2": "Bienvenido a freeCodeCamp.org", + "start-at-beginning": "Si es nuevo en la codificación, le recomendamos que <0>comience por el principio.", + "read-this": { + "heading": "Por favor, reduzca la velocidad y lea esto.", + "p1": "freeCodeCamp es un camino probado hacia su primer trabajo como desarrollador de software.", + "p2": "Más de 40.000 personas han obtenido trabajos de desarrollador después de completar esto, incluso en grandes empresas como Google y Microsoft.", + "p3": "Si es nuevo en la programación, le recomendamos que comience por el principio y obtenga estas certificaciones en orden.", + "p4": "Para obtener cada certificación, construya sus 5 proyectos requeridos y obtenga todas sus pruebas para aprobar.", + "p5": "Puede agregar estas certificaciones a su currículum o LinkedIn. Pero más importante que las certificaciones es la práctica que obtienes en el camino.", + "p6": "Si se siente abrumado, es normal. La programación es difícil.", + "p7": "La práctica es la clave. Práctica práctica práctica.", + "p8": "Y este plan de estudios le brindará miles de horas de práctica de programación práctica.", + "p9": "Y si desea aprender más teoría de las matemáticas y la informática, también tenemos miles de horas de cursos en video en el canal de <0>YouTube de freeCodeCamp.", + "p10": "Si desea obtener un trabajo de desarrollador o clientes independientes, las habilidades de programación serán solo una parte del rompecabezas. También necesita construir su red personal y su reputación como desarrollador.", + "p11": "Puede hacer esto en Twitter y GitHub, y también en <0>el foro freeCodeCamp.", + "p12": "Codificación feliz!" + }, + "upcoming-lessons": "Próximas lecciones", + "learn": "Aprender", + "add-subtitles": "Ayudar a mejorar o agregar subtítulos", + "wrong-answer": "Lo siento, esa no es la respuesta correcta. ¿Darle otra oportunidad?", + "check-answer": "Haga clic en el botón de abajo para verificar su respuesta.", + "solution-link": "Enlace de solución", + "github-link": "Enlace de GitHub", + "submit-and-go": "Envía y pasa a mi próximo desafío", + "i-completed": "He completado este desafío", + "test-output": "Su salida de prueba irá aquí", + "running-tests": "// ejecutando pruebas", + "tests-completed": "// pruebas completadas", + "console-output": "// salida de consola", + "sign-in-save": "Inicia sesión para guardar tu progreso", + "download-solution": "Descarga mi solución", + "percent-complete": "{{percent}}% Completa", + "tried-rsa": "Si ya probaste el <0>Leer-Buscar-Preguntar método, entonces puede pedir ayuda en el foro freeCodeCamp.", + "rsa": "Leer, buscar, preguntar", + "reset": "¿Restablecer esta lección?", + "reset-warn": "¿Está seguro de que desea restablecer esta lección? Los editores y las pruebas se restablecerán.", + "reset-warn-2": "Esto no se puede deshacer", + "scrimba-tip": "Sugerencia: Si el mini-navegador cubre el código, haga clic y arrastre para moverlo. Además, siéntase libre de detener y editar el código en el video en cualquier momento.", + "chal-preview": "Vista previa del desafío" + }, + "donate": { + "title": "Apoya a nuestra organización sin fines de lucro", + "processing": "Estamos procesando tu donación.", + "thank-you": "Gracias por ser una partidaria.", + "thank-you-2": "Gracias por apoyar a freeCodeCamp. Actualmente tienes una donación recurrente.", + "additional": "Puede hacer una donación adicional por única vez de cualquier monto utilizando este enlace: <0>{{url}}", + "help-more": "Ayúdanos a hacer más", + "error": "Algo salió mal con tu donación.", + "free-tech": "Sus donaciones apoyarán la educación tecnológica gratuita para personas de todo el mundo.", + "gift-frequency": "Seleccione la frecuencia de los obsequios:", + "gift-amount": "Seleccione el monto del regalo:", + "confirm": "Confirma tu donación", + "confirm-2": "Confirme su donación única de ${{usd}}", + "confirm-3": "Confirma tu donación de ${{usd}} / mes", + "confirm-4": "Confirma tu donación de ${{usd}} / año", + "your-donation": "Su donación de ${{usd}} proporcionará {{hours}} horas de aprendizaje a personas de todo el mundo.", + "your-donation-2": "Su donación de ${{usd}} proporcionará {{hours}} horas de aprendizaje a personas de todo el mundo cada mes.", + "your-donation-3": "Su donación de ${{usd}} proporcionará {{hours}} horas de aprendizaje a personas de todo el mundo cada año.", + "duration": "Conviértase en un colaborador único de nuestra organización sin fines de lucro.", + "duration-2": "Conviértase en un patrocinador mensual de nuestra organización sin fines de lucro.", + "duration-3": "Conviértase en un patrocinador anual de nuestra organización sin fines de lucro.", + "duration-4": "Conviértase en un partidario de nuestra organización sin fines de lucro.", + "nicely-done": "Bien hecho. Acaba de completar {{block}}.", + "credit-card": "Tarjeta de crédito", + "credit-card-2": "O dona con tarjeta de crédito:", + "paypal": "con PayPal:", + "need-email": "Necesitamos una dirección de correo electrónico válida a la que podamos enviar su recibo de impuestos de donación.", + "went-wrong": "Algo salió mal al procesar tu donación. Su tarjeta no ha sido cargada.", + "valid-info": "Ingrese una dirección de correo electrónico válida, un número de tarjeta de crédito y una fecha de vencimiento.", + "valid-email": "Por favor, introduce una dirección de correo electrónico válida.", + "valid-card": "Ingrese un número de tarjeta de crédito válido y la fecha de vencimiento.", + "email-receipt": "Correo electrónico (le enviaremos un recibo de donación deducible de impuestos):", + "need-help": "¿Necesita ayuda con sus donaciones actuales o pasadas?", + "forward-receipt": "Envíe una copia de su recibo de donación a donors@freecodecamp.org y díganos cómo podemos ayudar.", + "efficiency": "freeCodeCamp es una organización educativa sin fines de lucro altamente eficiente.", + "why-donate-1": "Cuando dona a freeCodeCamp, ayuda a las personas a aprender nuevas habilidades y a mantener a sus familias.", + "why-donate-2": "También nos ayuda a crear nuevos recursos para que los utilice a fin de ampliar sus propias habilidades tecnológicas.", + "failed-pay": "UH oh. Parece que su transacción no se realizó. ¿Podrías intentarlo de nuevo?", + "try-again": "Inténtalo de nuevo.", + "card-number": "Su número de tarjeta:", + "expiration": "Fecha de caducidad:", + "only-you": "Solo tú puedes ver este mensaje. Felicitaciones por obtener esta certificación. No es tarea fácil. Ejecutar freeCodeCamp tampoco es fácil. Tampoco es barato. Ayúdanos a ayudarte a ti y a muchas otras personas en todo el mundo. Realice una donación de apoyo deducible de impuestos a nuestra organización sin fines de lucro hoy." + }, + "report": { + "sign-in": "Necesitas iniciar sesión para informar a una usuario", + "details": "Proporcione tantos detalles como sea posible sobre la cuenta o el comportamiento que está denunciando.", + "portfolio": "Informar una cartera de usuarios", + "portfolio-2": "¿Quieres denunciar la cartera de {{username}} por abuso?", + "notify-1": "Notificaremos al equipo de moderadores de la comunidad y le enviaremos una copia de este informe a su correo electrónico: <0>{{email}}", + "notify-2": "Es posible que nos comuniquemos con usted para obtener más información, si es necesario.", + "what": "que te gustaria informar?", + "submit": "Envíe el informe" + }, + "404": { + "page-not-found": "Página no encontrada", + "not-found": "No encontrada:", + "heres-a-quote": "No pudimos encontrar lo que buscaba, pero aquí hay una cita:" + }, + "search": { + "label": "Buscar", + "placeholder": "Buscar más de 6000 tutoriales", + "see-results": "Ver todos los resultados para {{searchQuery}}", + "no-tutorials": "No se encontraron tutoriales", + "try": "¿En busca de algo? Prueba la barra de búsqueda en esta página.", + "no-results": "No pudimos encontrar nada relacionado con <0>{{query}}" + }, + "misc": { + "offline": "Parece que no estás conectado, es posible que tu progreso no se guarde", + "unsubscribed": "Se ha dado de baja con éxito", + "keep-coding": "Sea lo que sea que hagas, ¡sigue codificando!", + "email-signup": "Regístrese por correo electrónico", + "quincy": "- Quincy Larson, el maestro que fundó freeCodeCamp.org", + "email-blast": "Por cierto, cada viernes envío un correo electrónico con 5 enlaces sobre programación e informática. Los envío a unos 4 millones de personas. ¿Quieres que te envíe esto también?", + "update-email-1": "Actualice su dirección de correo electrónico", + "update-email-2": "Actualice su dirección de correo electrónico aquí:", + "email": "Correo electrónico", + "and": "y" + }, + "icons": { + "gold-cup": "Copa de Oro", + "avatar": "Avatar predeterminado", + "avatar-2": "Un avatar codificando con una computadora portátil.", + "donate": "Donar con PayPal", + "fail": "Prueba fallida", + "not-passed": "No pasó", + "passed": "Aprobada", + "heart": "Corazón", + "initial": "Inicial", + "info": "Información de introducción", + "spacer": "Espaciador", + "toggle": "Alternar marca de verificación" + }, + "aria": { + "fcc-logo": "freeCodeCamp Logotipo", + "answer": "Responder", + "linkedin": "Enlace a LinkedIn de {{username}}", + "github": "Enlace a GitHub de {{username}}", + "website": "Enlace al sitio web de {{username}}", + "twitter": "Enlace a Twitter de {{username}}", + "first-page": "Ir a la primera pagina", + "previous-page": "Regresar a la pagina anterior", + "next-page": "Ir a la página siguiente", + "last-page": "Ir a la última página" + }, + "flash": { + "msg-1": "Para reclamar una certificación, primero debe aceptar nuestra política de honestidad académica", + "msg-2": "Algo realmente extraño sucedió. Si vuelve a ocurrir, considere plantear un problema en https://github.com/freeCodeCamp/freeCodeCamp/issues/new", + "msg-3": "Algo no está bien. Se ha generado un informe y se ha notificado al equipo de freeCodeCamp.org", + "msg-4": "Algo salió mal, verifique e intente nuevamente", + "msg-5": "Su cuenta ha sido eliminada correctamente", + "msg-6": "Tu progreso se ha restablecido", + "msg-7": "No tienes autorización para continuar por esta ruta", + "msg-8": "No pudimos encontrar lo que buscaba. Por favor revisa e intenta de nuevo", + "msg-9": "Se produjo un error al actualizar su cuenta. Por favor revisa e intenta de nuevo", + "msg-10": "Hemos actualizado sus preferencias", + "msg-11": "El formato de correo electrónico no es válido", + "msg-12": "currentChallengeId no es un ID de desafío válido", + "msg-13": "El tema no es válido", + "msg-14": "Tema ya establecido", + "msg-15": "¡Tu tema ha sido actualizado!", + "msg-16": "El nombre de usuario ya está asociado a esta cuenta", + "msg-17": "El nombre de usuario ya está asociado a una cuenta diferente", + "msg-18": "Hemos actualizado su nombre de usuario a {{username}}", + "msg-19": "No pudimos cerrar la sesión. Vuelve a intentarlo en un momento.", + "msg-20": "El correo electrónico codificado en el enlace tiene un formato incorrecto", + "msg-21": "Vaya, algo no está bien, solicite un enlace nuevo para iniciar sesión / registrarse", + "msg-22": "Parece que el enlace en el que hizo clic ha caducado. Solicite un enlace nuevo para iniciar sesión.", + "msg-23": "¡Éxito! Ha iniciado sesión en su cuenta. ¡Feliz codificación!", + "msg-24": "Nos estamos alejando de la autenticación social por razones de privacidad. La próxima vez, le recomendamos que utilice su dirección de correo electrónico: {{email}} para iniciar sesión.", + "msg-25": "Necesitamos su nombre para poder incluirlo en su certificación. Agregue su nombre a la configuración de su cuenta y haga clic en el botón Guardar. Entonces podemos emitir su certificación.", + "msg-26": "Parece que no ha completado los pasos necesarios. Complete los proyectos requeridos para reclamar la certificación {{name}}.", + "msg-27": "Parece que ya reclamó la certificación {{name}}", + "msg-28": "@{{username}}, reclamó con éxito la certificación {{name}}. ¡Felicitaciones en nombre del equipo de freeCodeCamp.org!", + "msg-29": "Se produjo un error con la verificación de {{name}}. Vuelve a intentarlo. Si continúa recibiendo este error, puede enviar un mensaje a support@freeCodeCamp.org para obtener ayuda.", + "msg-30": "Error al reclamar {{certName}}", + "msg-31": "No pudimos encontrar un usuario con el nombre de usuario \"{{username}}\"", + "msg-32": "Este usuario debe agregar su nombre a su cuenta para que otros puedan ver su certificación.", + "msg-33": "Este usuario no es elegible para las certificaciones de freeCodeCamp.org en este momento.", + "msg-34": "{{username}} ha optado por hacer que su cartera sea privada. Deberán hacer pública su cartera para que otros puedan ver su certificación.", + "msg-35": "{{username}} ha optado por hacer que sus certificaciones sean privadas. Deberán hacer públicas sus certificaciones para que otros puedan verlas.", + "msg-36": "{{username}} aún no está de acuerdo con nuestro compromiso de honestidad académica.", + "msg-37": "Parece que el usuario {{username}} no está {{cert}} certificado por", + "msg-38": "Eso no parece ser un envío de desafío válido", + "msg-39": "No ha proporcionado los enlaces válidos para que inspeccionemos su trabajo.", + "msg-40": "No se encontró ninguna cuenta social", + "msg-41": "Cuenta social no válida", + "msg-42": "No hay cuenta de {{website}} asociada", + "msg-43": "Has desvinculado correctamente tu {{website}}", + "msg-44": "Compruebe si ha proporcionado un nombre de usuario y un informe", + "msg-45": "Se envió un informe al equipo con {{email}} en copia" + }, + "validation": { + "msg-1": "Hay un límite máximo de 288 caracteres, te quedan {{charsLeft}}", + "msg-2": "Este correo electrónico es el mismo que su correo electrónico actual", + "msg-3": "No pudimos validar su correo electrónico correctamente, asegúrese de que sea correcto", + "msg-4": "Ambas nuevas direcciones de correo electrónico deben ser iguales", + "msg-5": "Se requiere un título", + "msg-6": "El título es demasiado corto", + "msg-7": "El título es demasiado largo", + "msg-8": "No pudimos validar su URL correctamente, asegúrese de que sea correcta", + "msg-9": "La URL debe comenzar con http o https", + "msg-10": "La URL debe vincularse directamente a un archivo de imagen", + "msg-11": "Utilice una URL válida" + } +} diff --git a/client/i18n/motivation-schema.js b/client/i18n/motivation-schema.js new file mode 100644 index 0000000000..4002e74696 --- /dev/null +++ b/client/i18n/motivation-schema.js @@ -0,0 +1,22 @@ +/* eslint-disable camelcase */ +/* This is used for testing to make sure the motivation.json files + * in each language have the correct structure + */ +const { + arrayOfItems, + strictObject, + stringType +} = require('jest-json-schema-extended'); + +const motivationSchema = strictObject({ + compliments: arrayOfItems(stringType, { minItems: 1 }), + motivationalQuotes: arrayOfItems( + strictObject({ + quote: stringType, + author: stringType + }), + { minItems: 1 } + ) +}); + +exports.motivationSchema = motivationSchema; diff --git a/client/i18n/translations-schema.js b/client/i18n/translations-schema.js new file mode 100644 index 0000000000..bd943d807a --- /dev/null +++ b/client/i18n/translations-schema.js @@ -0,0 +1,456 @@ +/* eslint-disable camelcase */ +/* This is used for testing. If a translations.json file doesn't match the + * structure here exactly, the tests will fail. + */ +const { + arrayOfItems, + strictObject, + stringType +} = require('jest-json-schema-extended'); + +const translationsSchema = strictObject({ + meta: strictObject({ + title: stringType, + description: stringType, + keywords: arrayOfItems(stringType, { minItems: 1 }), + 'youre-unsubscribed': stringType + }), + buttons: strictObject({ + 'logged-in-cta-btn': stringType, + 'logged-out-cta-btn': stringType, + 'view-curriculum': stringType, + 'first-lesson': stringType, + close: stringType, + edit: stringType, + 'show-code': stringType, + 'show-solution': stringType, + frontend: stringType, + backend: stringType, + view: stringType, + 'show-cert': stringType, + 'claim-cert': stringType, + 'save-progress': stringType, + 'accepted-honesty': stringType, + agree: stringType, + 'save-portfolio': stringType, + 'remove-portfolio': stringType, + 'add-portfolio': stringType, + 'download-data': stringType, + public: stringType, + private: stringType, + off: stringType, + on: stringType, + 'sign-in': stringType, + 'sign-out': stringType, + curriculum: stringType, + forum: stringType, + profile: stringType, + 'update-settings': stringType, + 'sign-me-out': stringType, + 'flag-user': stringType, + 'current-challenge': stringType, + 'try-again': stringType, + menu: stringType, + settings: stringType, + 'take-me': stringType, + 'check-answer': stringType, + 'get-hint': stringType, + 'ask-for-help': stringType, + 'create-post': stringType, + cancel: stringType, + 'reset-lesson': stringType, + run: stringType, + 'run-test': stringType, + reset: stringType, + 'reset-code': stringType, + help: stringType, + 'get-help': stringType, + 'watch-video': stringType, + resubscribe: stringType, + 'click-here': stringType, + save: stringType, + 'no-thanks': stringType, + 'yes-please': stringType, + 'update-email': stringType, + 'verify-email': stringType, + 'submit-and-go': stringType, + 'go-to-next': stringType, + 'ask-later': stringType + }), + landing: strictObject({ + 'big-heading-1': stringType, + 'big-heading-2': stringType, + 'big-heading-3': stringType, + 'h2-heading': stringType, + 'hero-img-description': stringType, + 'as-seen-in': stringType, + testimonials: strictObject({ + heading: stringType, + shawn: strictObject({ + location: stringType, + occupation: stringType, + testimony: stringType + }), + sarah: strictObject({ + location: stringType, + occupation: stringType, + testimony: stringType + }), + emma: strictObject({ + location: stringType, + occupation: stringType, + testimony: stringType + }) + }), + 'certification-heading': stringType + }), + settings: strictObject({ + 'share-projects': stringType, + privacy: stringType, + data: stringType, + disabled: stringType, + 'claim-legacy': stringType, + for: stringType, + username: strictObject({ + 'contains invalid characters': stringType, + 'is too short': stringType, + 'is a reserved error code': stringType, + unavailable: stringType, + validating: stringType, + available: stringType, + change: stringType + }), + labels: strictObject({ + username: stringType, + name: stringType, + location: stringType, + picture: stringType, + about: stringType, + personal: stringType, + title: stringType, + url: stringType, + image: stringType, + description: stringType, + 'project-name': stringType, + solution: stringType, + 'solution-for': stringType, + 'my-profile': stringType, + 'my-name': stringType, + 'my-location': stringType, + 'my-about': stringType, + 'my-points': stringType, + 'my-heatmap': stringType, + 'my-certs': stringType, + 'my-portfolio': stringType, + 'my-timeline': stringType, + 'my-donations': stringType, + 'night-mode': stringType + }), + headings: strictObject({ + certs: stringType, + 'legacy-certs': stringType, + honesty: stringType, + internet: stringType, + portfolio: stringType, + privacy: stringType + }), + danger: strictObject({ + heading: stringType, + 'be-careful': stringType, + reset: stringType, + delete: stringType, + 'delete-title': stringType, + 'delete-p1': stringType, + 'delete-p2': stringType, + 'delete-p3': stringType, + nevermind: stringType, + certain: stringType, + 'reset-heading': stringType, + 'reset-p1': stringType, + 'reset-p2': stringType, + 'nevermind-2': stringType, + 'reset-confirm': stringType + }), + email: strictObject({ + missing: stringType, + heading: stringType, + 'not-verified': stringType, + check: stringType, + current: stringType, + new: stringType, + confirm: stringType, + weekly: stringType + }), + honesty: strictObject({ + p1: stringType, + p2: stringType, + p3: stringType, + p4: stringType, + p5: stringType, + p6: stringType, + p7: stringType + }) + }), + profile: strictObject({ + 'you-not-public': stringType, + 'username-not-public': stringType, + 'you-change-privacy': stringType, + 'username-change-privacy': stringType, + supporter: stringType, + contributor: stringType, + 'no-certs': stringType, + 'fcc-certs': stringType, + 'longest-streak': stringType, + 'current-streak': stringType, + portfolio: stringType, + timeline: stringType, + 'none-completed': stringType, + 'get-started': stringType, + challenge: stringType, + completed: stringType, + 'add-linkedin': stringType, + 'add-twitter': stringType, + tweet: stringType, + avatar: stringType, + joined: stringType, + 'total-points': stringType, + 'total-points_plural': stringType, + points: stringType, + points_plural: stringType, + 'screen-shot': stringType, + 'page-number': stringType + }), + footer: strictObject({ + 'tax-exempt-status': stringType, + 'mission-statement': stringType, + 'donation-initiatives': stringType, + 'donate-text': stringType, + 'donate-link': stringType, + 'trending-guides': stringType, + 'our-nonprofit': stringType, + links: strictObject({ + about: stringType, + alumni: stringType, + 'open-source': stringType, + shop: stringType, + support: stringType, + sponsors: stringType, + honesty: stringType, + coc: stringType, + privacy: stringType, + tos: stringType, + copyright: stringType + }), + language: stringType + }), + learn: strictObject({ + heading: stringType, + 'welcome-1': stringType, + 'welcome-2': stringType, + 'start-at-beginning': stringType, + 'read-this': strictObject({ + heading: stringType, + p1: stringType, + p2: stringType, + p3: stringType, + p4: stringType, + p5: stringType, + p6: stringType, + p7: stringType, + p8: stringType, + p9: stringType, + p10: stringType, + p11: stringType, + p12: stringType + }), + 'upcoming-lessons': stringType, + learn: stringType, + 'add-subtitles': stringType, + 'wrong-answer': stringType, + 'check-answer': stringType, + 'solution-link': stringType, + 'github-link': stringType, + 'submit-and-go': stringType, + 'i-completed': stringType, + 'test-output': stringType, + 'running-tests': stringType, + 'tests-completed': stringType, + 'console-output': stringType, + 'sign-in-save': stringType, + 'download-solution': stringType, + 'percent-complete': stringType, + 'tried-rsa': stringType, + rsa: stringType, + reset: stringType, + 'reset-warn': stringType, + 'reset-warn-2': stringType, + 'scrimba-tip': stringType, + 'chal-preview': stringType + }), + donate: strictObject({ + title: stringType, + processing: stringType, + 'thank-you': stringType, + 'thank-you-2': stringType, + additional: stringType, + 'help-more': stringType, + error: stringType, + 'free-tech': stringType, + 'gift-frequency': stringType, + 'gift-amount': stringType, + confirm: stringType, + 'confirm-2': stringType, + 'confirm-3': stringType, + 'confirm-4': stringType, + 'your-donation': stringType, + 'your-donation-2': stringType, + 'your-donation-3': stringType, + duration: stringType, + 'duration-2': stringType, + 'duration-3': stringType, + 'duration-4': stringType, + 'nicely-done': stringType, + 'credit-card': stringType, + 'credit-card-2': stringType, + paypal: stringType, + 'need-email': stringType, + 'went-wrong': stringType, + 'valid-info': stringType, + 'valid-email': stringType, + 'valid-card': stringType, + 'email-receipt': stringType, + 'need-help': stringType, + 'forward-receipt': stringType, + efficiency: stringType, + 'why-donate-1': stringType, + 'why-donate-2': stringType, + 'failed-pay': stringType, + 'try-again': stringType, + 'card-number': stringType, + expiration: stringType, + 'only-you': stringType + }), + report: strictObject({ + 'sign-in': stringType, + details: stringType, + portfolio: stringType, + 'portfolio-2': stringType, + 'notify-1': stringType, + 'notify-2': stringType, + what: stringType, + submit: stringType + }), + '404': strictObject({ + 'page-not-found': stringType, + 'not-found': stringType, + 'heres-a-quote': stringType + }), + search: strictObject({ + label: stringType, + placeholder: stringType, + 'see-results': stringType, + 'no-tutorials': stringType, + try: stringType, + 'no-results': stringType + }), + misc: strictObject({ + offline: stringType, + unsubscribed: stringType, + 'keep-coding': stringType, + 'email-signup': stringType, + quincy: stringType, + 'email-blast': stringType, + 'update-email-1': stringType, + 'update-email-2': stringType, + email: stringType, + and: stringType + }), + icons: strictObject({ + 'gold-cup': stringType, + avatar: stringType, + 'avatar-2': stringType, + donate: stringType, + fail: stringType, + 'not-passed': stringType, + passed: stringType, + heart: stringType, + initial: stringType, + info: stringType, + spacer: stringType, + toggle: stringType + }), + aria: strictObject({ + 'fcc-logo': stringType, + answer: stringType, + linkedin: stringType, + github: stringType, + website: stringType, + twitter: stringType, + 'first-page': stringType, + 'previous-page': stringType, + 'next-page': stringType, + 'last-page': stringType + }), + flash: strictObject({ + 'msg-1': stringType, + 'msg-2': stringType, + 'msg-3': stringType, + 'msg-4': stringType, + 'msg-5': stringType, + 'msg-6': stringType, + 'msg-7': stringType, + 'msg-8': stringType, + 'msg-9': stringType, + 'msg-10': stringType, + 'msg-11': stringType, + 'msg-12': stringType, + 'msg-13': stringType, + 'msg-14': stringType, + 'msg-15': stringType, + 'msg-16': stringType, + 'msg-17': stringType, + 'msg-18': stringType, + 'msg-19': stringType, + 'msg-20': stringType, + 'msg-21': stringType, + 'msg-22': stringType, + 'msg-23': stringType, + 'msg-24': stringType, + 'msg-25': stringType, + 'msg-26': stringType, + 'msg-27': stringType, + 'msg-28': stringType, + 'msg-29': stringType, + 'msg-30': stringType, + 'msg-31': stringType, + 'msg-32': stringType, + 'msg-33': stringType, + 'msg-34': stringType, + 'msg-35': stringType, + 'msg-36': stringType, + 'msg-37': stringType, + 'msg-38': stringType, + 'msg-39': stringType, + 'msg-40': stringType, + 'msg-41': stringType, + 'msg-42': stringType, + 'msg-43': stringType, + 'msg-44': stringType, + 'msg-45': stringType + }), + validation: strictObject({ + 'msg-1': stringType, + 'msg-2': stringType, + 'msg-3': stringType, + 'msg-4': stringType, + 'msg-5': stringType, + 'msg-6': stringType, + 'msg-7': stringType, + 'msg-8': stringType, + 'msg-9': stringType, + 'msg-10': stringType, + 'msg-11': stringType + }) +}); + +exports.translationsSchema = translationsSchema; diff --git a/client/jest.config.js b/client/jest.config.js index 058b2e4df2..4254cc1e5b 100644 --- a/client/jest.config.js +++ b/client/jest.config.js @@ -6,7 +6,8 @@ module.exports = { '^(?!.*\\.module\\.css$).*\\.css$': '/src/__mocks__/styleMock.js', // CSS Modules - match files that end with 'module.css' '\\.module\\.css$': 'identity-obj-proxy', - analytics: '/src/__mocks__/analyticsMock.js' + analytics: '/src/__mocks__/analyticsMock.js', + 'react-i18next': '/src/__mocks__/react-i18nextMock.js' }, testPathIgnorePatterns: ['/node_modules/', '/.cache/'], globals: { diff --git a/client/package-lock.json b/client/package-lock.json index 8f5fe1eaf9..01138f087c 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -11879,20 +11879,6 @@ } } }, - "gatsby-core-utils": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/gatsby-core-utils/-/gatsby-core-utils-1.8.0.tgz", - "integrity": "sha512-MurWnytVVG9rOai0oAdcCsLODqj7P7Y9ndoAswHDk6hrlsWwiRMOsDS1kEyL7n2BM7lhgzZ+gz9OaOukqU1BhA==", - "requires": { - "ci-info": "2.0.0", - "configstore": "^5.0.1", - "fs-extra": "^8.1.0", - "node-object-hash": "^2.0.0", - "proper-lockfile": "^4.1.1", - "tmp": "^0.2.1", - "xdg-basedir": "^4.0.0" - } - }, "gatsby-graphiql-explorer": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/gatsby-graphiql-explorer/-/gatsby-graphiql-explorer-0.10.0.tgz", @@ -12360,14 +12346,6 @@ } } }, - "gatsby-plugin-utils": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/gatsby-plugin-utils/-/gatsby-plugin-utils-0.7.0.tgz", - "integrity": "sha512-fClolFlWQvczukRQhLfdFtz9GXIehgf567HJbggC2oPkVT0NCwwNPAAjVq1CcmxQE8k/kcp7kxQVc86pVcWuzA==", - "requires": { - "joi": "^17.2.1" - } - }, "gatsby-react-router-scroll": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/gatsby-react-router-scroll/-/gatsby-react-router-scroll-3.6.0.tgz", @@ -14106,6 +14084,14 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "html-parse-stringify2": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify2/-/html-parse-stringify2-2.0.1.tgz", + "integrity": "sha1-3FZwtyksoVi3vJFsmmc1rIhyg0o=", + "requires": { + "void-elements": "^2.0.1" + } + }, "html-void-elements": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-1.0.5.tgz", @@ -14205,6 +14191,24 @@ "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.3.tgz", "integrity": "sha512-EcuixamT82oplpoJ2XU4pDtKGWQ7b00CD9f1ug9IaQ3p1bkHMiKCZ9ut9QDI6qsa6cpUuB+A/I+zLtdNK4n2DQ==" }, + "i18next": { + "version": "19.8.4", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-19.8.4.tgz", + "integrity": "sha512-FfVPNWv+felJObeZ6DSXZkj9QM1Ivvh7NcFCgA8XPtJWHz0iXVa9BUy+QY8EPrCLE+vWgDfV/sc96BgXVo6HAA==", + "requires": { + "@babel/runtime": "^7.12.0" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz", + "integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==", + "requires": { + "regenerator-runtime": "^0.13.4" + } + } + } + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -16355,6 +16359,197 @@ } } }, + "jest-json-schema": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/jest-json-schema/-/jest-json-schema-2.1.0.tgz", + "integrity": "sha512-FaXuFj6Rak1OnV+cfQsD8YnfyfEJ/DeGbomRnmNRO0HeyCpqLsDkC0Lr6z0hXK4/d7Ekz1mqBewVJpcYQ6H89w==", + "dev": true, + "requires": { + "ajv": "^6.10.2", + "chalk": "^2.4.1", + "jest-matcher-utils": "^24.0.0" + }, + "dependencies": { + "@jest/types": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz", + "integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^13.0.0" + } + }, + "@types/yargs": { + "version": "13.0.11", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.11.tgz", + "integrity": "sha512-NRqD6T4gktUrDi1o1wLH3EKC1o2caCr7/wR87ODcbVITQF106OM3sFN92ysZ++wqelOd1CTzatnOBRDYYG6wGQ==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "diff-sequences": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-24.9.0.tgz", + "integrity": "sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew==", + "dev": true + }, + "jest-diff": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-24.9.0.tgz", + "integrity": "sha512-qMfrTs8AdJE2iqrTp0hzh7kTd2PQWrsFyj9tORoKmu32xjPjeE4NyjVRDz8ybYwqS2ik8N4hsIpiVTyFeo2lBQ==", + "dev": true, + "requires": { + "chalk": "^2.0.1", + "diff-sequences": "^24.9.0", + "jest-get-type": "^24.9.0", + "pretty-format": "^24.9.0" + } + }, + "jest-get-type": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-24.9.0.tgz", + "integrity": "sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q==", + "dev": true + }, + "jest-matcher-utils": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-24.9.0.tgz", + "integrity": "sha512-OZz2IXsu6eaiMAwe67c1T+5tUAtQyQx27/EMEkbFAGiw52tB9em+uGbzpcgYVpA8wl0hlxKPZxrly4CXU/GjHA==", + "dev": true, + "requires": { + "chalk": "^2.0.1", + "jest-diff": "^24.9.0", + "jest-get-type": "^24.9.0", + "pretty-format": "^24.9.0" + } + }, + "pretty-format": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-24.9.0.tgz", + "integrity": "sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA==", + "dev": true, + "requires": { + "@jest/types": "^24.9.0", + "ansi-regex": "^4.0.0", + "ansi-styles": "^3.2.0", + "react-is": "^16.8.4" + } + } + } + }, + "jest-json-schema-extended": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/jest-json-schema-extended/-/jest-json-schema-extended-0.3.0.tgz", + "integrity": "sha512-T3aRO6rRq6rykVkyVl624QO3nzrMjTe5y5KTkTKjxh8iwoZ2yzUKAEL6ITs6i1XyunUorlTDIpP72gvOTPYK7g==", + "dev": true, + "requires": { + "@types/node": "^12.0.8", + "@typescript-eslint/eslint-plugin": "^1.10.2", + "@typescript-eslint/parser": "^1.10.2", + "jest-json-schema": "^2.0.2", + "typescript": "^3.5.1" + }, + "dependencies": { + "@types/node": { + "version": "12.19.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.19.8.tgz", + "integrity": "sha512-D4k2kNi0URNBxIRCb1khTnkWNHv8KSL1owPmS/K5e5t8B2GzMReY7AsJIY1BnP5KdlgC4rj9jk2IkDMasIE7xg==", + "dev": true + }, + "@typescript-eslint/eslint-plugin": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-1.13.0.tgz", + "integrity": "sha512-WQHCozMnuNADiqMtsNzp96FNox5sOVpU8Xt4meaT4em8lOG1SrOv92/mUbEHQVh90sldKSfcOc/I0FOb/14G1g==", + "dev": true, + "requires": { + "@typescript-eslint/experimental-utils": "1.13.0", + "eslint-utils": "^1.3.1", + "functional-red-black-tree": "^1.0.1", + "regexpp": "^2.0.1", + "tsutils": "^3.7.0" + } + }, + "@typescript-eslint/experimental-utils": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-1.13.0.tgz", + "integrity": "sha512-zmpS6SyqG4ZF64ffaJ6uah6tWWWgZ8m+c54XXgwFtUv0jNz8aJAVx8chMCvnk7yl6xwn8d+d96+tWp7fXzTuDg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/typescript-estree": "1.13.0", + "eslint-scope": "^4.0.0" + } + }, + "@typescript-eslint/parser": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-1.13.0.tgz", + "integrity": "sha512-ITMBs52PCPgLb2nGPoeT4iU3HdQZHcPaZVw+7CsFagRJHUhyeTgorEwHXhFf3e7Evzi8oujKNpHc8TONth8AdQ==", + "dev": true, + "requires": { + "@types/eslint-visitor-keys": "^1.0.0", + "@typescript-eslint/experimental-utils": "1.13.0", + "@typescript-eslint/typescript-estree": "1.13.0", + "eslint-visitor-keys": "^1.0.0" + } + }, + "@typescript-eslint/typescript-estree": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-1.13.0.tgz", + "integrity": "sha512-b5rCmd2e6DCC6tCTN9GSUAuxdYwCM/k/2wdjHGrIRGPSJotWMCe/dGpi66u42bhuh8q3QBzqM4TMA1GUUCJvdw==", + "dev": true, + "requires": { + "lodash.unescape": "4.0.1", + "semver": "5.5.0" + } + }, + "eslint-scope": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", + "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true + }, + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "dev": true + }, + "typescript": { + "version": "3.9.7", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz", + "integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==", + "dev": true + } + } + }, "jest-leak-detector": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-26.6.2.tgz", @@ -18469,6 +18664,12 @@ "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", "integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ=" }, + "lodash.unescape": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.unescape/-/lodash.unescape-4.0.1.tgz", + "integrity": "sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw=", + "dev": true + }, "lodash.uniq": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", @@ -21702,6 +21903,15 @@ "prop-types": "^15.6.1" } }, + "react-i18next": { + "version": "11.8.5", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.8.5.tgz", + "integrity": "sha512-2jY/8NkhNv2KWBnZuhHxTn13aMxAbvhiDUNskm+1xVVnrPId78l8fA7fCyVeO3XU1kptM0t4MtvxV1Nu08cjLw==", + "requires": { + "@babel/runtime": "^7.3.1", + "html-parse-stringify2": "2.0.1" + } + }, "react-instantsearch-core": { "version": "6.8.3", "resolved": "https://registry.npmjs.org/react-instantsearch-core/-/react-instantsearch-core-6.8.3.tgz", @@ -25822,6 +26032,11 @@ "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==" }, + "void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=" + }, "w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", diff --git a/client/package.json b/client/package.json index 665cae77fd..580d886f48 100644 --- a/client/package.json +++ b/client/package.json @@ -41,6 +41,7 @@ "gatsby-remark-prismjs": "^3.12.0", "gatsby-source-filesystem": "^2.10.0", "gatsby-transformer-remark": "^2.15.0", + "i18next": "^19.8.4", "jquery": "^3.5.1", "lodash": "^4.17.20", "monaco-editor": "^0.20.0", @@ -53,6 +54,7 @@ "react-ga": "^2.7.0", "react-helmet": "^6.1.0", "react-hotkeys": "^2.0.0", + "react-i18next": "^11.8.5", "react-instantsearch-dom": "^6.8.3", "react-lazy-load": "^3.1.13", "react-monaco-editor": "^0.39.1", @@ -106,6 +108,7 @@ "chokidar": "^3.5.1", "copy-webpack-plugin": "^5.1.2", "jest": "^26.6.3", + "jest-json-schema-extended": "^0.3.0", "monaco-editor-webpack-plugin": "^1.9.0", "react-test-renderer": "^16.14.0", "redux-saga-test-plan": "^4.0.1", diff --git a/client/src/__mocks__/react-i18nextMock.js b/client/src/__mocks__/react-i18nextMock.js new file mode 100644 index 0000000000..a9e8b10438 --- /dev/null +++ b/client/src/__mocks__/react-i18nextMock.js @@ -0,0 +1,69 @@ +/* eslint-disable react/prop-types */ +/* eslint-disable react/display-name */ +/* global jest */ +import React from 'react'; + +const reactI18next = jest.genMockFromModule('react-i18next'); + +// modified from https://github.com/i18next/react-i18next/blob/master/example/test-jest/src/__mocks__/react-i18next.js +const hasChildren = node => + node && (node.children || (node.props && node.props.children)); + +const getChildren = node => + node && node.children ? node.children : node.props && node.props.children; + +const renderNodes = reactNodes => { + if (typeof reactNodes === 'string') { + return reactNodes; + } + + return Object.keys(reactNodes).map((key, i) => { + const child = reactNodes[key]; + const isElement = React.isValidElement(child); + + if (typeof child === 'string') { + return child; + } + if (hasChildren(child)) { + const inner = renderNodes(getChildren(child)); + return React.cloneElement(child, { ...child.props, key: i }, inner); + } + if (typeof child === 'object' && !isElement) { + return Object.keys(child).reduce( + (str, childKey) => `${str}${child[childKey]}`, + '' + ); + } + + return child; + }); +}; + +const withTranslation = () => Component => { + Component.defaultProps = { ...Component.defaultProps, t: str => str }; + return Component; +}; + +const useTranslation = () => { + return { + t: str => str, + i18n: { + changeLanguage: () => new Promise(() => {}) + } + }; +}; + +const Trans = ({ children }) => + Array.isArray(children) ? renderNodes(children) : renderNodes([children]); + +// translate isn't being used anywhere, uncomment if needed +/* const translate = () => Component => props => ( + ''} {...props} /> +); */ + +// reactI18next.translate = translate; +reactI18next.withTranslation = withTranslation; +reactI18next.useTranslation = useTranslation; +reactI18next.Trans = Trans; + +module.exports = reactI18next; diff --git a/client/src/assets/icons/Cup.js b/client/src/assets/icons/Cup.js index 8e0914ac56..05b72a63aa 100644 --- a/client/src/assets/icons/Cup.js +++ b/client/src/assets/icons/Cup.js @@ -1,12 +1,15 @@ /* eslint-disable max-len */ import React, { Fragment } from 'react'; +import { useTranslation } from 'react-i18next'; const propTypes = {}; function Cup(props) { + const { t } = useTranslation(); + return ( - Gold Cup + {t('icons.gold-cup')} - Gold Cup + {t('icons.gold-cup')} diff --git a/client/src/assets/icons/DefaultAvatar.js b/client/src/assets/icons/DefaultAvatar.js index 4a59a8cf44..c676992997 100644 --- a/client/src/assets/icons/DefaultAvatar.js +++ b/client/src/assets/icons/DefaultAvatar.js @@ -1,7 +1,10 @@ /* eslint-disable max-len */ import React from 'react'; +import { useTranslation } from 'react-i18next'; function DefaultAvatar(props) { + const { t } = useTranslation(); + return ( - default avatar - an avatar conding with a laptop + {t('icons.avatar')} + {t('icons.avatar-2')} diff --git a/client/src/assets/icons/DonateWithPayPal.js b/client/src/assets/icons/DonateWithPayPal.js index 657550c7ae..936aaed203 100644 --- a/client/src/assets/icons/DonateWithPayPal.js +++ b/client/src/assets/icons/DonateWithPayPal.js @@ -1,12 +1,15 @@ /* eslint-disable max-len */ import React, { Fragment } from 'react'; +import { useTranslation } from 'react-i18next'; const propTypes = {}; function DonateWithPayPal(props) { + const { t } = useTranslation(); + return ( - Donate with PayPal + {t('icons.donate')} - Donate with PayPal + {t('icons.donate')} diff --git a/client/src/assets/icons/Fail.js b/client/src/assets/icons/Fail.js index 8574387899..6ee7ba7f61 100644 --- a/client/src/assets/icons/Fail.js +++ b/client/src/assets/icons/Fail.js @@ -1,6 +1,9 @@ import React from 'react'; +import { useTranslation } from 'react-i18next'; function RedFail() { + const { t } = useTranslation(); + return ( - Test failed + {t('icons.fail')} - Not Passed + {t('icons.not-passed')} - Not Passed + {t('icons.not-passed')} - Passed + {t('icons.passed')} - Passed + {t('icons.passed')} - Heart + {t('icons.heart')} - Initial + {t('icons.initial')} - IntroInformation + {t('icons.info')} - IntroInformation + {t('icons.info')} - Passed + {t('icons.spacer')} - Spacer + {t('icons.spacer')} diff --git a/client/src/assets/icons/ToggleCheck.js b/client/src/assets/icons/ToggleCheck.js index fa84a4ad42..07165d29c7 100644 --- a/client/src/assets/icons/ToggleCheck.js +++ b/client/src/assets/icons/ToggleCheck.js @@ -1,9 +1,12 @@ import React, { Fragment } from 'react'; +import { useTranslation } from 'react-i18next'; function ToggleCheck(props) { + const { t } = useTranslation(); + return ( - Passed + {t('icons.toggle')} - Passed + {t('icons.toggle')} bindActionCreators({ createFlashMessage, showCert, executeGA }, dispatch); const ShowCertification = props => { + const { t } = useTranslation(); const [isDonationSubmitted, setIsDonationSubmitted] = useState(false); const [isDonationDisplayed, setIsDonationDisplayed] = useState(false); const [isDonationClosed, setIsDonationClosed] = useState(false); @@ -198,7 +200,7 @@ const ShowCertification = props => { bsStyle='primary' onClick={hideDonationSection} > - Close + {t('buttons.close')} ); @@ -208,13 +210,7 @@ const ShowCertification = props => { {!isDonationSubmitted && ( -

- Only you can see this message. Congratulations on earning this - certification. It’s no easy task. Running freeCodeCamp isn’t easy - either. Nor is it cheap. Help us help you and many other people - around the world. Make a tax-deductible supporting donation to our - nonprofit today. -

+

{t('donate.only-you')}

)} @@ -241,7 +237,7 @@ const ShowCertification = props => { target='_blank' href={`https://www.linkedin.com/profile/add?startTask=CERTIFICATION_NAME&name=${certTitle}&organizationId=4831032&issueYear=${certYear}&issueMonth=${certMonth}&certUrl=${certURL}`} > - Add this certification to my LinkedIn profile + {t('profile.add-linkedin')} ); diff --git a/client/src/client-only-routes/ShowSettings.js b/client/src/client-only-routes/ShowSettings.js index dc23eb839b..f4240c7ce5 100644 --- a/client/src/client-only-routes/ShowSettings.js +++ b/client/src/client-only-routes/ShowSettings.js @@ -14,6 +14,7 @@ import { } from '../redux'; import { submitNewAbout, updateUserFlag, verifyCert } from '../redux/settings'; import { createFlashMessage } from '../components/Flash/redux'; +import { useTranslation } from 'react-i18next'; import { FullWidthRow, Loader, Spacer } from '../components/helpers'; import About from '../components/settings/About'; @@ -114,6 +115,7 @@ const mapDispatchToProps = { }; export function ShowSettings(props) { + const { t } = useTranslation(); const { createFlashMessage, isSignedIn, @@ -173,7 +175,7 @@ export function ShowSettings(props) { return ( - +
@@ -185,12 +187,12 @@ export function ShowSettings(props) { className='btn-invert' href={`${apiLocation}/signout`} > - Sign me out of freeCodeCamp + {t('buttons.sign-me-out')}

- {`Account Settings for ${username}`} + {t('settings.for', { username: username })}

', () => { expect(result.type.toString()).toBe('Symbol(react.fragment)'); // Renders Helmet component rather than Loader expect(result.props.children[0].props.title).toEqual( - 'Settings | freeCodeCamp.org' + 'buttons.settings | freeCodeCamp.org' ); }); diff --git a/client/src/client-only-routes/ShowUnsubscribed.js b/client/src/client-only-routes/ShowUnsubscribed.js index 37cd34d62b..17b0d29d53 100644 --- a/client/src/client-only-routes/ShowUnsubscribed.js +++ b/client/src/client-only-routes/ShowUnsubscribed.js @@ -2,6 +2,7 @@ import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; import { Grid, Panel, Button } from '@freecodecamp/react-bootstrap'; import Helmet from 'react-helmet'; +import { useTranslation } from 'react-i18next'; import env from '../../config/env.json'; import FullWidthRow from '../components/helpers/FullWidthRow'; @@ -10,10 +11,11 @@ import { Spacer } from '../components/helpers'; const { apiLocation } = env; function ShowUnsubscribed({ unsubscribeId }) { + const { t } = useTranslation(); return ( - You have been unsubscribed | freeCodeCamp.org + {t('meta.youre-unsubscribed')} | freeCodeCamp.org
@@ -21,8 +23,8 @@ function ShowUnsubscribed({ unsubscribeId }) { -

You have successfully been unsubscribed

-

Whatever you go on to, keep coding!

+

{t('misc.unsubscribed')}

+

{t('misc.keep-coding')}

{unsubscribeId ? ( @@ -33,7 +35,7 @@ function ShowUnsubscribed({ unsubscribeId }) { bsStyle='primary' href={`${apiLocation}/resubscribe/${unsubscribeId}`} > - You can click here to resubscribe + {t('buttons.resubscribe')} ) : null} diff --git a/client/src/client-only-routes/ShowUser.js b/client/src/client-only-routes/ShowUser.js index c4baa400ba..225f95e748 100644 --- a/client/src/client-only-routes/ShowUser.js +++ b/client/src/client-only-routes/ShowUser.js @@ -12,6 +12,7 @@ import { Row } from '@freecodecamp/react-bootstrap'; import Helmet from 'react-helmet'; +import { Trans, withTranslation } from 'react-i18next'; import Login from '../components/Header/components/Login'; @@ -27,6 +28,7 @@ const propTypes = { email: PropTypes.string, isSignedIn: PropTypes.bool, reportUser: PropTypes.func.isRequired, + t: PropTypes.func.isRequired, userFetchState: PropTypes.shape({ pending: PropTypes.bool, complete: PropTypes.bool, @@ -76,7 +78,7 @@ class ShowUser extends Component { } render() { - const { username, isSignedIn, userFetchState, email } = this.props; + const { username, isSignedIn, userFetchState, email, t } = this.props; const { pending, complete, errored } = userFetchState; if (pending && !complete) { return ; @@ -90,13 +92,13 @@ class ShowUser extends Component { - You need to be signed in to report a user + {t('report.sign-in')} - Click here to sign in + {t('buttons.click-here')} @@ -107,31 +109,29 @@ class ShowUser extends Component { } const { textarea } = this.state; - const placeholderText = `Please provide as much detail as possible about the account or behavior you are reporting.`; + const placeholderText = t('report.details'); return ( - Report a users portfolio | freeCodeCamp.org + {t('report.portfolio')} | freeCodeCamp.org -

- Do you want to report {username} - 's portfolio for abuse? -

+

{t('report.portfolio-2', { username: username })}

- We will notify the community moderators' team, and a send copy of - this report to your email: {email} + + {{ email }} +

-

We may get back to you for more information, if required.

+

{t('report.notify-2')}

- What would you like to report? + {t('report.what')} @@ -154,7 +154,9 @@ class ShowUser extends Component { ShowUser.displayName = 'ShowUser'; ShowUser.propTypes = propTypes; -export default connect( - mapStateToProps, - mapDispatchToProps -)(ShowUser); +export default withTranslation()( + connect( + mapStateToProps, + mapDispatchToProps + )(ShowUser) +); diff --git a/client/src/components/Donation/DonateCompletion.js b/client/src/components/Donation/DonateCompletion.js index 3e2635811f..148a22d1c3 100644 --- a/client/src/components/Donation/DonateCompletion.js +++ b/client/src/components/Donation/DonateCompletion.js @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Alert, Button } from '@freecodecamp/react-bootstrap'; import Spinner from 'react-spinkit'; +import { useTranslation } from 'react-i18next'; import './Donation.css'; @@ -14,12 +15,13 @@ const propTypes = { function DonateCompletion({ processing, reset, success, error = null }) { /* eslint-disable no-nested-ternary */ + const { t } = useTranslation(); const style = processing ? 'info' : success ? 'success' : 'danger'; const heading = processing - ? 'We are processing your donation.' + ? `${t('donate.processing')}` : success - ? 'Thank you for being a supporter.' - : 'Something went wrong with your donation.'; + ? `${t('donate.thank-you')}` + : `${t('donate.error')}`; return (

@@ -36,10 +38,7 @@ function DonateCompletion({ processing, reset, success, error = null }) { )} {success && (
-

- Your donations will support free technology education for people - all over the world. -

+

{t('donate.free-tech')}

)} {error &&

{error}

} @@ -48,7 +47,7 @@ function DonateCompletion({ processing, reset, success, error = null }) { {error && (
)} diff --git a/client/src/components/Donation/DonateForm.js b/client/src/components/Donation/DonateForm.js index 44850f4842..4f199fa977 100644 --- a/client/src/components/Donation/DonateForm.js +++ b/client/src/components/Donation/DonateForm.js @@ -1,3 +1,4 @@ +/* eslint-disable no-nested-ternary */ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; @@ -12,6 +13,8 @@ import { ToggleButton, ToggleButtonGroup } from '@freecodecamp/react-bootstrap'; +import { withTranslation } from 'react-i18next'; + import { amountsConfig, durationsConfig, @@ -55,6 +58,7 @@ const propTypes = { navigate: PropTypes.func.isRequired, postChargeStripe: PropTypes.func.isRequired, showLoading: PropTypes.bool.isRequired, + t: PropTypes.func.isRequired, updateDonationFormState: PropTypes.func }; @@ -157,21 +161,26 @@ class DonateForm extends Component { return `${numToCommas((amount / 100) * 50)} hours`; } - getFormatedAmountLabel(amount) { - return `$${numToCommas(amount / 100)}`; + getFormattedAmountLabel(amount) { + return `${numToCommas(amount / 100)}`; } getDonationButtonLabel() { const { donationAmount, donationDuration } = this.state; - let donationBtnLabel = `Confirm your donation`; + const { t } = this.props; + const usd = this.getFormattedAmountLabel(donationAmount); + let donationBtnLabel = t('donate.confirm'); if (donationDuration === 'onetime') { - donationBtnLabel = `Confirm your one-time donation of ${this.getFormatedAmountLabel( - donationAmount - )}`; + donationBtnLabel = t('donate.confirm-2', { + usd: usd + }); } else { - donationBtnLabel = `Confirm your donation of ${this.getFormatedAmountLabel( - donationAmount - )} ${donationDuration === 'month' ? ' / month' : ' / year'}`; + donationBtnLabel = + donationDuration === 'month' + ? t('donate.confirm-3', { + usd: usd + }) + : t('donate.confirm-4', { usd: usd }); } return donationBtnLabel; } @@ -229,30 +238,35 @@ class DonateForm extends Component { key={`${this.durations[duration]}-donation-${amount}`} value={amount} > - {this.getFormatedAmountLabel(amount)} + {this.getFormattedAmountLabel(amount)} )); } renderDonationDescription() { const { donationAmount, donationDuration } = this.state; + const { t } = this.props; + const usd = this.getFormattedAmountLabel(donationAmount); + const hours = this.convertToTimeContributed(donationAmount); + return (

- {`Your `} - {this.getFormatedAmountLabel(donationAmount)} - {` donation will provide `} - {this.convertToTimeContributed(donationAmount)} - {` of learning to people around the world`} - {donationDuration === 'onetime' ? `.` : ` each ${donationDuration}.`} + {donationDuration === 'onetime' + ? t('donate.your-donation', { usd: usd, hours: hours }) + : donationDuration === 'month' + ? t('donate.your-donation-2', { usd: usd, hours: hours }) + : t('donate.your-donation-3', { usd: usd, hours: hours })}

); } renderDurationAmountOptions() { const { donationAmount, donationDuration, processing } = this.state; + const { t } = this.props; + return !processing ? (
-

Select gift frequency:

+

{t('donate.gift-frequency')}

-

Select gift amount:

+

{t('donate.gift-amount')}

{isOneTime ? ( - Confirm your one-time donation of ${donationAmount / 100}: + + {t('donate.confirm-1')} {donationAmount / 100}: + ) : ( - Confirm your donation of ${donationAmount / 100} /{' '} - {donationDuration}: + {t('donate.confirm-2')} {donationAmount / 100} / {donationDuration}: )} @@ -318,7 +333,7 @@ class DonateForm extends Component { id='confirm-donation-btn' onClick={e => this.handleStripeCheckoutRedirect(e, 'credit card')} > - Credit Card + {t('donate.credit-card')} - {this.getDonationButtonLabel()} with PayPal: + + {this.getDonationButtonLabel()} {t('donate.paypal')} + - Or donate with a credit card: + {t('donate.credit-card-2')} @@ -436,4 +454,4 @@ DonateForm.propTypes = propTypes; export default connect( mapStateToProps, mapDispatchToProps -)(DonateForm); +)(withTranslation()(DonateForm)); diff --git a/client/src/components/Donation/DonateFormChildViewForHOC.js b/client/src/components/Donation/DonateFormChildViewForHOC.js index f047e4eaa6..087d556d2e 100644 --- a/client/src/components/Donation/DonateFormChildViewForHOC.js +++ b/client/src/components/Donation/DonateFormChildViewForHOC.js @@ -16,6 +16,7 @@ import { injectStripe } from 'react-stripe-elements'; import StripeCardForm from './StripeCardForm'; import DonateCompletion from './DonateCompletion'; import { userSelector } from '../../redux'; +import { withTranslation } from 'react-i18next'; const propTypes = { defaultTheme: PropTypes.string, @@ -31,6 +32,7 @@ const propTypes = { stripe: PropTypes.shape({ createToken: PropTypes.func.isRequired }), + t: PropTypes.func.isRequired, theme: PropTypes.string }; @@ -94,20 +96,17 @@ class DonateFormChildViewForHOC extends Component { isSubmissionValid: null }); + const { t } = this.props; const email = this.getUserEmail(); if (!email || !isEmail(email)) { return this.props.onDonationStateChange({ - error: - 'We need a valid email address to which we can send your' + - ' donation tax receipt.' + error: t('donate.need-email') }); } return this.props.stripe.createToken({ email }).then(({ error, token }) => { if (error) { return this.props.onDonationStateChange({ - error: - 'Something went wrong processing your donation. Your card' + - ' has not been charged.' + error: t('donate.went-wrong') }); } return this.postDonation(token); @@ -147,35 +146,25 @@ class DonateFormChildViewForHOC extends Component { renderErrorMessage() { const { isEmailValid, isFormValid } = this.state; + const { t } = this.props; let message = ''; if (!isEmailValid && !isFormValid) - message = ( -

- Please enter valid email address, credit card number, and expiration - date. -

- ); - else if (!isEmailValid) - message =

Please enter a valid email address.

; - else - message = ( -

Please enter valid credit card number and expiration date.

- ); + message =

{t('donate.valid-info')}

; + else if (!isEmailValid) message =

{t('donate.valid-email')}

; + else message =

{t('donate.valid-card')}

; return {message}; } renderDonateForm() { const { isEmailValid, isSubmissionValid, email } = this.state; - const { getDonationButtonLabel, theme, defaultTheme } = this.props; + const { getDonationButtonLabel, theme, defaultTheme, t } = this.props; return (
{isSubmissionValid !== null ? this.renderErrorMessage() : ''}
- - Email (we'll send you a tax-deductible donation receipt): - + {t('donate.email-receipt')} { - if (donationDuration === 'onetime') return 'a one-time'; - else if (donationDuration === 'month') return 'a monthly'; - else if (donationDuration === 'year') return 'an annual'; - else return 'a'; + const getDonationText = () => { + const donationDuration = modalDefaultDonation.donationDuration; + switch (donationDuration) { + case 'onetime': + return {t('donate.duration')}; + case 'month': + return {t('donate.duration-2')}; + case 'year': + return {t('donate.duration-3')}; + default: + return {t('donate.duration-4')}; + } }; - const donationText = ( - - Become {durationToText(modalDefaultDonation.donationDuration)} supporter - of our nonprofit. - - ); const blockDonationText = (
@@ -114,9 +117,9 @@ function DonateModal({ {!closeLabel && ( - Nicely done. You just completed {blockNameify(block)}. + {t('donate.nicely-done', { block: blockNameify(block) })}
- {donationText} + {getDonationText()} )}
@@ -131,7 +134,7 @@ function DonateModal({ {!closeLabel && ( - {donationText} + {getDonationText()} )} @@ -155,7 +158,7 @@ function DonateModal({ onClick={closeDonationModal} tabIndex='0' > - {closeLabel ? 'Close' : 'Ask me later'} + {closeLabel ? t('buttons.close') : t('buttons.ask-later')} diff --git a/client/src/components/Donation/PaypalButton.js b/client/src/components/Donation/PaypalButton.js index a0c351606e..f329b51707 100644 --- a/client/src/components/Donation/PaypalButton.js +++ b/client/src/components/Donation/PaypalButton.js @@ -5,6 +5,8 @@ import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import PayPalButtonScriptLoader from './PayPalButtonScriptLoader'; +import { withTranslation } from 'react-i18next'; + import { paypalClientId, deploymentEnv } from '../../../config/env.json'; import { paypalConfigurator, @@ -55,6 +57,7 @@ export class PaypalButton extends Component { render() { const { duration, planId, amount } = this.state; + const { t } = this.props; const isSubscription = duration !== 'onetime'; if (!paypalClientId) { @@ -90,14 +93,14 @@ export class PaypalButton extends Component { this.props.onDonationStateChange({ processing: false, success: false, - error: `Uh - oh. It looks like your transaction didn't go through. Could you please try again?` + error: t('donate.failed-pay') }); }} onError={() => this.props.onDonationStateChange({ processing: false, success: false, - error: 'Please try again.' + error: t('donate.try-again') }) } plantId={planId} @@ -117,7 +120,8 @@ const propTypes = { handleProcessing: PropTypes.func, isDonating: PropTypes.bool, onDonationStateChange: PropTypes.func, - skipAddDonation: PropTypes.bool + skipAddDonation: PropTypes.bool, + t: PropTypes.func.isRequired }; const mapStateToProps = createSelector( @@ -132,4 +136,4 @@ const mapStateToProps = createSelector( PaypalButton.displayName = 'PaypalButton'; PaypalButton.propTypes = propTypes; -export default connect(mapStateToProps)(PaypalButton); +export default connect(mapStateToProps)(withTranslation()(PaypalButton)); diff --git a/client/src/components/Donation/StripeCardForm.js b/client/src/components/Donation/StripeCardForm.js index fb6e9fa556..9695893302 100644 --- a/client/src/components/Donation/StripeCardForm.js +++ b/client/src/components/Donation/StripeCardForm.js @@ -8,9 +8,11 @@ import { FormGroup, Image } from '@freecodecamp/react-bootstrap'; +import { withTranslation } from 'react-i18next'; const propTypes = { getValidationState: PropTypes.func.isRequired, + t: PropTypes.func.isRequired, theme: PropTypes.string }; @@ -70,12 +72,13 @@ class StripeCardForm extends Component { } render() { + const { t } = this.props; // set color based on theme style.base.color = this.props.theme === 'night' ? '#fff' : '#0a0a23'; return (
- Your Card Number: + {t('donate.card-number')} - Expiration Date: + {t('donate.expiration')} { @@ -33,7 +38,7 @@ function Flash({ flashMessage, onClose }) { className='flash-message' onDismiss={handleClose} > - {message} + {needsTranslating ? t(`${message}`) : message} @@ -53,7 +58,8 @@ Flash.propTypes = { flashMessage: PropTypes.shape({ id: PropTypes.string, type: PropTypes.string, - message: PropTypes.string + message: PropTypes.string, + needsTranslating: PropTypes.bool }), onClose: PropTypes.func.isRequired }; diff --git a/client/src/components/Footer/LanguageMenu.js b/client/src/components/Footer/LanguageMenu.js new file mode 100644 index 0000000000..e24b46fd1d --- /dev/null +++ b/client/src/components/Footer/LanguageMenu.js @@ -0,0 +1,48 @@ +/* eslint-disable jsx-a11y/no-onchange */ +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +const { + availableLangs, + i18nextCodes, + langDisplayNames +} = require('../../../i18n/allLangs'); +const { homeLocation } = require('../../../config/env'); + +const locales = availableLangs.client; + +const LanguageMenu = () => { + const { i18n, t } = useTranslation(); + const i18nLanguage = i18n.language; + + const currentLanguage = Object.keys(i18nextCodes).find( + key => i18nextCodes[key] === i18nLanguage + ); + + const changeLanguage = e => { + const path = window.location.pathname; + + if (e.target.value === 'espanol') { + window.location.replace(`${homeLocation}/espanol${path}`); + } else { + window.location.replace(`${homeLocation}${path}`); + } + }; + + return ( +
+ {t('footer.language')} + +
+ ); +}; + +export default LanguageMenu; diff --git a/client/src/components/Footer/__snapshots__/Footer.test.js.snap b/client/src/components/Footer/__snapshots__/Footer.test.js.snap index e2a9a29dc8..46921a85be 100644 --- a/client/src/components/Footer/__snapshots__/Footer.test.js.snap +++ b/client/src/components/Footer/__snapshots__/Footer.test.js.snap @@ -13,24 +13,46 @@ exports[`