feat: i18n user interface (#40306)
Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
@ -12,7 +12,7 @@ let curriculum;
|
||||
export async function getCurriculum() {
|
||||
curriculum = curriculum
|
||||
? curriculum
|
||||
: getChallengesForLang(process.env.LOCALE);
|
||||
: getChallengesForLang(process.env.CURRICULUM_LOCALE);
|
||||
return curriculum;
|
||||
}
|
||||
|
||||
|
@ -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 (
|
||||
<Provider store={store}>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<AppMountNotifier render={() => element} />
|
||||
</I18nextProvider>
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
@ -34,7 +34,7 @@ module.exports = {
|
||||
options: {
|
||||
name: 'challenges',
|
||||
source: buildChallenges,
|
||||
onSourceChange: replaceChallengeNode(config.locale),
|
||||
onSourceChange: replaceChallengeNode(config.curriculumLocale),
|
||||
curriculumPath: localeChallengesRootDir
|
||||
}
|
||||
},
|
||||
|
38
client/i18n/allLangs.js
Normal file
38
client/i18n/allLangs.js
Normal file
@ -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;
|
32
client/i18n/config.js
Normal file
32
client/i18n/config.js
Normal file
@ -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;
|
16
client/i18n/configForTests.js
Normal file
16
client/i18n/configForTests.js
Normal file
@ -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;
|
60
client/i18n/locales.test.js
Normal file
60
client/i18n/locales.test.js
Normal file
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
466
client/i18n/locales/english/translations.json
Normal file
466
client/i18n/locales/english/translations.json
Normal file
@ -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": "<strong>Shawn Wang</strong> in Singapore",
|
||||
"occupation": "Software Engineer at <strong>Amazon</strong>",
|
||||
"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. <strong>freeCodeCamp changed my life.</strong>\""
|
||||
},
|
||||
"sarah": {
|
||||
"location": "<strong>Sarah Chima</strong> in Nigeria",
|
||||
"occupation": "Software Engineer at <strong>ChatDesk</strong>",
|
||||
"testimony": "\"<strong>freeCodeCamp was the gateway to my career</strong> 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": "<strong>Emma Bostian</strong> in Sweden",
|
||||
"occupation": "Software Engineer at <strong>Spotify</strong>",
|
||||
"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 <strong>freeCodeCamp gave me the skills</strong> 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}}</0>",
|
||||
"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</0>.",
|
||||
"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}}</0> 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</0>.",
|
||||
"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</0>.",
|
||||
"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</0>.",
|
||||
"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</0> 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}}</0>",
|
||||
"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: <strong>{{email}}</strong>",
|
||||
"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}}</0>"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
29
client/i18n/locales/espanol/motivation.json
Normal file
29
client/i18n/locales/espanol/motivation.json
Normal file
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
444
client/i18n/locales/espanol/translations.json
Normal file
444
client/i18n/locales/espanol/translations.json
Normal file
@ -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": "<strong>Shawn Wang</strong> en Singapur",
|
||||
"occupation": "Ingeniero de software en <strong>Amazon</strong>",
|
||||
"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. <strong>freeCodeCamp cambió mi vida.</strong>\""
|
||||
},
|
||||
"sarah": {
|
||||
"location": "<strong>Sarah Chima</strong> en Nigeria",
|
||||
"occupation": "Ingeniera de software en <strong>ChatDesk</strong>",
|
||||
"testimony": "\"<strong>freeCodeCamp fue la puerta de entrada a mi carrera</strong> 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": "<strong>Emma Bostian</strong> en Suecia",
|
||||
"occupation": "Ingeniera de software en <strong>Spotify</strong>",
|
||||
"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 <strong>freeCodeCamp me dio las habilidades</strong> 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}}</0>",
|
||||
"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í</0>.",
|
||||
"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}} </0> 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</0>.",
|
||||
"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</0>.",
|
||||
"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</0>.",
|
||||
"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</0> 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}}</0>",
|
||||
"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}}</0>",
|
||||
"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}}</0>"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
22
client/i18n/motivation-schema.js
Normal file
22
client/i18n/motivation-schema.js
Normal file
@ -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;
|
456
client/i18n/translations-schema.js
Normal file
456
client/i18n/translations-schema.js
Normal file
@ -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;
|
@ -6,7 +6,8 @@ module.exports = {
|
||||
'^(?!.*\\.module\\.css$).*\\.css$': '<rootDir>/src/__mocks__/styleMock.js',
|
||||
// CSS Modules - match files that end with 'module.css'
|
||||
'\\.module\\.css$': 'identity-obj-proxy',
|
||||
analytics: '<rootDir>/src/__mocks__/analyticsMock.js'
|
||||
analytics: '<rootDir>/src/__mocks__/analyticsMock.js',
|
||||
'react-i18next': '<rootDir>/src/__mocks__/react-i18nextMock.js'
|
||||
},
|
||||
testPathIgnorePatterns: ['/node_modules/', '<rootDir>/.cache/'],
|
||||
globals: {
|
||||
|
259
client/package-lock.json
generated
259
client/package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
69
client/src/__mocks__/react-i18nextMock.js
vendored
Normal file
69
client/src/__mocks__/react-i18nextMock.js
vendored
Normal file
@ -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 => (
|
||||
<Component t={() => ''} {...props} />
|
||||
); */
|
||||
|
||||
// reactI18next.translate = translate;
|
||||
reactI18next.withTranslation = withTranslation;
|
||||
reactI18next.useTranslation = useTranslation;
|
||||
reactI18next.Trans = Trans;
|
||||
|
||||
module.exports = reactI18next;
|
@ -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 (
|
||||
<Fragment>
|
||||
<span className='sr-only'>Gold Cup</span>
|
||||
<span className='sr-only'>{t('icons.gold-cup')}</span>
|
||||
<svg
|
||||
height={200}
|
||||
version='1.1'
|
||||
@ -15,7 +18,7 @@ function Cup(props) {
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
{...props}
|
||||
>
|
||||
<title>Gold Cup</title>
|
||||
<title>{t('icons.gold-cup')}</title>
|
||||
<g fill='none' fillRule='evenodd'>
|
||||
<g transform='translate(-14)'>
|
||||
<g transform='translate(20)'>
|
||||
|
@ -1,7 +1,10 @@
|
||||
/* eslint-disable max-len */
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function DefaultAvatar(props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<svg
|
||||
className='default-avatar'
|
||||
@ -13,8 +16,8 @@ function DefaultAvatar(props) {
|
||||
xmlnsXlink='http://www.w3.org/1999/xlink'
|
||||
{...props}
|
||||
>
|
||||
<title>default avatar</title>
|
||||
<desc>an avatar conding with a laptop</desc>
|
||||
<title>{t('icons.avatar')}</title>
|
||||
<desc>{t('icons.avatar-2')}</desc>
|
||||
<g fill='none' fillRule='evenodd'>
|
||||
<g id='g'>
|
||||
<rect fill='#D0D0D5' height='500' width='500' />
|
||||
|
@ -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 (
|
||||
<Fragment>
|
||||
<span className='sr-only'>Donate with PayPal</span>
|
||||
<span className='sr-only'>{t('icons.donate')}</span>
|
||||
<svg
|
||||
height={31}
|
||||
version='1.1'
|
||||
@ -43,7 +46,7 @@ function DonateWithPayPal(props) {
|
||||
fontSize={25}
|
||||
>
|
||||
<tspan x='44.2924805' y={19}>
|
||||
Donate with PayPal
|
||||
{t('icons.donate')}
|
||||
</tspan>
|
||||
</text>
|
||||
</g>
|
||||
|
@ -1,6 +1,9 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function RedFail() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<svg
|
||||
height='50'
|
||||
@ -9,7 +12,7 @@ function RedFail() {
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<g>
|
||||
<title>Test failed</title>
|
||||
<title>{t('icons.fail')}</title>
|
||||
<circle
|
||||
cx='100'
|
||||
cy='99'
|
||||
|
@ -1,10 +1,13 @@
|
||||
/* eslint-disable max-len */
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function FreeCodeCampLogo() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
function freeCodeCampLogo() {
|
||||
return (
|
||||
<svg
|
||||
aria-label='[freeCodeCamp Logo]'
|
||||
aria-label={t('aria.fcc-logo')}
|
||||
height={24}
|
||||
role='math'
|
||||
version='1.1'
|
||||
@ -109,6 +112,6 @@ function freeCodeCampLogo() {
|
||||
);
|
||||
}
|
||||
|
||||
freeCodeCampLogo.displayName = 'freeCodeCampLogo';
|
||||
FreeCodeCampLogo.displayName = 'FreeCodeCampLogo';
|
||||
|
||||
export default freeCodeCampLogo;
|
||||
export default FreeCodeCampLogo;
|
@ -1,11 +1,14 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const propTypes = {};
|
||||
|
||||
function GreenNotCompleted(props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<span className='sr-only'>Not Passed</span>
|
||||
<span className='sr-only'>{t('icons.not-passed')}</span>
|
||||
<svg
|
||||
height='50'
|
||||
viewBox='0 0 200 200'
|
||||
@ -14,7 +17,7 @@ function GreenNotCompleted(props) {
|
||||
{...props}
|
||||
>
|
||||
<g>
|
||||
<title>Not Passed</title>
|
||||
<title>{t('icons.not-passed')}</title>
|
||||
<circle
|
||||
cx='100'
|
||||
cy='99'
|
||||
|
@ -1,9 +1,12 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function GreenPass(props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<span className='sr-only'>Passed</span>
|
||||
<span className='sr-only'>{t('icons.passed')}</span>
|
||||
<svg
|
||||
height='50'
|
||||
viewBox='0 0 200 200'
|
||||
@ -12,7 +15,7 @@ function GreenPass(props) {
|
||||
{...props}
|
||||
>
|
||||
<g>
|
||||
<title>Passed</title>
|
||||
<title>{t('icons.passed')}</title>
|
||||
<circle
|
||||
cx='100'
|
||||
cy='99'
|
||||
|
@ -1,11 +1,14 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const propTypes = {};
|
||||
|
||||
function Heart(props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<span className='sr-only'>Heart</span>
|
||||
<span className='sr-only'>{t('icons.heart')}</span>
|
||||
<svg
|
||||
height={184}
|
||||
version='1.1'
|
||||
|
@ -1,6 +1,9 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function Initial(props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<svg
|
||||
height='50'
|
||||
@ -10,7 +13,7 @@ function Initial(props) {
|
||||
{...props}
|
||||
>
|
||||
<g>
|
||||
<title>Initial</title>
|
||||
<title>{t('icons.initial')}</title>
|
||||
<circle
|
||||
cx='100'
|
||||
cy='99'
|
||||
|
@ -1,11 +1,14 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const propTypes = {};
|
||||
|
||||
function IntroInformation(props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<span className='sr-only'>IntroInformation</span>
|
||||
<span className='sr-only'>{t('icons.info')}</span>
|
||||
<svg
|
||||
height='50'
|
||||
viewBox='0 0 200 200'
|
||||
@ -14,7 +17,7 @@ function IntroInformation(props) {
|
||||
{...props}
|
||||
>
|
||||
<g>
|
||||
<title>IntroInformation</title>
|
||||
<title>{t('icons.info')}</title>
|
||||
<circle
|
||||
cx='100'
|
||||
cy='99'
|
||||
|
@ -1,9 +1,12 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function Spacer(props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<span className='sr-only'>Passed</span>
|
||||
<span className='sr-only'>{t('icons.spacer')}</span>
|
||||
<svg
|
||||
className='tick'
|
||||
height='50'
|
||||
@ -13,7 +16,7 @@ function Spacer(props) {
|
||||
{...props}
|
||||
>
|
||||
<g>
|
||||
<title>Spacer</title>
|
||||
<title>{t('icons.spacer')}</title>
|
||||
<rect fillOpacity='0' height='200' paddingtop='5' width='200' />
|
||||
</g>
|
||||
</svg>
|
||||
|
@ -1,9 +1,12 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function ToggleCheck(props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<span className='sr-only'>Passed</span>
|
||||
<span className='sr-only'>{t('icons.toggle')}</span>
|
||||
<svg
|
||||
className='tick'
|
||||
height='50'
|
||||
@ -13,7 +16,7 @@ function ToggleCheck(props) {
|
||||
{...props}
|
||||
>
|
||||
<g>
|
||||
<title>Passed</title>
|
||||
<title>{t('icons.toggle')}</title>
|
||||
<rect
|
||||
fill='white'
|
||||
height='60'
|
||||
|
@ -6,9 +6,10 @@ import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import format from 'date-fns/format';
|
||||
import { Grid, Row, Col, Image, Button } from '@freecodecamp/react-bootstrap';
|
||||
import FreeCodeCampLogo from '../assets/icons/freeCodeCampLogo';
|
||||
import FreeCodeCampLogo from '../assets/icons/FreeCodeCampLogo';
|
||||
// eslint-disable-next-line max-len
|
||||
import DonateForm from '../components/Donation/DonateForm';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import {
|
||||
showCertSelector,
|
||||
@ -84,6 +85,7 @@ const mapDispatchToProps = dispatch =>
|
||||
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')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
@ -208,13 +210,7 @@ const ShowCertification = props => {
|
||||
{!isDonationSubmitted && (
|
||||
<Row>
|
||||
<Col lg={8} lgOffset={2} sm={10} smOffset={1} xs={12}>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>{t('donate.only-you')}</p>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
@ -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')}
|
||||
</Button>
|
||||
<Spacer />
|
||||
<Button
|
||||
@ -249,9 +245,12 @@ const ShowCertification = props => {
|
||||
bsSize='lg'
|
||||
bsStyle='primary'
|
||||
target='_blank'
|
||||
href={`https://twitter.com/intent/tweet?text=I just earned the ${certTitle} certification @freeCodeCamp! Check it out here: ${certURL}`}
|
||||
href={`https://twitter.com/intent/tweet?text=${t('profile.tweet', {
|
||||
certTitle: certTitle,
|
||||
certURL: certURL
|
||||
})}`}
|
||||
>
|
||||
Share this certification on Twitter
|
||||
{t('profile.add-twitter')}
|
||||
</Button>
|
||||
</Row>
|
||||
);
|
||||
|
@ -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 (
|
||||
<Fragment>
|
||||
<Helmet title='Settings | freeCodeCamp.org' />
|
||||
<Helmet title={`${t('buttons.settings')} | freeCodeCamp.org`} />
|
||||
<Grid>
|
||||
<main>
|
||||
<Spacer size={2} />
|
||||
@ -185,12 +187,12 @@ export function ShowSettings(props) {
|
||||
className='btn-invert'
|
||||
href={`${apiLocation}/signout`}
|
||||
>
|
||||
Sign me out of freeCodeCamp
|
||||
{t('buttons.sign-me-out')}
|
||||
</Button>
|
||||
</FullWidthRow>
|
||||
<Spacer />
|
||||
<h1 className='text-center' style={{ overflowWrap: 'break-word' }}>
|
||||
{`Account Settings for ${username}`}
|
||||
{t('settings.for', { username: username })}
|
||||
</h1>
|
||||
<About
|
||||
about={about}
|
||||
|
@ -14,7 +14,7 @@ describe('<ShowSettings />', () => {
|
||||
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'
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -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 (
|
||||
<Fragment>
|
||||
<Helmet>
|
||||
<title>You have been unsubscribed | freeCodeCamp.org</title>
|
||||
<title>{t('meta.youre-unsubscribed')} | freeCodeCamp.org</title>
|
||||
</Helmet>
|
||||
<Grid>
|
||||
<main>
|
||||
@ -21,8 +23,8 @@ function ShowUnsubscribed({ unsubscribeId }) {
|
||||
<Spacer size={2} />
|
||||
<Panel bsStyle='primary' className='text-center'>
|
||||
<Spacer />
|
||||
<h2>You have successfully been unsubscribed</h2>
|
||||
<p>Whatever you go on to, keep coding!</p>
|
||||
<h2>{t('misc.unsubscribed')}</h2>
|
||||
<p>{t('misc.keep-coding')}</p>
|
||||
</Panel>
|
||||
</FullWidthRow>
|
||||
{unsubscribeId ? (
|
||||
@ -33,7 +35,7 @@ function ShowUnsubscribed({ unsubscribeId }) {
|
||||
bsStyle='primary'
|
||||
href={`${apiLocation}/resubscribe/${unsubscribeId}`}
|
||||
>
|
||||
You can click here to resubscribe
|
||||
{t('buttons.resubscribe')}
|
||||
</Button>
|
||||
</FullWidthRow>
|
||||
) : null}
|
||||
|
@ -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 <Loader fullScreen={true} />;
|
||||
@ -90,13 +92,13 @@ class ShowUser extends Component {
|
||||
<Panel bsStyle='info' className='text-center'>
|
||||
<Panel.Heading>
|
||||
<Panel.Title componentClass='h3'>
|
||||
You need to be signed in to report a user
|
||||
{t('report.sign-in')}
|
||||
</Panel.Title>
|
||||
</Panel.Heading>
|
||||
<Panel.Body className='text-center'>
|
||||
<Spacer size={2} />
|
||||
<Col md={6} mdOffset={3} sm={8} smOffset={2} xs={12}>
|
||||
<Login block={true}>Click here to sign in</Login>
|
||||
<Login block={true}>{t('buttons.click-here')}</Login>
|
||||
</Col>
|
||||
<Spacer size={3} />
|
||||
</Panel.Body>
|
||||
@ -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 (
|
||||
<Fragment>
|
||||
<Helmet>
|
||||
<title>Report a users portfolio | freeCodeCamp.org</title>
|
||||
<title>{t('report.portfolio')} | freeCodeCamp.org</title>
|
||||
</Helmet>
|
||||
<Spacer size={2} />
|
||||
<Row className='text-center'>
|
||||
<Col sm={8} smOffset={2} xs={12}>
|
||||
<h2>
|
||||
Do you want to report {username}
|
||||
's portfolio for abuse?
|
||||
</h2>
|
||||
<h2>{t('report.portfolio-2', { username: username })}</h2>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col sm={6} smOffset={3} xs={12}>
|
||||
<p>
|
||||
We will notify the community moderators' team, and a send copy of
|
||||
this report to your email: <strong>{email}</strong>
|
||||
<Trans email={email} i18nKey='report.notify-1'>
|
||||
<strong>{{ email }}</strong>
|
||||
</Trans>
|
||||
</p>
|
||||
<p>We may get back to you for more information, if required.</p>
|
||||
<p>{t('report.notify-2')}</p>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<FormGroup controlId='report-user-textarea'>
|
||||
<ControlLabel>What would you like to report?</ControlLabel>
|
||||
<ControlLabel>{t('report.what')}</ControlLabel>
|
||||
<FormControl
|
||||
componentClass='textarea'
|
||||
onChange={this.handleChange}
|
||||
@ -140,7 +140,7 @@ class ShowUser extends Component {
|
||||
/>
|
||||
</FormGroup>
|
||||
<Button block={true} bsStyle='primary' type='submit'>
|
||||
Submit the report
|
||||
{t('report.submit')}
|
||||
</Button>
|
||||
<Spacer />
|
||||
</form>
|
||||
@ -154,7 +154,9 @@ class ShowUser extends Component {
|
||||
ShowUser.displayName = 'ShowUser';
|
||||
ShowUser.propTypes = propTypes;
|
||||
|
||||
export default connect(
|
||||
export default withTranslation()(
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ShowUser);
|
||||
)(ShowUser)
|
||||
);
|
||||
|
@ -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 (
|
||||
<Alert bsStyle={style} className='donation-completion'>
|
||||
<h4>
|
||||
@ -36,10 +38,7 @@ function DonateCompletion({ processing, reset, success, error = null }) {
|
||||
)}
|
||||
{success && (
|
||||
<div>
|
||||
<p>
|
||||
Your donations will support free technology education for people
|
||||
all over the world.
|
||||
</p>
|
||||
<p>{t('donate.free-tech')}</p>
|
||||
</div>
|
||||
)}
|
||||
{error && <p>{error}</p>}
|
||||
@ -48,7 +47,7 @@ function DonateCompletion({ processing, reset, success, error = null }) {
|
||||
{error && (
|
||||
<div>
|
||||
<Button bsStyle='primary' onClick={reset}>
|
||||
Try again
|
||||
{t('buttons.try-again')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
@ -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)}
|
||||
</ToggleButton>
|
||||
));
|
||||
}
|
||||
|
||||
renderDonationDescription() {
|
||||
const { donationAmount, donationDuration } = this.state;
|
||||
const { t } = this.props;
|
||||
const usd = this.getFormattedAmountLabel(donationAmount);
|
||||
const hours = this.convertToTimeContributed(donationAmount);
|
||||
|
||||
return (
|
||||
<p className='donation-description'>
|
||||
{`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 })}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
renderDurationAmountOptions() {
|
||||
const { donationAmount, donationDuration, processing } = this.state;
|
||||
const { t } = this.props;
|
||||
|
||||
return !processing ? (
|
||||
<div>
|
||||
<h3>Select gift frequency:</h3>
|
||||
<h3>{t('donate.gift-frequency')}</h3>
|
||||
<Tabs
|
||||
activeKey={donationDuration}
|
||||
animation={false}
|
||||
@ -268,7 +282,7 @@ class DonateForm extends Component {
|
||||
title={this.durations[duration]}
|
||||
>
|
||||
<Spacer />
|
||||
<h3>Select gift amount:</h3>
|
||||
<h3>{t('donate.gift-amount')}</h3>
|
||||
<div>
|
||||
<ToggleButtonGroup
|
||||
animation={`false`}
|
||||
@ -295,7 +309,7 @@ class DonateForm extends Component {
|
||||
}
|
||||
|
||||
renderDonationOptions() {
|
||||
const { handleProcessing, isSignedIn, addDonation } = this.props;
|
||||
const { handleProcessing, isSignedIn, addDonation, t } = this.props;
|
||||
const { donationAmount, donationDuration } = this.state;
|
||||
|
||||
const isOneTime = donationDuration === 'onetime';
|
||||
@ -303,11 +317,12 @@ class DonateForm extends Component {
|
||||
return (
|
||||
<div>
|
||||
{isOneTime ? (
|
||||
<b>Confirm your one-time donation of ${donationAmount / 100}:</b>
|
||||
<b>
|
||||
{t('donate.confirm-1')} {donationAmount / 100}:
|
||||
</b>
|
||||
) : (
|
||||
<b>
|
||||
Confirm your donation of ${donationAmount / 100} /{' '}
|
||||
{donationDuration}:
|
||||
{t('donate.confirm-2')} {donationAmount / 100} / {donationDuration}:
|
||||
</b>
|
||||
)}
|
||||
<Spacer />
|
||||
@ -318,7 +333,7 @@ class DonateForm extends Component {
|
||||
id='confirm-donation-btn'
|
||||
onClick={e => this.handleStripeCheckoutRedirect(e, 'credit card')}
|
||||
>
|
||||
<b>Credit Card</b>
|
||||
<b>{t('donate.credit-card')}</b>
|
||||
</Button>
|
||||
<PaypalButton
|
||||
addDonation={addDonation}
|
||||
@ -348,14 +363,17 @@ class DonateForm extends Component {
|
||||
handleProcessing,
|
||||
defaultTheme,
|
||||
addDonation,
|
||||
postChargeStripe
|
||||
postChargeStripe,
|
||||
t
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<Col lg={8} lgOffset={2} sm={10} smOffset={1} xs={12}>
|
||||
<Spacer />
|
||||
<b>{this.getDonationButtonLabel()} with PayPal:</b>
|
||||
<b>
|
||||
{this.getDonationButtonLabel()} {t('donate.paypal')}
|
||||
</b>
|
||||
<Spacer />
|
||||
<PaypalButton
|
||||
addDonation={addDonation}
|
||||
@ -367,7 +385,7 @@ class DonateForm extends Component {
|
||||
</Col>
|
||||
<Col lg={8} lgOffset={2} sm={10} smOffset={1} xs={12}>
|
||||
<Spacer />
|
||||
<b>Or donate with a credit card:</b>
|
||||
<b>{t('donate.credit-card-2')}</b>
|
||||
<Spacer />
|
||||
<StripeProvider stripe={stripe}>
|
||||
<Elements>
|
||||
@ -436,4 +454,4 @@ DonateForm.propTypes = propTypes;
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(DonateForm);
|
||||
)(withTranslation()(DonateForm));
|
||||
|
@ -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 = (
|
||||
<p>
|
||||
Please enter valid email address, credit card number, and expiration
|
||||
date.
|
||||
</p>
|
||||
);
|
||||
else if (!isEmailValid)
|
||||
message = <p>Please enter a valid email address.</p>;
|
||||
else
|
||||
message = (
|
||||
<p>Please enter valid credit card number and expiration date.</p>
|
||||
);
|
||||
message = <p>{t('donate.valid-info')}</p>;
|
||||
else if (!isEmailValid) message = <p>{t('donate.valid-email')}</p>;
|
||||
else message = <p>{t('donate.valid-card')}</p>;
|
||||
|
||||
return <Alert bsStyle='danger'>{message}</Alert>;
|
||||
}
|
||||
|
||||
renderDonateForm() {
|
||||
const { isEmailValid, isSubmissionValid, email } = this.state;
|
||||
const { getDonationButtonLabel, theme, defaultTheme } = this.props;
|
||||
const { getDonationButtonLabel, theme, defaultTheme, t } = this.props;
|
||||
|
||||
return (
|
||||
<Form className='donation-form' onSubmit={this.handleSubmit}>
|
||||
<div>{isSubmissionValid !== null ? this.renderErrorMessage() : ''}</div>
|
||||
<FormGroup className='donation-email-container'>
|
||||
<ControlLabel>
|
||||
Email (we'll send you a tax-deductible donation receipt):
|
||||
</ControlLabel>
|
||||
<ControlLabel>{t('donate.email-receipt')}</ControlLabel>
|
||||
<FormControl
|
||||
className={!isEmailValid && email ? 'email--invalid' : ''}
|
||||
key='3'
|
||||
@ -219,5 +208,5 @@ DonateFormChildViewForHOC.displayName = 'DonateFormChildViewForHOC';
|
||||
DonateFormChildViewForHOC.propTypes = propTypes;
|
||||
|
||||
export default injectStripe(
|
||||
connect(mapStateToProps)(DonateFormChildViewForHOC)
|
||||
connect(mapStateToProps)(withTranslation()(DonateFormChildViewForHOC))
|
||||
);
|
||||
|
@ -11,6 +11,7 @@ import Heart from '../../assets/icons/Heart';
|
||||
import Cup from '../../assets/icons/Cup';
|
||||
import DonateForm from './DonateForm';
|
||||
import { modalDefaultDonation } from '../../../../config/donation-settings';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import {
|
||||
closeDonationModal,
|
||||
@ -60,6 +61,7 @@ function DonateModal({
|
||||
executeGA
|
||||
}) {
|
||||
const [closeLabel, setCloseLabel] = React.useState(false);
|
||||
const { t } = useTranslation();
|
||||
const handleProcessing = (
|
||||
duration,
|
||||
amount,
|
||||
@ -93,19 +95,20 @@ function DonateModal({
|
||||
}
|
||||
}, [show, isBlockDonation, executeGA]);
|
||||
|
||||
const durationToText = donationDuration => {
|
||||
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 <b>{t('donate.duration')}</b>;
|
||||
case 'month':
|
||||
return <b>{t('donate.duration-2')}</b>;
|
||||
case 'year':
|
||||
return <b>{t('donate.duration-3')}</b>;
|
||||
default:
|
||||
return <b>{t('donate.duration-4')}</b>;
|
||||
}
|
||||
};
|
||||
|
||||
const donationText = (
|
||||
<b>
|
||||
Become {durationToText(modalDefaultDonation.donationDuration)} supporter
|
||||
of our nonprofit.
|
||||
</b>
|
||||
);
|
||||
const blockDonationText = (
|
||||
<div className=' text-center block-modal-text'>
|
||||
<div className='donation-icon-container'>
|
||||
@ -114,9 +117,9 @@ function DonateModal({
|
||||
<Row>
|
||||
{!closeLabel && (
|
||||
<Col sm={10} smOffset={1} xs={12}>
|
||||
<b>Nicely done. You just completed {blockNameify(block)}. </b>
|
||||
<b>{t('donate.nicely-done', { block: blockNameify(block) })}</b>
|
||||
<br />
|
||||
{donationText}
|
||||
{getDonationText()}
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
@ -131,7 +134,7 @@ function DonateModal({
|
||||
<Row>
|
||||
{!closeLabel && (
|
||||
<Col sm={10} smOffset={1} xs={12}>
|
||||
{donationText}
|
||||
{getDonationText()}
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
@ -155,7 +158,7 @@ function DonateModal({
|
||||
onClick={closeDonationModal}
|
||||
tabIndex='0'
|
||||
>
|
||||
{closeLabel ? 'Close' : 'Ask me later'}
|
||||
{closeLabel ? t('buttons.close') : t('buttons.ask-later')}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
|
@ -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));
|
||||
|
@ -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 (
|
||||
<div className='donation-elements'>
|
||||
<FormGroup>
|
||||
<ControlLabel>Your Card Number:</ControlLabel>
|
||||
<ControlLabel>{t('donate.card-number')}</ControlLabel>
|
||||
<CardNumberElement
|
||||
className='form-control donate-input-element'
|
||||
onChange={this.handleInputChange}
|
||||
@ -83,7 +86,7 @@ class StripeCardForm extends Component {
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<ControlLabel>Expiration Date:</ControlLabel>
|
||||
<ControlLabel>{t('donate.expiration')}</ControlLabel>
|
||||
<Row>
|
||||
<Col md={5} xs={12}>
|
||||
<CardExpiryElement
|
||||
@ -112,4 +115,4 @@ class StripeCardForm extends Component {
|
||||
StripeCardForm.displayName = 'StripeCardForm';
|
||||
StripeCardForm.propTypes = propTypes;
|
||||
|
||||
export default StripeCardForm;
|
||||
export default withTranslation()(StripeCardForm);
|
||||
|
@ -2,11 +2,16 @@ import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Alert } from '@freecodecamp/react-bootstrap';
|
||||
import { TransitionGroup, CSSTransition } from 'react-transition-group';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import './flash.css';
|
||||
|
||||
function Flash({ flashMessage, onClose }) {
|
||||
const { type, message, id } = flashMessage;
|
||||
// flash messages coming from the server are already translated
|
||||
// messages on the client get translated here and need a
|
||||
// needsTranslating variable set to true with the object
|
||||
const { type, message, id, needsTranslating = false } = flashMessage;
|
||||
const { t } = useTranslation();
|
||||
const [flashMessageHeight, setFlashMessageHeight] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
@ -33,7 +38,7 @@ function Flash({ flashMessage, onClose }) {
|
||||
className='flash-message'
|
||||
onDismiss={handleClose}
|
||||
>
|
||||
{message}
|
||||
{needsTranslating ? t(`${message}`) : message}
|
||||
</Alert>
|
||||
</CSSTransition>
|
||||
</TransitionGroup>
|
||||
@ -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
|
||||
};
|
||||
|
48
client/src/components/Footer/LanguageMenu.js
Normal file
48
client/src/components/Footer/LanguageMenu.js
Normal file
@ -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 (
|
||||
<div className='language-menu'>
|
||||
<span>{t('footer.language')}</span>
|
||||
<select onChange={e => changeLanguage(e)} value={currentLanguage}>
|
||||
{locales.map((lang, i) => {
|
||||
return (
|
||||
<option key={i} value={lang}>
|
||||
{langDisplayNames[lang]}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LanguageMenu;
|
@ -13,24 +13,46 @@ exports[`<Footer /> matches snapshot 1`] = `
|
||||
<div
|
||||
className="footer-desc-col"
|
||||
>
|
||||
<div
|
||||
className="language-menu"
|
||||
>
|
||||
<span>
|
||||
footer.language
|
||||
</span>
|
||||
<select
|
||||
onChange={[Function]}
|
||||
>
|
||||
<option
|
||||
value="english"
|
||||
>
|
||||
English
|
||||
</option>
|
||||
<option
|
||||
value="espanol"
|
||||
>
|
||||
Español
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<p>
|
||||
freeCodeCamp is a donor-supported tax-exempt 501(c)(3) nonprofit organization (United States Federal Tax Identification Number: 82-0779546)
|
||||
footer.tax-exempt-status
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
footer.mission-statement
|
||||
</p>
|
||||
<p>
|
||||
Donations to freeCodeCamp go toward our education initiatives, and help pay for servers, services, and staff.
|
||||
footer.donation-initiatives
|
||||
</p>
|
||||
<p
|
||||
className="footer-donation"
|
||||
>
|
||||
You can
|
||||
footer.donate-text
|
||||
|
||||
<a
|
||||
className="inline"
|
||||
href="/donate"
|
||||
>
|
||||
make a tax-deductible donation here
|
||||
footer.donate-link
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
@ -41,7 +63,7 @@ exports[`<Footer /> matches snapshot 1`] = `
|
||||
<div
|
||||
className="col-header"
|
||||
>
|
||||
Trending Guides
|
||||
footer.trending-guides
|
||||
</div>
|
||||
<div
|
||||
className="trending-guides-row"
|
||||
@ -285,7 +307,7 @@ exports[`<Footer /> matches snapshot 1`] = `
|
||||
<div
|
||||
className="col-header"
|
||||
>
|
||||
Our Nonprofit
|
||||
footer.our-nonprofit
|
||||
</div>
|
||||
<div
|
||||
className="footer-divder"
|
||||
@ -298,77 +320,77 @@ exports[`<Footer /> matches snapshot 1`] = `
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
About
|
||||
footer.links.about
|
||||
</a>
|
||||
<a
|
||||
href="https://www.linkedin.com/school/free-code-camp/people/"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Alumni Network
|
||||
footer.links.alumni
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/freeCodeCamp/"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Open Source
|
||||
footer.links.open-source
|
||||
</a>
|
||||
<a
|
||||
href="https://www.freecodecamp.org/shop/"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Shop
|
||||
footer.links.shop
|
||||
</a>
|
||||
<a
|
||||
href="https://www.freecodecamp.org/news/support/"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Support
|
||||
footer.links.support
|
||||
</a>
|
||||
<a
|
||||
href="https://www.freecodecamp.org/news/sponsors/"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Sponsors
|
||||
footer.links.sponsors
|
||||
</a>
|
||||
<a
|
||||
href="https://www.freecodecamp.org/news/academic-honesty-policy/"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Academic Honesty
|
||||
footer.links.honesty
|
||||
</a>
|
||||
<a
|
||||
href="https://www.freecodecamp.org/news/code-of-conduct/"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Code of Conduct
|
||||
footer.links.coc
|
||||
</a>
|
||||
<a
|
||||
href="https://www.freecodecamp.org/news/privacy-policy/"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Privacy Policy
|
||||
footer.links.privacy
|
||||
</a>
|
||||
<a
|
||||
href="https://www.freecodecamp.org/news/terms-of-service/"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Terms of Service
|
||||
footer.links.tos
|
||||
</a>
|
||||
<a
|
||||
href="https://www.freecodecamp.org/news/copyright-policy/"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Copyright Policy
|
||||
footer.links.copyright
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -20,6 +20,19 @@
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.footer-container .language-menu {
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.footer-container .language-menu span {
|
||||
margin-right: 14px;
|
||||
}
|
||||
|
||||
.footer-container .language-menu select {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.footer-container p {
|
||||
margin: 0 0 1.45rem;
|
||||
line-height: 30px;
|
||||
|
@ -1,10 +1,12 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Link from '../helpers/Link';
|
||||
|
||||
import LanguageMenu from './LanguageMenu';
|
||||
import './footer.css';
|
||||
|
||||
const { showLocaleDropdownMenu = false } = require('../../../config/env');
|
||||
|
||||
const propTypes = {
|
||||
children: PropTypes.any
|
||||
};
|
||||
@ -17,36 +19,27 @@ const ColHeader = ({ children, ...other }) => (
|
||||
ColHeader.propTypes = propTypes;
|
||||
|
||||
function Footer() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<footer className='site-footer'>
|
||||
<div className='footer-container'>
|
||||
<div className='footer-top'>
|
||||
<div className='footer-desc-col'>
|
||||
<p>
|
||||
freeCodeCamp is a donor-supported tax-exempt 501(c)(3) nonprofit
|
||||
organization (United States Federal Tax Identification Number:
|
||||
82-0779546)
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
Donations to freeCodeCamp go toward our education initiatives, and
|
||||
help pay for servers, services, and staff.
|
||||
</p>
|
||||
{showLocaleDropdownMenu ? <LanguageMenu /> : null}
|
||||
<p>{t('footer.tax-exempt-status')}</p>
|
||||
<p>{t('footer.mission-statement')}</p>
|
||||
<p>{t('footer.donation-initiatives')}</p>
|
||||
<p className='footer-donation'>
|
||||
You can
|
||||
{t('footer.donate-text')}{' '}
|
||||
<Link className='inline' to='/donate'>
|
||||
make a tax-deductible donation here
|
||||
{t('footer.donate-link')}
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
<div className='trending-guides'>
|
||||
<div className='col-header'>Trending Guides</div>
|
||||
<div className='col-header'>{t('footer.trending-guides')}</div>
|
||||
<div className='trending-guides-row'>
|
||||
<div className='footer-col footer-col-1'>
|
||||
<Link
|
||||
@ -303,73 +296,73 @@ function Footer() {
|
||||
</div>
|
||||
</div>
|
||||
<div className='footer-buttom'>
|
||||
<div className='col-header'>Our Nonprofit</div>
|
||||
<div className='col-header'>{t('footer.our-nonprofit')}</div>
|
||||
<div className='footer-divder' />
|
||||
<div className='our-nonprofit'>
|
||||
<Link
|
||||
external={false}
|
||||
to={'https://www.freecodecamp.org/news/about/'}
|
||||
>
|
||||
About
|
||||
{t('footer.links.about')}
|
||||
</Link>
|
||||
<Link
|
||||
external={false}
|
||||
sameTab={false}
|
||||
to={'https://www.linkedin.com/school/free-code-camp/people/'}
|
||||
>
|
||||
Alumni Network
|
||||
{t('footer.links.alumni')}
|
||||
</Link>
|
||||
<Link external={false} to={'https://github.com/freeCodeCamp/'}>
|
||||
Open Source
|
||||
{t('footer.links.open-source')}
|
||||
</Link>
|
||||
<Link
|
||||
external={false}
|
||||
sameTab={false}
|
||||
to={'https://www.freecodecamp.org/shop/'}
|
||||
>
|
||||
Shop
|
||||
{t('footer.links.shop')}
|
||||
</Link>
|
||||
<Link
|
||||
external={false}
|
||||
to={'https://www.freecodecamp.org/news/support/'}
|
||||
>
|
||||
Support
|
||||
{t('footer.links.support')}
|
||||
</Link>
|
||||
<Link
|
||||
external={false}
|
||||
to={'https://www.freecodecamp.org/news/sponsors/'}
|
||||
>
|
||||
Sponsors
|
||||
{t('footer.links.sponsors')}
|
||||
</Link>
|
||||
<Link
|
||||
external={false}
|
||||
to={'https://www.freecodecamp.org/news/academic-honesty-policy/'}
|
||||
>
|
||||
Academic Honesty
|
||||
{t('footer.links.honesty')}
|
||||
</Link>
|
||||
<Link
|
||||
external={false}
|
||||
to={'https://www.freecodecamp.org/news/code-of-conduct/'}
|
||||
>
|
||||
Code of Conduct
|
||||
{t('footer.links.coc')}
|
||||
</Link>
|
||||
<Link
|
||||
external={false}
|
||||
to={'https://www.freecodecamp.org/news/privacy-policy/'}
|
||||
>
|
||||
Privacy Policy
|
||||
{t('footer.links.privacy')}
|
||||
</Link>
|
||||
<Link
|
||||
external={false}
|
||||
to={'https://www.freecodecamp.org/news/terms-of-service/'}
|
||||
>
|
||||
Terms of Service
|
||||
{t('footer.links.tos')}
|
||||
</Link>
|
||||
<Link
|
||||
external={false}
|
||||
to={'https://www.freecodecamp.org/news/copyright-policy/'}
|
||||
>
|
||||
Copyright Policy
|
||||
{t('footer.links.copyright')}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -2,6 +2,7 @@ import React from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
import { Spacer } from '../../components/helpers';
|
||||
import { Link } from 'gatsby';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import notFoundLogo from '../../assets/images/freeCodeCamp-404.svg';
|
||||
import { randomQuote } from '../../utils/get-words';
|
||||
@ -9,18 +10,17 @@ import { randomQuote } from '../../utils/get-words';
|
||||
import './404.css';
|
||||
|
||||
const FourOhFour = () => {
|
||||
const { t } = useTranslation();
|
||||
const quote = randomQuote();
|
||||
return (
|
||||
<div className='notfound-page-wrapper'>
|
||||
<Helmet title='Page Not Found | freeCodeCamp' />
|
||||
<img alt='404 Not Found' src={notFoundLogo} />
|
||||
<Helmet title={t('404.page-not-found') + '| freeCodeCamp'} />
|
||||
<img alt={t('404.not-found')} src={notFoundLogo} />
|
||||
<Spacer />
|
||||
<h1>Page not found.</h1>
|
||||
<h1>{t('404.page-not-found')}.</h1>
|
||||
<Spacer />
|
||||
<div>
|
||||
<p>
|
||||
We couldn't find what you were looking for, but here is a quote:
|
||||
</p>
|
||||
<p>{t('404.heres-a-quote')}</p>
|
||||
<Spacer />
|
||||
<blockquote className='quote-wrapper'>
|
||||
<p className='quote'>
|
||||
@ -32,7 +32,7 @@ const FourOhFour = () => {
|
||||
</div>
|
||||
<Spacer size={2} />
|
||||
<Link className='btn btn-cta' to='/learn'>
|
||||
View the Curriculum
|
||||
{t('buttons.view-curriculum')}
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
|
@ -2,7 +2,10 @@
|
||||
import React from 'react';
|
||||
import ShallowRenderer from 'react-test-renderer/shallow';
|
||||
import renderer from 'react-test-renderer';
|
||||
/* import { useTranslation } from 'react-i18next';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
|
||||
import i18n from '../../../i18n/configForTests';*/
|
||||
import { UniversalNav } from './components/UniversalNav';
|
||||
import { AuthOrProfile } from './components/NavLinks';
|
||||
|
||||
@ -30,7 +33,6 @@ describe('<NavLinks />', () => {
|
||||
const shallow = new ShallowRenderer();
|
||||
shallow.render(<AuthOrProfile {...landingPageProps} />);
|
||||
const result = shallow.getRenderOutput();
|
||||
|
||||
expect(
|
||||
hasForumNavItem(result) &&
|
||||
hasCurriculumNavItem(result) &&
|
||||
@ -112,16 +114,18 @@ const profileNavItem = component => component[2].children[0];
|
||||
|
||||
const hasForumNavItem = component => {
|
||||
const { children, to } = navigationLinks(component, 0);
|
||||
return children === 'Forum' && to === 'https://forum.freecodecamp.org';
|
||||
return (
|
||||
children === 'buttons.forum' && to === 'https://forum.freecodecamp.org'
|
||||
);
|
||||
};
|
||||
|
||||
const hasCurriculumNavItem = component => {
|
||||
const { children, to } = navigationLinks(component, 1);
|
||||
return children === 'Curriculum' && to === '/learn';
|
||||
return children === 'buttons.curriculum' && to === '/learn';
|
||||
};
|
||||
|
||||
const hasSignInButton = component =>
|
||||
component.props.children[1].props.children === 'Sign In';
|
||||
component.props.children[1].props.children === 'buttons.sign-in';
|
||||
|
||||
const avatarHasClass = (componentTree, classes) => {
|
||||
// componentTree[1].children[0].children[1].props.className
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
AvatarRenderer
|
||||
} from '../../helpers';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import Login from '../components/Login';
|
||||
|
||||
@ -17,6 +18,7 @@ const propTypes = {
|
||||
};
|
||||
|
||||
export function AuthOrProfile({ user, pathName, pending }) {
|
||||
const { t } = useTranslation();
|
||||
const isUserDonating = user && user.isDonating;
|
||||
const isUserSignedIn = user && user.username;
|
||||
const isTopContributor =
|
||||
@ -31,18 +33,20 @@ export function AuthOrProfile({ user, pathName, pending }) {
|
||||
</div>
|
||||
);
|
||||
} else if (pathName === '/' || !isUserSignedIn) {
|
||||
return <Login data-test-label='landing-small-cta'>Sign In</Login>;
|
||||
return (
|
||||
<Login data-test-label='landing-small-cta'>{t('buttons.sign-in')}</Login>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<li>
|
||||
<Link className='nav-link' to='/learn'>
|
||||
Curriculum
|
||||
{t('buttons.curriculum')}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link className='nav-link' to={`/${user.username}`}>
|
||||
Profile
|
||||
{t('buttons.profile')}
|
||||
</Link>
|
||||
<Link
|
||||
className={`avatar-nav-link ${badgeColorClass}`}
|
||||
|
@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { Button } from '@freecodecamp/react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { isSignedInSelector } from '../../../redux';
|
||||
import { apiLocation } from '../../../../config/env.json';
|
||||
@ -19,6 +20,7 @@ const mapStateToProps = createSelector(
|
||||
);
|
||||
|
||||
function Login(props) {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
block,
|
||||
'data-test-label': dataTestLabel,
|
||||
@ -34,7 +36,7 @@ function Login(props) {
|
||||
href={href}
|
||||
onClick={() => gtagReportConversion()}
|
||||
>
|
||||
{children || 'Sign In'}
|
||||
{children || t('buttons.sign-in')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
@ -1,7 +1,11 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const MenuButton = props => (
|
||||
const MenuButton = props => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<button
|
||||
aria-expanded={props.displayMenu}
|
||||
className={
|
||||
@ -10,9 +14,10 @@ const MenuButton = props => (
|
||||
onClick={props.onClick}
|
||||
ref={props.innerRef}
|
||||
>
|
||||
Menu
|
||||
{t('buttons.menu')}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
MenuButton.displayName = 'MenuButton';
|
||||
MenuButton.propTypes = {
|
||||
|
@ -3,6 +3,7 @@ import { Link, SkeletonSprite, AvatarRenderer } from '../../helpers';
|
||||
import PropTypes from 'prop-types';
|
||||
import Login from '../components/Login';
|
||||
import { forumLocation } from '../../../../../config/env.json';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const propTypes = {
|
||||
displayMenu: PropTypes.bool,
|
||||
@ -11,6 +12,7 @@ const propTypes = {
|
||||
};
|
||||
|
||||
export function AuthOrProfile({ user, pending }) {
|
||||
const { t } = useTranslation();
|
||||
const isUserDonating = user && user.isDonating;
|
||||
const isUserSignedIn = user && user.username;
|
||||
const isTopContributor =
|
||||
@ -25,12 +27,12 @@ export function AuthOrProfile({ user, pending }) {
|
||||
sameTab={true}
|
||||
to={forumLocation}
|
||||
>
|
||||
Forum
|
||||
{t('buttons.forum')}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link className='nav-link' to='/learn'>
|
||||
Curriculum
|
||||
{t('buttons.curriculum')}
|
||||
</Link>
|
||||
</li>
|
||||
</>
|
||||
@ -46,7 +48,9 @@ export function AuthOrProfile({ user, pending }) {
|
||||
return (
|
||||
<>
|
||||
{CurriculumAndForumLinks}
|
||||
<Login data-test-label='landing-small-cta'>Sign In</Login>
|
||||
<Login data-test-label='landing-small-cta'>
|
||||
{t('buttons.sign-in')}
|
||||
</Login>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
@ -55,7 +59,7 @@ export function AuthOrProfile({ user, pending }) {
|
||||
{CurriculumAndForumLinks}
|
||||
<li>
|
||||
<Link className='nav-link' to={`/${user.username}`}>
|
||||
Profile
|
||||
{t('buttons.profile')}
|
||||
<AvatarRenderer
|
||||
isDonating={isUserDonating}
|
||||
isTopContributor={isTopContributor}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import FreeCodeCampLogo from '../../../assets/icons/freeCodeCampLogo';
|
||||
import FreeCodeCampLogo from '../../../assets/icons/FreeCodeCampLogo';
|
||||
|
||||
function NavLogo() {
|
||||
return <FreeCodeCampLogo />;
|
||||
|
@ -4,6 +4,7 @@ import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { Link } from 'gatsby';
|
||||
import Spinner from 'react-spinkit';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { isSignedInSelector, userFetchStateSelector } from '../../../redux';
|
||||
import Login from './Login';
|
||||
@ -27,6 +28,8 @@ const propTypes = {
|
||||
|
||||
function UserState(props) {
|
||||
const { isSignedIn, showLoading, disableSettings } = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (disableSettings) {
|
||||
return <Login />;
|
||||
}
|
||||
@ -43,7 +46,7 @@ function UserState(props) {
|
||||
}
|
||||
return isSignedIn ? (
|
||||
<Link className='top-right-nav-link' to='/settings'>
|
||||
Settings
|
||||
{t('buttons.settings')}
|
||||
</Link>
|
||||
) : (
|
||||
<Login />
|
||||
|
@ -2,10 +2,13 @@ import React from 'react';
|
||||
import { Link, Spacer } from '../../helpers';
|
||||
import { Col } from '@freecodecamp/react-bootstrap';
|
||||
import { forumLocation } from '../../../../config/env.json';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
import '../intro.css';
|
||||
|
||||
function IntroDescription() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Col
|
||||
className='intro-description'
|
||||
@ -15,52 +18,28 @@ function IntroDescription() {
|
||||
smOffset={1}
|
||||
xs={12}
|
||||
>
|
||||
<strong>Please slow down and read this.</strong>
|
||||
<strong>{t('learn.read-this.heading')}</strong>
|
||||
<Spacer />
|
||||
<p>freeCodeCamp is a proven path to your first software developer job.</p>
|
||||
<p>{t('learn.read-this.p1')}</p>
|
||||
<p>{t('learn.read-this.p2')}</p>
|
||||
<p>{t('learn.read-this.p3')}</p>
|
||||
<p>{t('learn.read-this.p4')}</p>
|
||||
<p>{t('learn.read-this.p5')}</p>
|
||||
<p>{t('learn.read-this.p6')}</p>
|
||||
<p>{t('learn.read-this.p7')}</p>
|
||||
<p>{t('learn.read-this.p8')}</p>
|
||||
<p>
|
||||
More than 40,000 people have gotten developer jobs after completing this
|
||||
– including at big companies like Google and Microsoft.
|
||||
<Trans i18nKey='learn.read-this.p9'>
|
||||
<Link className='inline' to='https://youtube.com/freecodecamp' />
|
||||
</Trans>
|
||||
</p>
|
||||
<p>{t('learn.read-this.p10')}</p>
|
||||
<p>
|
||||
If you are new to programming, we recommend you start at the beginning
|
||||
and earn these certifications in order.
|
||||
<Trans i18nKey='learn.read-this.p11'>
|
||||
<Link className='inline' to={forumLocation} />
|
||||
</Trans>
|
||||
</p>
|
||||
<p>
|
||||
To earn each certification, build its 5 required projects and get all
|
||||
their tests to pass.
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>If you feel overwhelmed, that is normal. Programming is hard.</p>
|
||||
<p>Practice is the key. Practice, practice, practice.</p>
|
||||
<p>
|
||||
And this curriculum will give you thousands of hours of hands-on
|
||||
programming practice.
|
||||
</p>
|
||||
<p>
|
||||
And if you want to learn more math and computer science theory, we also
|
||||
have thousands of hours of video courses on{' '}
|
||||
<Link className='inline' to='https://youtube.com/freecodecamp'>
|
||||
freeCodeCamp's YouTube channel
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
You can do this on Twitter and GitHub, and also on{' '}
|
||||
<Link className='inline' to={forumLocation}>
|
||||
the freeCodeCamp forum
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
<p>Happy coding.</p>
|
||||
<p>{t('learn.read-this.p12')}</p>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import { Row, Col } from '@freecodecamp/react-bootstrap';
|
||||
import { randomQuote } from '../../utils/get-words';
|
||||
import CurrentChallengeLink from '../helpers/CurrentChallengeLink';
|
||||
import IntroDescription from './components/IntroDescription';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
import './intro.css';
|
||||
import Login from '../Header/components/Login';
|
||||
@ -27,6 +28,7 @@ function Intro({
|
||||
completedChallengeCount,
|
||||
slug
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
if (pending && !complete) {
|
||||
return (
|
||||
<>
|
||||
@ -43,18 +45,20 @@ function Intro({
|
||||
<Col sm={10} smOffset={1} xs={12}>
|
||||
<Spacer />
|
||||
<h1 className='text-center '>
|
||||
{name ? `Welcome back, ${name}.` : `Welcome to freeCodeCamp.org`}
|
||||
{name
|
||||
? `${t('learn.welcome-1', { name: name })}`
|
||||
: `${t('learn.welcome-2')}`}
|
||||
</h1>
|
||||
<Spacer />
|
||||
</Col>
|
||||
</Row>
|
||||
<FullWidthRow>
|
||||
<Link className='btn btn-lg btn-primary btn-block' to='/settings'>
|
||||
Update my account settings
|
||||
{t('buttons.update-settings')}
|
||||
</Link>
|
||||
{completedChallengeCount > 0 ? (
|
||||
<CurrentChallengeLink isLargeBtn={true}>
|
||||
Go to current challenge
|
||||
{t('buttons.current-challenge')}
|
||||
</CurrentChallengeLink>
|
||||
) : (
|
||||
''
|
||||
@ -78,8 +82,9 @@ function Intro({
|
||||
<Col sm={10} smOffset={1} xs={12}>
|
||||
<Spacer />
|
||||
<h4>
|
||||
If you are new to coding, we recommend you{' '}
|
||||
<Link to={slug}>start at the beginning</Link>.
|
||||
<Trans i18nKey='learn.start-at-beginning'>
|
||||
<Link to={slug} />
|
||||
</Trans>
|
||||
</h4>
|
||||
</Col>
|
||||
) : (
|
||||
@ -94,15 +99,13 @@ function Intro({
|
||||
<Row>
|
||||
<Col sm={8} smOffset={2} xs={12}>
|
||||
<Spacer />
|
||||
<h1>Welcome to freeCodeCamp's curriculum.</h1>
|
||||
<h1>{t('learn.heading')}</h1>
|
||||
<Spacer size={1} />
|
||||
</Col>
|
||||
<IntroDescription />
|
||||
<Col sm={8} smOffset={2} xs={12}>
|
||||
<Spacer />
|
||||
<Login block={true}>
|
||||
Sign in to save your progress (it's free)
|
||||
</Login>
|
||||
<Login block={true}>{t('buttons.logged-out-cta-btn')}</Login>
|
||||
</Col>
|
||||
</Row>
|
||||
<Spacer />
|
||||
|
@ -29,7 +29,7 @@ exports[`<Block /> not expanded snapshot: block-not-expanded 1`] = `
|
||||
<span
|
||||
class="sr-only"
|
||||
>
|
||||
Not Passed
|
||||
icons.not-passed
|
||||
</span>
|
||||
<svg
|
||||
height="50"
|
||||
@ -40,7 +40,7 @@ exports[`<Block /> not expanded snapshot: block-not-expanded 1`] = `
|
||||
>
|
||||
<g>
|
||||
<title>
|
||||
Not Passed
|
||||
icons.not-passed
|
||||
</title>
|
||||
<circle
|
||||
cx="100"
|
||||
@ -95,7 +95,7 @@ exports[`<Block expanded snapshot: block-expanded 1`] = `
|
||||
<span
|
||||
class="sr-only"
|
||||
>
|
||||
Not Passed
|
||||
icons.not-passed
|
||||
</span>
|
||||
<svg
|
||||
height="50"
|
||||
@ -106,7 +106,7 @@ exports[`<Block expanded snapshot: block-expanded 1`] = `
|
||||
>
|
||||
<g>
|
||||
<title>
|
||||
Not Passed
|
||||
icons.not-passed
|
||||
</title>
|
||||
<circle
|
||||
cx="100"
|
||||
@ -138,7 +138,7 @@ exports[`<Block expanded snapshot: block-expanded 1`] = `
|
||||
<span
|
||||
class="sr-only"
|
||||
>
|
||||
IntroInformation
|
||||
icons.info
|
||||
</span>
|
||||
<svg
|
||||
height="50"
|
||||
@ -149,7 +149,7 @@ exports[`<Block expanded snapshot: block-expanded 1`] = `
|
||||
>
|
||||
<g>
|
||||
<title>
|
||||
IntroInformation
|
||||
icons.info
|
||||
</title>
|
||||
<circle
|
||||
cx="100"
|
||||
@ -192,7 +192,7 @@ exports[`<Block expanded snapshot: block-expanded 1`] = `
|
||||
<span
|
||||
class="sr-only"
|
||||
>
|
||||
Passed
|
||||
icons.passed
|
||||
</span>
|
||||
<svg
|
||||
height="50"
|
||||
@ -203,7 +203,7 @@ exports[`<Block expanded snapshot: block-expanded 1`] = `
|
||||
>
|
||||
<g>
|
||||
<title>
|
||||
Passed
|
||||
icons.passed
|
||||
</title>
|
||||
<circle
|
||||
cx="100"
|
||||
@ -258,7 +258,7 @@ exports[`<Block expanded snapshot: block-expanded 1`] = `
|
||||
<span
|
||||
class="sr-only"
|
||||
>
|
||||
Not Passed
|
||||
icons.not-passed
|
||||
</span>
|
||||
<svg
|
||||
height="50"
|
||||
@ -269,7 +269,7 @@ exports[`<Block expanded snapshot: block-expanded 1`] = `
|
||||
>
|
||||
<g>
|
||||
<title>
|
||||
Not Passed
|
||||
icons.not-passed
|
||||
</title>
|
||||
<circle
|
||||
cx="100"
|
||||
@ -301,7 +301,7 @@ exports[`<Block expanded snapshot: block-expanded 1`] = `
|
||||
<span
|
||||
class="sr-only"
|
||||
>
|
||||
Not Passed
|
||||
icons.not-passed
|
||||
</span>
|
||||
<svg
|
||||
height="50"
|
||||
@ -312,7 +312,7 @@ exports[`<Block expanded snapshot: block-expanded 1`] = `
|
||||
>
|
||||
<g>
|
||||
<title>
|
||||
Not Passed
|
||||
icons.not-passed
|
||||
</title>
|
||||
<circle
|
||||
cx="100"
|
||||
@ -344,7 +344,7 @@ exports[`<Block expanded snapshot: block-expanded 1`] = `
|
||||
<span
|
||||
class="sr-only"
|
||||
>
|
||||
Passed
|
||||
icons.passed
|
||||
</span>
|
||||
<svg
|
||||
height="50"
|
||||
@ -355,7 +355,7 @@ exports[`<Block expanded snapshot: block-expanded 1`] = `
|
||||
>
|
||||
<g>
|
||||
<title>
|
||||
Passed
|
||||
icons.passed
|
||||
</title>
|
||||
<circle
|
||||
cx="100"
|
||||
@ -410,7 +410,7 @@ exports[`<Block expanded snapshot: block-expanded 1`] = `
|
||||
<span
|
||||
class="sr-only"
|
||||
>
|
||||
Not Passed
|
||||
icons.not-passed
|
||||
</span>
|
||||
<svg
|
||||
height="50"
|
||||
@ -421,7 +421,7 @@ exports[`<Block expanded snapshot: block-expanded 1`] = `
|
||||
>
|
||||
<g>
|
||||
<title>
|
||||
Not Passed
|
||||
icons.not-passed
|
||||
</title>
|
||||
<circle
|
||||
cx="100"
|
||||
|
@ -52,7 +52,7 @@ exports[`<SuperBlock /> expanded snapshot: superBlock-expanded 1`] = `
|
||||
<span
|
||||
class="sr-only"
|
||||
>
|
||||
Not Passed
|
||||
icons.not-passed
|
||||
</span>
|
||||
<svg
|
||||
height="50"
|
||||
@ -63,7 +63,7 @@ exports[`<SuperBlock /> expanded snapshot: superBlock-expanded 1`] = `
|
||||
>
|
||||
<g>
|
||||
<title>
|
||||
Not Passed
|
||||
icons.not-passed
|
||||
</title>
|
||||
<circle
|
||||
cx="100"
|
||||
@ -113,7 +113,7 @@ exports[`<SuperBlock /> expanded snapshot: superBlock-expanded 1`] = `
|
||||
<span
|
||||
class="sr-only"
|
||||
>
|
||||
Not Passed
|
||||
icons.not-passed
|
||||
</span>
|
||||
<svg
|
||||
height="50"
|
||||
@ -124,7 +124,7 @@ exports[`<SuperBlock /> expanded snapshot: superBlock-expanded 1`] = `
|
||||
>
|
||||
<g>
|
||||
<title>
|
||||
Not Passed
|
||||
icons.not-passed
|
||||
</title>
|
||||
<circle
|
||||
cx="100"
|
||||
@ -174,7 +174,7 @@ exports[`<SuperBlock /> expanded snapshot: superBlock-expanded 1`] = `
|
||||
<span
|
||||
class="sr-only"
|
||||
>
|
||||
Not Passed
|
||||
icons.not-passed
|
||||
</span>
|
||||
<svg
|
||||
height="50"
|
||||
@ -185,7 +185,7 @@ exports[`<SuperBlock /> expanded snapshot: superBlock-expanded 1`] = `
|
||||
>
|
||||
<g>
|
||||
<title>
|
||||
Not Passed
|
||||
icons.not-passed
|
||||
</title>
|
||||
<circle
|
||||
cx="100"
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import './offline-warning.css';
|
||||
|
||||
@ -10,7 +11,9 @@ const propTypes = {
|
||||
isSignedIn: PropTypes.bool.isRequired
|
||||
};
|
||||
function OfflineWarning({ isOnline, isSignedIn }) {
|
||||
const { t } = useTranslation();
|
||||
const [showWarning, setShowWarning] = React.useState(false);
|
||||
|
||||
if (!isSignedIn || isOnline) {
|
||||
clearTimeout(id);
|
||||
if (showWarning) setShowWarning(false);
|
||||
@ -25,9 +28,7 @@ function OfflineWarning({ isOnline, isSignedIn }) {
|
||||
}
|
||||
|
||||
return showWarning ? (
|
||||
<div className='offline-warning alert-info'>
|
||||
You appear to be offline, your progress may not be being saved.
|
||||
</div>
|
||||
<div className='offline-warning alert-info'>{t('misc.offline')}</div>
|
||||
) : null;
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,13 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button } from '@freecodecamp/react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function BlockSaveButton(props) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Button block={true} bsStyle='primary' {...props} type='submit'>
|
||||
{props.children || 'Save'}
|
||||
{props.children || t('buttons.save')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
@ -11,10 +11,10 @@ test('<BlockSaveButton /> snapshot', () => {
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('Button text should default to "Save"', () => {
|
||||
test('Button text should default to the correct translation key', () => {
|
||||
const { getByRole } = render(<BlockSaveButton />);
|
||||
|
||||
expect(getByRole('button')).toHaveTextContent('Save');
|
||||
expect(getByRole('button')).toHaveTextContent('buttons.save');
|
||||
});
|
||||
|
||||
test('Button text should match "children"', () => {
|
||||
|
@ -6,7 +6,7 @@ exports[`<BlockSaveButton /> snapshot 1`] = `
|
||||
class="btn btn-primary btn-block"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
buttons.save
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
@ -4,6 +4,7 @@ import { Image } from '@freecodecamp/react-bootstrap';
|
||||
import DefaultAvatar from '../../assets/icons/DefaultAvatar';
|
||||
import { defaultUserImage } from '../../../../config/misc';
|
||||
import { borderColorPicker } from '../helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const propTypes = {
|
||||
isDonating: PropTypes.bool,
|
||||
@ -13,6 +14,7 @@ const propTypes = {
|
||||
};
|
||||
|
||||
function AvatarRenderer({ picture, userName, isDonating, isTopContributor }) {
|
||||
const { t } = useTranslation();
|
||||
let borderColor = borderColorPicker(isDonating, isTopContributor);
|
||||
let isPlaceHolderImage =
|
||||
/example.com|identicon.org/.test(picture) || picture === defaultUserImage;
|
||||
@ -23,7 +25,7 @@ function AvatarRenderer({ picture, userName, isDonating, isTopContributor }) {
|
||||
<DefaultAvatar className='avatar default-avatar' />
|
||||
) : (
|
||||
<Image
|
||||
alt={userName + "'s avatar"}
|
||||
alt={t('profile.avatar', { username: userName })}
|
||||
className='avatar'
|
||||
responsive={true}
|
||||
src={picture}
|
||||
|
@ -1,8 +1,11 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button } from '@freecodecamp/react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function BlockSaveButton({ children, ...restProps }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Button
|
||||
block={true}
|
||||
@ -11,7 +14,7 @@ function BlockSaveButton({ children, ...restProps }) {
|
||||
type='submit'
|
||||
{...restProps}
|
||||
>
|
||||
{children || 'Save'}
|
||||
{children || t('buttons.save')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
@ -1,18 +1,22 @@
|
||||
import React from 'react';
|
||||
import { Col, Row } from '@freecodecamp/react-bootstrap';
|
||||
|
||||
import { AsFeatureLogo } from '../../../assets/images/components';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const AsSeenIn = () => (
|
||||
const AsSeenIn = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Row className='as-seen-in'>
|
||||
<Col sm={8} smOffset={2} xs={12}>
|
||||
<div className='text-center'>
|
||||
<p className='big-heading'>As seen in:</p>
|
||||
<p className='big-heading'>{t('landing.as-seen-in')}</p>
|
||||
<AsFeatureLogo fill='light' />
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
AsSeenIn.displayName = 'AsSeenIn';
|
||||
export default AsSeenIn;
|
||||
|
@ -1,18 +1,23 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Login from '../../Header/components/Login';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const propTypes = {
|
||||
page: PropTypes.string
|
||||
};
|
||||
|
||||
const BigCallToAction = ({ page }) => (
|
||||
const BigCallToAction = ({ page }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Login block={true} data-test-label={`${page}-big-cta`}>
|
||||
{page === 'landing'
|
||||
? "Get started (it's free)"
|
||||
: "Sign in to save your progress (it's free)"}
|
||||
? t('buttons.logged-in-cta-btn')
|
||||
: t('buttons.logged-out-cta-btn')}
|
||||
</Login>
|
||||
);
|
||||
};
|
||||
|
||||
BigCallToAction.displayName = 'BigCallToAction';
|
||||
BigCallToAction.propTypes = propTypes;
|
||||
|
@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import Media from 'react-responsive';
|
||||
import { Spacer, ImageLoader } from '../../helpers';
|
||||
import wideImg from '../../../assets/images/landing/wide-image.png';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const propTypes = {
|
||||
page: PropTypes.string
|
||||
@ -24,20 +25,20 @@ const imageConfig = {
|
||||
};
|
||||
|
||||
function CampersImage({ page }) {
|
||||
const { t } = useTranslation();
|
||||
const { spacerSize, height, width } = imageConfig[page];
|
||||
|
||||
return (
|
||||
<Media minWidth={LARGE_SCREEN_SIZE}>
|
||||
<Spacer size={spacerSize} />
|
||||
<ImageLoader
|
||||
alt='freeCodeCamp students at a local study group in South Korea.'
|
||||
alt={t('landing.hero-img-description')}
|
||||
className='landing-page-image'
|
||||
height={height}
|
||||
src={wideImg}
|
||||
width={width}
|
||||
/>
|
||||
<p className='text-center caption'>
|
||||
freeCodeCamp students at a local study group in South Korea.
|
||||
</p>
|
||||
<p className='text-center caption'>{t('landing.hero-img-description')}</p>
|
||||
</Media>
|
||||
);
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import { uniq } from 'lodash';
|
||||
import { Spacer, Link } from '../../helpers';
|
||||
import LinkButton from '../../../assets/icons/LinkButton';
|
||||
import BigCallToAction from './BigCallToAction';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const propTypes = {
|
||||
nodes: PropTypes.array,
|
||||
@ -12,6 +13,7 @@ const propTypes = {
|
||||
};
|
||||
|
||||
const Certifications = ({ nodes, page }) => {
|
||||
const { t } = useTranslation();
|
||||
const superBlocks = uniq(nodes.map(node => node.superBlock)).filter(
|
||||
cert => cert !== 'Coding Interview Prep'
|
||||
);
|
||||
@ -19,7 +21,7 @@ const Certifications = ({ nodes, page }) => {
|
||||
return (
|
||||
<Row className='certification-section'>
|
||||
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
|
||||
<p className='big-heading'>Earn free verified certifications in:</p>
|
||||
<p className='big-heading'>{t('landing.certification-heading')}</p>
|
||||
<ul data-test-label='certifications'>
|
||||
{superBlocks.map((superBlock, i) => (
|
||||
<li key={i}>
|
||||
|
@ -2,7 +2,6 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Col, Row } from '@freecodecamp/react-bootstrap';
|
||||
import { Spacer } from '../../helpers';
|
||||
import Login from '../../Header/components/Login';
|
||||
import {
|
||||
AmazonLogo,
|
||||
AppleLogo,
|
||||
@ -11,33 +10,27 @@ import {
|
||||
GoogleLogo
|
||||
} from '../../../assets/images/components';
|
||||
import CampersImage from './CampersImage';
|
||||
import BigCallToAction from './BigCallToAction';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const propTypes = {
|
||||
page: PropTypes.string
|
||||
};
|
||||
|
||||
function landingTop({ page }) {
|
||||
const BigCallToAction = (
|
||||
<Login block={true} data-test-label={`${page}-big-cta`}>
|
||||
{page === 'landing'
|
||||
? "Get started (it's free)"
|
||||
: "Sign in to save your progress (it's free)"}
|
||||
</Login>
|
||||
);
|
||||
function LandingTop({ page }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className='landing-top'>
|
||||
<Row>
|
||||
<Spacer />
|
||||
<Col lg={8} lgOffset={2} sm={10} smOffset={1} xs={12}>
|
||||
<h1 className='big-heading' data-test-label={`${page}-header`}>
|
||||
Learn to code — for free.
|
||||
{t('landing.big-heading-1')}
|
||||
</h1>
|
||||
<p className='big-heading'>Build projects.</p>
|
||||
<p className='big-heading'>Earn certifications.</p>
|
||||
<p>
|
||||
Since 2014, more than 40,000 freeCodeCamp.org graduates have gotten
|
||||
jobs at tech companies including:
|
||||
</p>
|
||||
<p className='big-heading'>{t('landing.big-heading-2')}</p>
|
||||
<p className='big-heading'>{t('landing.big-heading-3')}</p>
|
||||
<p>{t('landing.h2-heading')}</p>
|
||||
<div className='logo-row'>
|
||||
<AppleLogo />
|
||||
<GoogleLogo />
|
||||
@ -46,7 +39,7 @@ function landingTop({ page }) {
|
||||
<SpotifyLogo />
|
||||
</div>
|
||||
<Spacer />
|
||||
{BigCallToAction}
|
||||
<BigCallToAction page={page} />
|
||||
<CampersImage page={page} />
|
||||
<Spacer />
|
||||
</Col>
|
||||
@ -55,6 +48,6 @@ function landingTop({ page }) {
|
||||
);
|
||||
}
|
||||
|
||||
landingTop.displayName = 'LandingTop';
|
||||
landingTop.propTypes = propTypes;
|
||||
export default landingTop;
|
||||
LandingTop.displayName = 'LandingTop';
|
||||
LandingTop.propTypes = propTypes;
|
||||
export default LandingTop;
|
||||
|
@ -2,37 +2,30 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { ImageLoader } from '../../helpers';
|
||||
import testimonialsMeta from '../landingMeta';
|
||||
import shawnImg from '../../../assets/images/landing/Shawn.png';
|
||||
import sarahImg from '../../../assets/images/landing/Sarah.png';
|
||||
import emmaImg from '../../../assets/images/landing/Emma.png';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
const propTypes = {
|
||||
page: PropTypes.string
|
||||
};
|
||||
|
||||
const Testimonials = () => {
|
||||
const campers = Object.keys(testimonialsMeta);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className='testimonials'>
|
||||
<p className='big-heading text-center'>
|
||||
Here is what our alumni say about freeCodeCamp:
|
||||
</p>
|
||||
<h1 className='big-heading text-center'>
|
||||
{t('landing.testimonials.heading')}
|
||||
</h1>
|
||||
<div className='testimonials-row' data-test-label='testimonial-cards'>
|
||||
{campers.map((camper, i) => {
|
||||
let {
|
||||
name,
|
||||
country,
|
||||
position,
|
||||
company,
|
||||
testimony
|
||||
} = testimonialsMeta[camper];
|
||||
let imageSource = require(`../../../assets/images/landing/${camper}.png`);
|
||||
return (
|
||||
<div className='testimonial-card' key={i}>
|
||||
<div className='testimonial-card'>
|
||||
<div className='testimonial-card-header'>
|
||||
<ImageLoader
|
||||
alt={`${name}`}
|
||||
alt='Shawn Wang'
|
||||
className='testimonial-image'
|
||||
src={imageSource}
|
||||
src={shawnImg}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -40,17 +33,73 @@ const Testimonials = () => {
|
||||
<div className='testimonial-meta'>
|
||||
<p>
|
||||
{' '}
|
||||
<strong>{name}</strong> in {country}
|
||||
<Trans>landing.testimonials.shawn.location</Trans>
|
||||
</p>
|
||||
<p>
|
||||
{position} at <strong>{company}</strong>
|
||||
<Trans>landing.testimonials.shawn.occupation</Trans>
|
||||
</p>
|
||||
</div>
|
||||
<div className='testimony'>
|
||||
<p>
|
||||
<Trans>landing.testimonials.shawn.testimony</Trans>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='testimonial-card'>
|
||||
<div className='testimonial-card-header'>
|
||||
<ImageLoader
|
||||
alt='Sarah Chima'
|
||||
className='testimonial-image'
|
||||
src={sarahImg}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='testimonials-footer'>
|
||||
<div className='testimonial-meta'>
|
||||
<p>
|
||||
{' '}
|
||||
<Trans>landing.testimonials.sarah.location</Trans>
|
||||
</p>
|
||||
<p>
|
||||
<Trans>landing.testimonials.sarah.occupation</Trans>
|
||||
</p>
|
||||
</div>
|
||||
<div className='testimony'>
|
||||
<p>
|
||||
<Trans>landing.testimonials.sarah.testimony</Trans>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='testimonial-card'>
|
||||
<div className='testimonial-card-header'>
|
||||
<ImageLoader
|
||||
alt='Emma Bostian'
|
||||
className='testimonial-image'
|
||||
src={emmaImg}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='testimonials-footer'>
|
||||
<div className='testimonial-meta'>
|
||||
<p>
|
||||
{' '}
|
||||
<Trans>landing.testimonials.emma.location</Trans>
|
||||
</p>
|
||||
<p>
|
||||
<Trans>landing.testimonials.emma.occupation</Trans>
|
||||
</p>
|
||||
</div>
|
||||
<div className='testimony'>
|
||||
<p>
|
||||
<Trans>landing.testimonials.emma.testimony</Trans>
|
||||
</p>
|
||||
</div>
|
||||
<div className='testimony'>{testimony}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -3,6 +3,7 @@ import { Grid } from '@freecodecamp/react-bootstrap';
|
||||
import Helmet from 'react-helmet';
|
||||
import PropTypes from 'prop-types';
|
||||
import { graphql, useStaticQuery } from 'gatsby';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import Testimonials from './components/Testimonials';
|
||||
import LandingTop from './components/LandingTop';
|
||||
@ -16,6 +17,8 @@ const propTypes = {
|
||||
};
|
||||
|
||||
export const Landing = ({ page = 'landing' }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const data = useStaticQuery(graphql`
|
||||
query certifications {
|
||||
challenges: allChallengeNode(
|
||||
@ -31,7 +34,7 @@ export const Landing = ({ page = 'landing' }) => {
|
||||
return (
|
||||
<Fragment>
|
||||
<Helmet>
|
||||
<title>Learn to Code — For Free — Coding Courses for Busy People</title>
|
||||
<title>{t('meta.title')}</title>
|
||||
</Helmet>
|
||||
<main className='landing-page'>
|
||||
<Grid>
|
||||
|
@ -1,48 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
export default {
|
||||
Shawn: {
|
||||
name: 'Shawn Wang',
|
||||
country: 'Singapore',
|
||||
position: 'Software Engineer',
|
||||
company: 'Amazon',
|
||||
testimony: (
|
||||
<p>
|
||||
"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.
|
||||
<strong> freeCodeCamp changed my life.</strong>"
|
||||
</p>
|
||||
)
|
||||
},
|
||||
Sarah: {
|
||||
name: 'Sarah Chima',
|
||||
country: 'Nigeria',
|
||||
position: 'Software Engineer',
|
||||
company: 'ChatDesk',
|
||||
testimony: (
|
||||
<p>
|
||||
“<strong>freeCodeCamp was the gateway to my career </strong>
|
||||
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."
|
||||
</p>
|
||||
)
|
||||
},
|
||||
Emma: {
|
||||
name: 'Emma Bostian',
|
||||
country: 'Sweden',
|
||||
position: 'Software Engineer',
|
||||
company: 'Spotify',
|
||||
testimony: (
|
||||
<p>
|
||||
"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{' '}
|
||||
<strong>freeCodeCamp gave me the skills</strong> and confidence I needed
|
||||
to land my dream job as a software engineer at Spotify."
|
||||
</p>
|
||||
)
|
||||
}
|
||||
};
|
@ -5,6 +5,7 @@ import { bindActionCreators } from 'redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import Helmet from 'react-helmet';
|
||||
import fontawesome from '@fortawesome/fontawesome';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
|
||||
import {
|
||||
fetchUser,
|
||||
@ -44,30 +45,6 @@ fontawesome.config = {
|
||||
autoAddCss: false
|
||||
};
|
||||
|
||||
const metaKeywords = [
|
||||
'javascript',
|
||||
'js',
|
||||
'website',
|
||||
'web',
|
||||
'development',
|
||||
'free',
|
||||
'code',
|
||||
'camp',
|
||||
'course',
|
||||
'courses',
|
||||
'html',
|
||||
'css',
|
||||
'react',
|
||||
'redux',
|
||||
'api',
|
||||
'front',
|
||||
'back',
|
||||
'end',
|
||||
'learn',
|
||||
'tutorial',
|
||||
'programming'
|
||||
];
|
||||
|
||||
const propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
executeGA: PropTypes.func,
|
||||
@ -86,6 +63,7 @@ const propTypes = {
|
||||
removeFlashMessage: PropTypes.func.isRequired,
|
||||
showFooter: PropTypes.bool,
|
||||
signedInUserName: PropTypes.string,
|
||||
t: PropTypes.func.isRequired,
|
||||
theme: PropTypes.string,
|
||||
useTheme: PropTypes.bool,
|
||||
user: PropTypes.object
|
||||
@ -157,6 +135,7 @@ class DefaultLayout extends Component {
|
||||
isSignedIn,
|
||||
removeFlashMessage,
|
||||
showFooter = true,
|
||||
t,
|
||||
theme = 'default',
|
||||
user,
|
||||
useTheme = true
|
||||
@ -173,9 +152,9 @@ class DefaultLayout extends Component {
|
||||
meta={[
|
||||
{
|
||||
name: 'description',
|
||||
content: `Learn to code — for free.`
|
||||
content: t('meta.description')
|
||||
},
|
||||
{ name: 'keywords', content: metaKeywords.join(', ') }
|
||||
{ name: 'keywords', content: t('meta.keywords') }
|
||||
]}
|
||||
>
|
||||
<link
|
||||
@ -244,4 +223,4 @@ DefaultLayout.propTypes = propTypes;
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(DefaultLayout);
|
||||
)(withTranslation()(DefaultLayout));
|
||||
|
@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import { Grid, Row, Button } from '@freecodecamp/react-bootstrap';
|
||||
import Helmet from 'react-helmet';
|
||||
import Link from '../helpers/Link';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { CurrentChallengeLink, FullWidthRow, Spacer } from '../helpers';
|
||||
import Camper from './components/Camper';
|
||||
@ -50,20 +51,14 @@ const propTypes = {
|
||||
})
|
||||
};
|
||||
|
||||
function renderMessage(isSessionUser, username) {
|
||||
function renderMessage(isSessionUser, username, t) {
|
||||
return isSessionUser ? (
|
||||
<Fragment>
|
||||
<FullWidthRow>
|
||||
<h2 className='text-center'>
|
||||
You have not made your portfolio public.
|
||||
</h2>
|
||||
<h2 className='text-center'>{t('profile.you-not-public')}</h2>
|
||||
</FullWidthRow>
|
||||
<FullWidthRow>
|
||||
<p className='alert alert-info'>
|
||||
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.
|
||||
</p>
|
||||
<p className='alert alert-info'>{t('profile.you-change-privacy')}</p>
|
||||
</FullWidthRow>
|
||||
<Spacer />
|
||||
</Fragment>
|
||||
@ -71,18 +66,17 @@ function renderMessage(isSessionUser, username) {
|
||||
<Fragment>
|
||||
<FullWidthRow>
|
||||
<h2 className='text-center' style={{ overflowWrap: 'break-word' }}>
|
||||
{username} has not made their portfolio public.
|
||||
{t('profile.username-not-public', { username: username })}
|
||||
</h2>
|
||||
</FullWidthRow>
|
||||
<FullWidthRow>
|
||||
<p className='alert alert-info'>
|
||||
{username} needs to change their privacy setting in order for you to
|
||||
view their portfolio.
|
||||
{t('profile.username-change-privacy', { username: username })}
|
||||
</p>
|
||||
</FullWidthRow>
|
||||
<Spacer />
|
||||
<FullWidthRow>
|
||||
<CurrentChallengeLink>Take me to the Challenges</CurrentChallengeLink>
|
||||
<CurrentChallengeLink>{t('buttons.take-me')}</CurrentChallengeLink>
|
||||
</FullWidthRow>
|
||||
<Spacer />
|
||||
</Fragment>
|
||||
@ -157,6 +151,7 @@ function renderProfile(user) {
|
||||
}
|
||||
|
||||
function Profile({ user, isSessionUser }) {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
profileUI: { isLocked = true },
|
||||
username
|
||||
@ -165,14 +160,14 @@ function Profile({ user, isSessionUser }) {
|
||||
return (
|
||||
<Fragment>
|
||||
<Helmet>
|
||||
<title>Profile | freeCodeCamp.org</title>
|
||||
<title>{t('buttons.profile')} | freeCodeCamp.org</title>
|
||||
</Helmet>
|
||||
<Spacer />
|
||||
<Grid>
|
||||
{isSessionUser ? (
|
||||
<FullWidthRow className='button-group'>
|
||||
<Link className='btn btn-lg btn-primary btn-block' to='/settings'>
|
||||
Update my account settings
|
||||
{t('buttons.update-settings')}
|
||||
</Link>
|
||||
<Button
|
||||
block={true}
|
||||
@ -181,17 +176,17 @@ function Profile({ user, isSessionUser }) {
|
||||
className='btn-invert'
|
||||
href={`${apiLocation}/signout`}
|
||||
>
|
||||
Sign me out of freeCodeCamp
|
||||
{t('buttons.sign-me-out')}
|
||||
</Button>
|
||||
</FullWidthRow>
|
||||
) : null}
|
||||
<Spacer />
|
||||
{isLocked ? renderMessage(isSessionUser, username) : null}
|
||||
{isLocked ? renderMessage(isSessionUser, username, t) : null}
|
||||
{!isLocked || isSessionUser ? renderProfile(user) : null}
|
||||
{isSessionUser ? null : (
|
||||
<Row className='text-center'>
|
||||
<Link to={`/user/${username}/report-user`}>
|
||||
Flag This User's Account for Abuse
|
||||
{t('buttons.flag-user')}
|
||||
</Link>
|
||||
</Row>
|
||||
)}
|
||||
|
@ -59,7 +59,7 @@ describe('<Profile/>', () => {
|
||||
it('renders the settings button on your own profile', () => {
|
||||
const { getByText } = render(<Profile {...myProfileProps} />);
|
||||
|
||||
expect(getByText('Update my account settings')).toHaveAttribute(
|
||||
expect(getByText('buttons.update-settings')).toHaveAttribute(
|
||||
'href',
|
||||
'/settings'
|
||||
);
|
||||
@ -68,7 +68,7 @@ describe('<Profile/>', () => {
|
||||
it('renders the report button on another persons profile', () => {
|
||||
const { getByText } = render(<Profile {...notMyProfileProps} />);
|
||||
|
||||
expect(getByText("Flag This User's Account for Abuse")).toHaveAttribute(
|
||||
expect(getByText('buttons.flag-user')).toHaveAttribute(
|
||||
'href',
|
||||
'/user/string/report-user'
|
||||
);
|
||||
|
@ -24,7 +24,7 @@ exports[`<Profile/> renders correctly 1`] = `
|
||||
class="avatar-container default-border"
|
||||
>
|
||||
<img
|
||||
alt="string's avatar"
|
||||
alt="profile.avatar"
|
||||
class="avatar img-responsive"
|
||||
src="string"
|
||||
/>
|
||||
@ -38,7 +38,7 @@ exports[`<Profile/> renders correctly 1`] = `
|
||||
class="text-center social-media-icons col-sm-6 col-sm-offset-3"
|
||||
>
|
||||
<a
|
||||
aria-label="Link to string's LinkedIn"
|
||||
aria-label="aria.linkedin"
|
||||
href="string"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
@ -60,7 +60,7 @@ exports[`<Profile/> renders correctly 1`] = `
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
aria-label="Link to string's Github"
|
||||
aria-label="aria.github"
|
||||
href="string"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
@ -82,7 +82,7 @@ exports[`<Profile/> renders correctly 1`] = `
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
aria-label="Link to string's website"
|
||||
aria-label="aria.website"
|
||||
href="string"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
@ -104,7 +104,7 @@ exports[`<Profile/> renders correctly 1`] = `
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
aria-label="Link to string's Twitter"
|
||||
aria-label="aria.twitter"
|
||||
href="string"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
@ -146,7 +146,7 @@ exports[`<Profile/> renders correctly 1`] = `
|
||||
<a
|
||||
href="/user/string/report-user"
|
||||
>
|
||||
Flag This User's Account for Abuse
|
||||
buttons.flag-user
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
|
@ -7,14 +7,19 @@ import {
|
||||
faHeart,
|
||||
faCalendar
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { AvatarRenderer } from '../../helpers';
|
||||
|
||||
import SocialIcons from './SocialIcons';
|
||||
import Link from '../../helpers/Link';
|
||||
|
||||
import './camper.css';
|
||||
|
||||
import { langCodes } from '../../../../i18n/allLangs';
|
||||
import { clientLocale } from '../../../../config/env';
|
||||
|
||||
const localeCode = langCodes[clientLocale];
|
||||
|
||||
const propTypes = {
|
||||
about: PropTypes.string,
|
||||
githubProfile: PropTypes.string,
|
||||
@ -35,15 +40,11 @@ const propTypes = {
|
||||
yearsTopContributor: PropTypes.array
|
||||
};
|
||||
|
||||
function pluralise(word, condition) {
|
||||
return condition ? word + 's' : word;
|
||||
}
|
||||
|
||||
function joinArray(array) {
|
||||
function joinArray(array, t) {
|
||||
return array.reduce((string, item, index, array) => {
|
||||
if (string.length > 0) {
|
||||
if (index === array.length - 1) {
|
||||
return `${string} and ${item}`;
|
||||
return `${string} ${t('misc.and')} ${item}`;
|
||||
} else {
|
||||
return `${string}, ${item}`;
|
||||
}
|
||||
@ -53,11 +54,13 @@ function joinArray(array) {
|
||||
});
|
||||
}
|
||||
|
||||
function parseDate(joinDate) {
|
||||
function parseDate(joinDate, t) {
|
||||
joinDate = new Date(joinDate);
|
||||
const year = joinDate.getFullYear();
|
||||
const month = joinDate.toLocaleString('en-US', { month: 'long' });
|
||||
return `Joined ${month} ${year}`;
|
||||
const date = joinDate.toLocaleString([localeCode, 'en-US'], {
|
||||
year: 'numeric',
|
||||
month: 'long'
|
||||
});
|
||||
return t('profile.joined', { date: date });
|
||||
}
|
||||
|
||||
function Camper({
|
||||
@ -79,6 +82,8 @@ function Camper({
|
||||
twitter,
|
||||
website
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Row>
|
||||
@ -108,13 +113,13 @@ function Camper({
|
||||
{location && <p className='text-center location'>{location}</p>}
|
||||
{isDonating && (
|
||||
<p className='text-center supporter'>
|
||||
<FontAwesomeIcon icon={faHeart} /> Supporter
|
||||
<FontAwesomeIcon icon={faHeart} /> {t('profile.supporter')}
|
||||
</p>
|
||||
)}
|
||||
{about && <p className='bio text-center'>{about}</p>}
|
||||
{joinDate && (
|
||||
<p className='bio text-center'>
|
||||
<FontAwesomeIcon icon={faCalendar} /> {parseDate(joinDate)}
|
||||
<FontAwesomeIcon icon={faCalendar} /> {parseDate(joinDate, t)}
|
||||
</p>
|
||||
)}
|
||||
{yearsTopContributor.filter(Boolean).length > 0 && (
|
||||
@ -122,15 +127,15 @@ function Camper({
|
||||
<br />
|
||||
<p className='text-center yearsTopContributor'>
|
||||
<FontAwesomeIcon icon={faAward} />{' '}
|
||||
<Link to={'/top-contributors'}>Top Contributor</Link>
|
||||
<Link to={'/top-contributors'}>{t('profile.contributor')}</Link>
|
||||
</p>
|
||||
<p className='text-center'>{joinArray(yearsTopContributor)}</p>
|
||||
<p className='text-center'>{joinArray(yearsTopContributor, t)}</p>
|
||||
</div>
|
||||
)}
|
||||
<br />
|
||||
{typeof points === 'number' ? (
|
||||
<p className='text-center points'>
|
||||
{`${points} ${pluralise('total point', points !== 1)}`}
|
||||
{t('profile.total-points', { count: points })}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
@ -4,6 +4,7 @@ import { curry } from 'lodash';
|
||||
import { createSelector } from 'reselect';
|
||||
import { connect } from 'react-redux';
|
||||
import { Row, Col } from '@freecodecamp/react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { certificatesByNameSelector } from '../../../redux';
|
||||
import { ButtonSpacer, FullWidthRow, Link, Spacer } from '../../helpers';
|
||||
@ -62,22 +63,21 @@ function Certificates({
|
||||
hasModernCert,
|
||||
username
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const renderCertShowWithUsername = curry(renderCertShow)(username);
|
||||
return (
|
||||
<FullWidthRow className='certifications'>
|
||||
<h2 className='text-center'>freeCodeCamp Certifications</h2>
|
||||
<h2 className='text-center'>{t('profile.fcc-certs')}</h2>
|
||||
<br />
|
||||
{hasModernCert ? (
|
||||
currentCerts.map(renderCertShowWithUsername)
|
||||
) : (
|
||||
<p className='text-center'>
|
||||
No certifications have been earned under the current curriculum
|
||||
</p>
|
||||
<p className='text-center'>{t('profile.no-certs')}</p>
|
||||
)}
|
||||
{hasLegacyCert ? (
|
||||
<div>
|
||||
<br />
|
||||
<h3 className='text-center'>Legacy Certifications</h3>
|
||||
<h3 className='text-center'>{t('settings.headings.legacy-certs')}</h3>
|
||||
<br />
|
||||
{legacyCerts.map(renderCertShowWithUsername)}
|
||||
<Spacer size={2} />
|
||||
|
@ -7,6 +7,7 @@ import addDays from 'date-fns/addDays';
|
||||
import addMonths from 'date-fns/addMonths';
|
||||
import startOfDay from 'date-fns/startOfDay';
|
||||
import isEqual from 'date-fns/isEqual';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import FullWidthRow from '../../helpers/FullWidthRow';
|
||||
import Spacer from '../../helpers/Spacer';
|
||||
@ -14,6 +15,11 @@ import Spacer from '../../helpers/Spacer';
|
||||
import '@freecodecamp/react-calendar-heatmap/dist/styles.css';
|
||||
import './heatmap.css';
|
||||
|
||||
import { langCodes } from '../../../../i18n/allLangs';
|
||||
import { clientLocale } from '../../../../config/env';
|
||||
|
||||
const localeCode = langCodes[clientLocale];
|
||||
|
||||
const propTypes = {
|
||||
calendar: PropTypes.object
|
||||
};
|
||||
@ -23,7 +29,8 @@ const innerPropTypes = {
|
||||
currentStreak: PropTypes.number,
|
||||
longestStreak: PropTypes.number,
|
||||
pages: PropTypes.array,
|
||||
points: PropTypes.number
|
||||
points: PropTypes.number,
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
class HeatMapInner extends Component {
|
||||
@ -57,12 +64,12 @@ class HeatMapInner extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { calendarData, currentStreak, longestStreak, pages } = this.props;
|
||||
const { calendarData, currentStreak, longestStreak, pages, t } = this.props;
|
||||
const { startOfCalendar, endOfCalendar } = pages[this.state.pageIndex];
|
||||
const title = `${startOfCalendar.toLocaleDateString('en-US', {
|
||||
const title = `${startOfCalendar.toLocaleDateString([localeCode, 'en-US'], {
|
||||
year: 'numeric',
|
||||
month: 'short'
|
||||
})} - ${endOfCalendar.toLocaleDateString('en-US', {
|
||||
})} - ${endOfCalendar.toLocaleDateString([localeCode, 'en-US'], {
|
||||
year: 'numeric',
|
||||
month: 'short'
|
||||
})}`;
|
||||
@ -108,24 +115,22 @@ class HeatMapInner extends Component {
|
||||
endDate={endOfCalendar}
|
||||
startDate={startOfCalendar}
|
||||
tooltipDataAttrs={value => {
|
||||
let valueCount;
|
||||
if (value && value.count === 1) {
|
||||
valueCount = '1 point';
|
||||
} else if (value && value.count > 1) {
|
||||
valueCount = `${value.count} points`;
|
||||
} else {
|
||||
valueCount = 'No points';
|
||||
}
|
||||
const dateFormatted = value.date
|
||||
? 'on ' +
|
||||
value.date.toLocaleDateString('en-US', {
|
||||
const dateFormatted =
|
||||
value && value.date
|
||||
? value.date.toLocaleDateString([localeCode, 'en-US'], {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})
|
||||
: '';
|
||||
return {
|
||||
'data-tip': `<b>${valueCount}</b> ${dateFormatted}`
|
||||
'data-tip':
|
||||
value && value.count > -1
|
||||
? t('profile.points', {
|
||||
count: value.count,
|
||||
date: dateFormatted
|
||||
})
|
||||
: ''
|
||||
};
|
||||
}}
|
||||
values={dataToDisplay}
|
||||
@ -136,10 +141,10 @@ class HeatMapInner extends Component {
|
||||
<Row>
|
||||
<div className='streak-container'>
|
||||
<span className='streak' data-testid='longest-streak'>
|
||||
<b>Longest Streak:</b> {longestStreak || 0}
|
||||
<b>{t('profile.longest-streak')}</b> {longestStreak || 0}
|
||||
</span>
|
||||
<span className='streak' data-testid='current-streak'>
|
||||
<b>Current Streak:</b> {currentStreak || 0}
|
||||
<b>{t('profile.current-streak')}</b> {currentStreak || 0}
|
||||
</span>
|
||||
</div>
|
||||
</Row>
|
||||
@ -152,6 +157,7 @@ class HeatMapInner extends Component {
|
||||
HeatMapInner.propTypes = innerPropTypes;
|
||||
|
||||
const HeatMap = props => {
|
||||
const { t } = useTranslation();
|
||||
const { calendar } = props;
|
||||
|
||||
/**
|
||||
@ -244,6 +250,7 @@ const HeatMap = props => {
|
||||
currentStreak={currentStreak}
|
||||
longestStreak={longestStreak}
|
||||
pages={pages}
|
||||
t={t}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -44,14 +44,14 @@ describe('<HeatMap/>', () => {
|
||||
it('calculates the correct longest streak', () => {
|
||||
const { getByTestId } = render(<HeatMap {...props} />);
|
||||
expect(getByTestId('longest-streak').textContent).toContain(
|
||||
'Longest Streak: 2'
|
||||
'profile.longest-streak'
|
||||
);
|
||||
});
|
||||
|
||||
it('calculates the correct current streak', () => {
|
||||
const { getByTestId } = render(<HeatMap {...props} />);
|
||||
expect(getByTestId('current-streak').textContent).toContain(
|
||||
'Current Streak: 1'
|
||||
'profile.current-streak'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Media } from '@freecodecamp/react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { FullWidthRow } from '../../helpers';
|
||||
|
||||
@ -19,18 +20,19 @@ const propTypes = {
|
||||
};
|
||||
|
||||
function Portfolio({ portfolio = [] }) {
|
||||
const { t } = useTranslation();
|
||||
if (!portfolio.length) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<FullWidthRow>
|
||||
<h2 className='text-center'>Portfolio</h2>
|
||||
<h2 className='text-center'>{t('profile.portfolio')}</h2>
|
||||
{portfolio.map(({ title, url, image, description, id }) => (
|
||||
<Media key={id}>
|
||||
<Media.Left align='middle'>
|
||||
{image && (
|
||||
<img
|
||||
alt={`A screen shot of ${title}`}
|
||||
alt={t('profile.screen-shot', { title: title })}
|
||||
className='portfolio-screen-shot'
|
||||
src={image}
|
||||
/>
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
faTwitter
|
||||
} from '@fortawesome/free-brands-svg-icons';
|
||||
import { faLink } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import './social-icons.css';
|
||||
|
||||
const propTypes = {
|
||||
@ -26,9 +26,10 @@ const propTypes = {
|
||||
};
|
||||
|
||||
function LinkedInIcon(linkedIn, username) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<a
|
||||
aria-label={`Link to ${username}'s LinkedIn`}
|
||||
aria-label={t('aria.linkedin', { username: username })}
|
||||
href={linkedIn}
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
@ -39,9 +40,10 @@ function LinkedInIcon(linkedIn, username) {
|
||||
}
|
||||
|
||||
function GithubIcon(ghURL, username) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<a
|
||||
aria-label={`Link to ${username}'s Github`}
|
||||
aria-label={t('aria.github', { username: username })}
|
||||
href={ghURL}
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
@ -52,9 +54,10 @@ function GithubIcon(ghURL, username) {
|
||||
}
|
||||
|
||||
function WebsiteIcon(website, username) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<a
|
||||
aria-label={`Link to ${username}'s website`}
|
||||
aria-label={t('aria.website', { username: username })}
|
||||
href={website}
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
@ -65,9 +68,10 @@ function WebsiteIcon(website, username) {
|
||||
}
|
||||
|
||||
function TwitterIcon(handle, username) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<a
|
||||
aria-label={`Link to ${username}'s Twitter`}
|
||||
aria-label={t('aria.twitter', { username: username })}
|
||||
href={handle}
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React, { Component, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import format from 'date-fns/format';
|
||||
import { reverse, sortBy } from 'lodash';
|
||||
import {
|
||||
Button,
|
||||
@ -10,6 +9,7 @@ import {
|
||||
MenuItem
|
||||
} from '@freecodecamp/react-bootstrap';
|
||||
import { useStaticQuery, graphql } from 'gatsby';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
|
||||
import './timeline.css';
|
||||
import TimelinePagination from './TimelinePagination';
|
||||
@ -24,6 +24,11 @@ import {
|
||||
import { maybeUrlRE } from '../../../utils';
|
||||
import CertificationIcon from '../../../assets/icons/CertificationIcon';
|
||||
|
||||
import { langCodes } from '../../../../i18n/allLangs';
|
||||
import { clientLocale } from '../../../../config/env';
|
||||
|
||||
const localeCode = langCodes[clientLocale];
|
||||
|
||||
// Items per page in timeline.
|
||||
const ITEMS_PER_PAGE = 15;
|
||||
|
||||
@ -42,6 +47,7 @@ const propTypes = {
|
||||
)
|
||||
})
|
||||
),
|
||||
t: PropTypes.func.isRequired,
|
||||
username: PropTypes.string
|
||||
};
|
||||
|
||||
@ -91,6 +97,7 @@ class TimelineInner extends Component {
|
||||
}
|
||||
|
||||
renderViewButton(id, files, githubLink, solution) {
|
||||
const { t } = this.props;
|
||||
if (files && files.length) {
|
||||
return (
|
||||
<Button
|
||||
@ -100,7 +107,7 @@ class TimelineInner extends Component {
|
||||
id={`btn-for-${id}`}
|
||||
onClick={() => this.viewSolution(id, solution, files)}
|
||||
>
|
||||
Show Code
|
||||
{t('buttons.show-code')}
|
||||
</Button>
|
||||
);
|
||||
} else if (githubLink) {
|
||||
@ -119,7 +126,7 @@ class TimelineInner extends Component {
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
Front End
|
||||
{t('buttons.frontend')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
bsStyle='primary'
|
||||
@ -127,7 +134,7 @@ class TimelineInner extends Component {
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
Back End
|
||||
{t('buttons.backend')}
|
||||
</MenuItem>
|
||||
</DropdownButton>
|
||||
</div>
|
||||
@ -143,7 +150,7 @@ class TimelineInner extends Component {
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
View
|
||||
{t('buttons.view')}
|
||||
</Button>
|
||||
);
|
||||
} else {
|
||||
@ -175,7 +182,11 @@ class TimelineInner extends Component {
|
||||
<td>{this.renderViewButton(id, files, githubLink, solution)}</td>
|
||||
<td className='text-center'>
|
||||
<time dateTime={completedDate.toISOString()}>
|
||||
{format(completedDate, 'MMMM d, y')}
|
||||
{completedDate.toLocaleString([localeCode, 'en-US'], {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})}
|
||||
</time>
|
||||
</td>
|
||||
</tr>
|
||||
@ -228,6 +239,7 @@ class TimelineInner extends Component {
|
||||
idToNameMap,
|
||||
username,
|
||||
sortedTimeline,
|
||||
t,
|
||||
totalPages = 1
|
||||
} = this.props;
|
||||
const { solutionToView: id, solutionOpen, pageNo = 1 } = this.state;
|
||||
@ -236,19 +248,19 @@ class TimelineInner extends Component {
|
||||
|
||||
return (
|
||||
<FullWidthRow>
|
||||
<h2 className='text-center'>Timeline</h2>
|
||||
<h2 className='text-center'>{t('profile.timeline')}</h2>
|
||||
{completedMap.length === 0 ? (
|
||||
<p className='text-center'>
|
||||
No challenges have been completed yet.
|
||||
<Link to='/learn'>Get started here.</Link>
|
||||
{t('profile.none-completed')}
|
||||
<Link to='/learn'>{t('profile.get-started')}</Link>
|
||||
</p>
|
||||
) : (
|
||||
<Table condensed={true} striped={true}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Challenge</th>
|
||||
<th>Solution</th>
|
||||
<th className='text-center'>Completed</th>
|
||||
<th>{t('profile.challenge')}</th>
|
||||
<th>{t('settings.labels.solution')}</th>
|
||||
<th className='text-center'>{t('profile.completed')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -278,7 +290,7 @@ class TimelineInner extends Component {
|
||||
/>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button onClick={this.closeSolution}>Close</Button>
|
||||
<Button onClick={this.closeSolution}>{t('buttons.close')}</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
)}
|
||||
@ -357,4 +369,4 @@ Timeline.propTypes = propTypes;
|
||||
|
||||
Timeline.displayName = 'Timeline';
|
||||
|
||||
export default Timeline;
|
||||
export default withTranslation()(Timeline);
|
||||
|
@ -72,7 +72,7 @@ describe('<TimeLine />', () => {
|
||||
it('rendering the correct button when files is present', () => {
|
||||
const { getByText } = render(<TimeLine {...propsForOnlySolution} />);
|
||||
|
||||
const button = getByText('Show Code');
|
||||
const button = getByText('buttons.show-code');
|
||||
expect(button).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
@ -1,8 +1,12 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const TimelinePagination = props => {
|
||||
const { pageNo, totalPages, firstPage, prevPage, nextPage, lastPage } = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<nav aria-label='Timeline Pagination' role='navigation'>
|
||||
<ul aria-hidden='true' className='timeline-pagination_list'>
|
||||
@ -14,7 +18,7 @@ const TimelinePagination = props => {
|
||||
}}
|
||||
>
|
||||
<button
|
||||
aria-label='Go to First page'
|
||||
aria-label={t('aria.first-page')}
|
||||
disabled={pageNo === 1}
|
||||
onClick={firstPage}
|
||||
>
|
||||
@ -29,7 +33,7 @@ const TimelinePagination = props => {
|
||||
}}
|
||||
>
|
||||
<button
|
||||
aria-label='Go to Previous page'
|
||||
aria-label={t('aria.previous-page')}
|
||||
disabled={pageNo === 1}
|
||||
onClick={prevPage}
|
||||
>
|
||||
@ -37,7 +41,10 @@ const TimelinePagination = props => {
|
||||
</button>
|
||||
</li>
|
||||
<li className='timeline-pagination_list_item'>
|
||||
{pageNo} of {totalPages}
|
||||
{t('profile.page-number', {
|
||||
pageNumber: pageNo,
|
||||
totalPages: totalPages
|
||||
})}
|
||||
</li>
|
||||
<li
|
||||
className='timeline-pagination_list_item'
|
||||
@ -46,7 +53,7 @@ const TimelinePagination = props => {
|
||||
}}
|
||||
>
|
||||
<button
|
||||
aria-label='Go to Next page'
|
||||
aria-label={t('aria.next-page')}
|
||||
disabled={pageNo === totalPages}
|
||||
onClick={nextPage}
|
||||
>
|
||||
@ -61,7 +68,7 @@ const TimelinePagination = props => {
|
||||
}}
|
||||
>
|
||||
<button
|
||||
aria-label='Go to Last page'
|
||||
aria-label={t('aria.last-page')}
|
||||
disabled={pageNo === totalPages}
|
||||
onClick={lastPage}
|
||||
>
|
||||
|
@ -6,6 +6,7 @@ import { createSelector } from 'reselect';
|
||||
import { SearchBox } from 'react-instantsearch-dom';
|
||||
import { HotKeys, ObserveKeys } from 'react-hotkeys';
|
||||
import { isEqual } from 'lodash';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
|
||||
import {
|
||||
isSearchDropdownEnabledSelector,
|
||||
@ -23,6 +24,7 @@ const propTypes = {
|
||||
innerRef: PropTypes.object,
|
||||
isDropdownEnabled: PropTypes.bool,
|
||||
isSearchFocused: PropTypes.bool,
|
||||
t: PropTypes.func.isRequired,
|
||||
toggleSearchDropdown: PropTypes.func.isRequired,
|
||||
toggleSearchFocused: PropTypes.func.isRequired,
|
||||
updateSearchQuery: PropTypes.func.isRequired
|
||||
@ -43,8 +45,6 @@ const mapDispatchToProps = dispatch =>
|
||||
dispatch
|
||||
);
|
||||
|
||||
const placeholder = 'Search 6,000+ tutorials';
|
||||
|
||||
export class SearchBar extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@ -173,15 +173,16 @@ export class SearchBar extends Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { isDropdownEnabled, isSearchFocused, innerRef } = this.props;
|
||||
const { isDropdownEnabled, isSearchFocused, innerRef, t } = this.props;
|
||||
const { index } = this.state;
|
||||
const placeholder = t('search.placeholder');
|
||||
|
||||
return (
|
||||
<div className='fcc_searchBar' data-testid='fcc_searchBar' ref={innerRef}>
|
||||
<HotKeys handlers={this.keyHandlers} keyMap={this.keyMap}>
|
||||
<div className='fcc_search_wrapper'>
|
||||
<label className='fcc_sr_only' htmlFor='fcc_instantsearch'>
|
||||
Search
|
||||
{t('search.label')}
|
||||
</label>
|
||||
<ObserveKeys except={['Space']}>
|
||||
<SearchBox
|
||||
@ -214,4 +215,4 @@ SearchBar.propTypes = propTypes;
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(SearchBar);
|
||||
)(withTranslation()(SearchBar));
|
||||
|
@ -2,6 +2,8 @@ import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connectStateResults, connectHits } from 'react-instantsearch-dom';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import Suggestion from './SearchSuggestion';
|
||||
import NoHitsSuggestion from './NoHitsSuggestion';
|
||||
|
||||
@ -14,8 +16,9 @@ const CustomHits = connectHits(
|
||||
selectedIndex,
|
||||
handleHits
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const noHits = isEmpty(hits);
|
||||
const noHitsTitle = 'No tutorials found';
|
||||
const noHitsTitle = t('search.no-tutorials');
|
||||
const footer = [
|
||||
{
|
||||
objectID: `footer-${searchQuery}`,
|
||||
@ -25,15 +28,12 @@ const CustomHits = connectHits(
|
||||
: `https://www.freecodecamp.org/news/search/?query=${encodeURIComponent(
|
||||
searchQuery
|
||||
)}`,
|
||||
title: noHits ? noHitsTitle : `See all results for ${searchQuery}`,
|
||||
title: t('search.see-results', { searchQuery: searchQuery }),
|
||||
_highlightResult: {
|
||||
query: {
|
||||
value: noHits
|
||||
? noHitsTitle
|
||||
: `
|
||||
value: `
|
||||
<ais-highlight-0000000000>
|
||||
See all results for
|
||||
${searchQuery}
|
||||
${t('search.see-results', { searchQuery: searchQuery })}
|
||||
</ais-highlight-0000000000>
|
||||
`
|
||||
}
|
||||
|
@ -1,13 +1,12 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import './empty-search.css';
|
||||
|
||||
function EmptySearch() {
|
||||
return (
|
||||
<div className='empty-search-wrapper'>
|
||||
Looking for something? Try the search bar on this page.
|
||||
</div>
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return <div className='empty-search-wrapper'>{t('search.try')}</div>;
|
||||
}
|
||||
|
||||
EmptySearch.displayName = 'EmptySearch';
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
const propTypes = {
|
||||
query: PropTypes.string
|
||||
@ -9,7 +10,9 @@ function NoResults({ query }) {
|
||||
return (
|
||||
<div className='no-results-wrapper'>
|
||||
<p>
|
||||
We could not find anything relating to <em>{query}</em>
|
||||
<Trans i18nKey='search.no-results' query={query}>
|
||||
<em>{{ query }}</em>
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
@ -10,6 +10,7 @@ import { FullWidthRow, Spacer } from '../helpers';
|
||||
import ThemeSettings from './Theme';
|
||||
import UsernameSettings from './Username';
|
||||
import BlockSaveButton from '../helpers/form/BlockSaveButton';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
|
||||
const propTypes = {
|
||||
about: PropTypes.string,
|
||||
@ -19,6 +20,7 @@ const propTypes = {
|
||||
picture: PropTypes.string,
|
||||
points: PropTypes.number,
|
||||
submitNewAbout: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
toggleNightMode: PropTypes.func.isRequired,
|
||||
username: PropTypes.string
|
||||
};
|
||||
@ -125,7 +127,7 @@ class AboutSettings extends Component {
|
||||
const {
|
||||
formValues: { name, location, picture, about }
|
||||
} = this.state;
|
||||
const { currentTheme, username, toggleNightMode } = this.props;
|
||||
const { currentTheme, username, t, toggleNightMode } = this.props;
|
||||
return (
|
||||
<div className='about-settings'>
|
||||
<UsernameSettings username={username} />
|
||||
@ -134,7 +136,7 @@ class AboutSettings extends Component {
|
||||
<form id='camper-identity' onSubmit={this.handleSubmit}>
|
||||
<FormGroup controlId='about-name'>
|
||||
<ControlLabel>
|
||||
<strong>Name</strong>
|
||||
<strong>{t('settings.labels.name')}</strong>
|
||||
</ControlLabel>
|
||||
<FormControl
|
||||
onChange={this.handleNameChange}
|
||||
@ -144,7 +146,7 @@ class AboutSettings extends Component {
|
||||
</FormGroup>
|
||||
<FormGroup controlId='about-location'>
|
||||
<ControlLabel>
|
||||
<strong>Location</strong>
|
||||
<strong>{t('settings.labels.location')}</strong>
|
||||
</ControlLabel>
|
||||
<FormControl
|
||||
onChange={this.handleLocationChange}
|
||||
@ -154,7 +156,7 @@ class AboutSettings extends Component {
|
||||
</FormGroup>
|
||||
<FormGroup controlId='about-picture'>
|
||||
<ControlLabel>
|
||||
<strong>Picture</strong>
|
||||
<strong>{t('settings.labels.picture')}</strong>
|
||||
</ControlLabel>
|
||||
<FormControl
|
||||
onChange={this.handlePictureChange}
|
||||
@ -165,7 +167,7 @@ class AboutSettings extends Component {
|
||||
</FormGroup>
|
||||
<FormGroup controlId='about-about'>
|
||||
<ControlLabel>
|
||||
<strong>About</strong>
|
||||
<strong>{t('settings.labels.about')}</strong>
|
||||
</ControlLabel>
|
||||
<FormControl
|
||||
componentClass='textarea'
|
||||
@ -191,4 +193,4 @@ class AboutSettings extends Component {
|
||||
AboutSettings.displayName = 'AboutSettings';
|
||||
AboutSettings.propTypes = propTypes;
|
||||
|
||||
export default AboutSettings;
|
||||
export default withTranslation()(AboutSettings);
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
} from '@freecodecamp/react-bootstrap';
|
||||
import { Link, navigate } from 'gatsby';
|
||||
import { createSelector } from 'reselect';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
|
||||
import {
|
||||
projectMap,
|
||||
@ -60,6 +61,7 @@ const propTypes = {
|
||||
isQaCertV7: PropTypes.bool,
|
||||
isRespWebDesignCert: PropTypes.bool,
|
||||
isSciCompPyCertV7: PropTypes.bool,
|
||||
t: PropTypes.func.isRequired,
|
||||
updateLegacyCert: PropTypes.func.isRequired,
|
||||
username: PropTypes.string,
|
||||
verifyCert: PropTypes.func.isRequired
|
||||
@ -138,9 +140,8 @@ const isCertMapSelector = createSelector(
|
||||
|
||||
const honestyInfoMessage = {
|
||||
type: 'info',
|
||||
message:
|
||||
'To claim a certification, you must first accept our academic ' +
|
||||
'honesty policy'
|
||||
message: 'flash.msg-1',
|
||||
needsTranslating: true
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
@ -170,7 +171,7 @@ export class CertificationSettings extends Component {
|
||||
getUserIsCertMap = () => isCertMapSelector(this.props);
|
||||
|
||||
getProjectSolution = (projectId, projectTitle) => {
|
||||
const { completedChallenges } = this.props;
|
||||
const { completedChallenges, t } = this.props;
|
||||
const completedProject = find(
|
||||
completedChallenges,
|
||||
({ id }) => projectId === id
|
||||
@ -197,7 +198,7 @@ export class CertificationSettings extends Component {
|
||||
id={`btn-for-${projectId}`}
|
||||
onClick={onClickHandler}
|
||||
>
|
||||
Show Code
|
||||
{t('buttons.show-code')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@ -217,7 +218,7 @@ export class CertificationSettings extends Component {
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
Front End
|
||||
{t('buttons.frontend')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
bsStyle='primary'
|
||||
@ -225,7 +226,7 @@ export class CertificationSettings extends Component {
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
Back End
|
||||
{t('buttons.backend')}
|
||||
</MenuItem>
|
||||
</DropdownButton>
|
||||
</div>
|
||||
@ -242,7 +243,7 @@ export class CertificationSettings extends Component {
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
Show Solution
|
||||
{t('buttons.show-solution')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@ -254,31 +255,42 @@ export class CertificationSettings extends Component {
|
||||
id={`btn-for-${projectId}`}
|
||||
onClick={onClickHandler}
|
||||
>
|
||||
Show Code
|
||||
{t('buttons.show-code')}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
renderCertifications = certName => (
|
||||
renderCertifications = certName => {
|
||||
const { t } = this.props;
|
||||
return (
|
||||
<FullWidthRow key={certName}>
|
||||
<Spacer />
|
||||
<h3 className='text-center'>{certName}</h3>
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Project Name</th>
|
||||
<th>Solution</th>
|
||||
<th>{t('settings.labels.project-name')}</th>
|
||||
<th>{t('settings.labels.solution')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{this.renderProjectsFor(certName, this.getUserIsCertMap()[certName])}
|
||||
{this.renderProjectsFor(
|
||||
certName,
|
||||
this.getUserIsCertMap()[certName]
|
||||
)}
|
||||
</tbody>
|
||||
</Table>
|
||||
</FullWidthRow>
|
||||
);
|
||||
|
||||
};
|
||||
renderProjectsFor = (certName, isCert) => {
|
||||
const { username, isHonest, createFlashMessage, verifyCert } = this.props;
|
||||
const {
|
||||
username,
|
||||
isHonest,
|
||||
createFlashMessage,
|
||||
t,
|
||||
verifyCert
|
||||
} = this.props;
|
||||
const { superBlock } = first(projectMap[certName]);
|
||||
const certLocation = `/certification/${username}/${superBlock}`;
|
||||
const createClickHandler = superBlock => e => {
|
||||
@ -310,7 +322,7 @@ export class CertificationSettings extends Component {
|
||||
href={certLocation}
|
||||
onClick={createClickHandler(superBlock)}
|
||||
>
|
||||
{isCert ? 'Show Certification' : 'Claim Certification'}
|
||||
{isCert ? t('buttons.show-cert') : t('buttons.claim-cert')}
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
@ -391,7 +403,7 @@ export class CertificationSettings extends Component {
|
||||
}
|
||||
|
||||
renderLegacyCertifications = certName => {
|
||||
const { username, createFlashMessage, completedChallenges } = this.props;
|
||||
const { username, createFlashMessage, completedChallenges, t } = this.props;
|
||||
const { superBlock } = first(legacyProjectMap[certName]);
|
||||
const certLocation = `/certification/${username}/${superBlock}`;
|
||||
const challengeTitles = legacyProjectMap[certName].map(item => item.title);
|
||||
@ -442,7 +454,9 @@ export class CertificationSettings extends Component {
|
||||
<Spacer />
|
||||
<h3 className='text-center'>{certName}</h3>
|
||||
<Form
|
||||
buttonText={fullForm ? 'Claim Certification' : 'Save Progress'}
|
||||
buttonText={
|
||||
fullForm ? t('buttons.claim-cert') : t('buttons.save-progress')
|
||||
}
|
||||
enableSubmit={fullForm}
|
||||
formFields={formFields}
|
||||
hideButton={isCertClaimed}
|
||||
@ -465,7 +479,7 @@ export class CertificationSettings extends Component {
|
||||
style={buttonStyle}
|
||||
target='_blank'
|
||||
>
|
||||
Show Certification
|
||||
{t('buttons.show-cert')}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
@ -485,7 +499,8 @@ export class CertificationSettings extends Component {
|
||||
isFrontEndLibsCert,
|
||||
isInfosecQaCert,
|
||||
isJsAlgoDataStructCert,
|
||||
isRespWebDesignCert
|
||||
isRespWebDesignCert,
|
||||
t
|
||||
} = this.props;
|
||||
|
||||
const fullStackClaimable =
|
||||
@ -522,8 +537,9 @@ export class CertificationSettings extends Component {
|
||||
<h3 className='text-center'>Legacy Full Stack Certification</h3>
|
||||
<div>
|
||||
<p>
|
||||
Once you've earned the following freeCodeCamp certifications, you'll
|
||||
be able to claim the Legacy Full Stack Developer Certification:
|
||||
{t('settings.claim-legacy', {
|
||||
cert: 'Legacy Full Stack Certification'
|
||||
})}
|
||||
</p>
|
||||
<ul>
|
||||
<li>Responsive Web Design</li>
|
||||
@ -547,7 +563,9 @@ export class CertificationSettings extends Component {
|
||||
style={buttonStyle}
|
||||
target='_blank'
|
||||
>
|
||||
{isFullStackCert ? 'Show Certification' : 'Claim Certification'}
|
||||
{isFullStackCert
|
||||
? t('buttons.show-cert')
|
||||
: t('buttons.claim-cert')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
@ -559,7 +577,7 @@ export class CertificationSettings extends Component {
|
||||
style={buttonStyle}
|
||||
target='_blank'
|
||||
>
|
||||
Claim Certification
|
||||
{t('buttons.claim-cert')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@ -572,11 +590,13 @@ export class CertificationSettings extends Component {
|
||||
const {
|
||||
solutionViewer: { files, solution, isOpen, projectTitle }
|
||||
} = this.state;
|
||||
|
||||
const { t } = this.props;
|
||||
return (
|
||||
<section id='certification-settings'>
|
||||
<SectionHeader>Certifications</SectionHeader>
|
||||
<SectionHeader>{t('settings.headings.certs')}</SectionHeader>
|
||||
{certifications.map(this.renderCertifications)}
|
||||
<SectionHeader>Legacy Certifications</SectionHeader>
|
||||
<SectionHeader>{t('settings.headings.legacy-certs')}</SectionHeader>
|
||||
{this.renderLegacyFullStack()}
|
||||
{legacyCertifications.map(this.renderLegacyCertifications)}
|
||||
{isOpen ? (
|
||||
@ -588,14 +608,18 @@ export class CertificationSettings extends Component {
|
||||
>
|
||||
<Modal.Header className='this-one?' closeButton={true}>
|
||||
<Modal.Title id='solution-viewer-modal-title'>
|
||||
Solution for {projectTitle}
|
||||
{t('settings.labels.solution-for', {
|
||||
projectTitle: projectTitle
|
||||
})}
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<SolutionViewer files={files} solution={solution} />
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button onClick={this.handleSolutionModalHide}>Close</Button>
|
||||
<Button onClick={this.handleSolutionModalHide}>
|
||||
{t('buttons.close')}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
) : null}
|
||||
@ -610,4 +634,4 @@ CertificationSettings.propTypes = propTypes;
|
||||
export default connect(
|
||||
null,
|
||||
mapDispatchToProps
|
||||
)(CertificationSettings);
|
||||
)(withTranslation()(CertificationSettings));
|
||||
|
@ -21,7 +21,7 @@ describe('<certification />', () => {
|
||||
|
||||
expect(
|
||||
container.querySelector('#button-legacy-data-visualization')
|
||||
).toHaveTextContent('Show Certification');
|
||||
).toHaveTextContent('buttons.show-cert');
|
||||
});
|
||||
|
||||
it('Should link show cert button to the claimed legacy cert', () => {
|
||||
@ -95,7 +95,7 @@ describe('<certification />', () => {
|
||||
<CertificationSettings {...propsForOnlySolution} />
|
||||
);
|
||||
|
||||
const button = getByText('Show Code');
|
||||
const button = getByText('buttons.show-code');
|
||||
expect(button).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import { Button, Panel } from '@freecodecamp/react-bootstrap';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
|
||||
import { FullWidthRow, ButtonSpacer, Spacer } from '../helpers';
|
||||
import { deleteAccount, resetProgress } from '../../redux/settings';
|
||||
@ -13,7 +14,8 @@ import './danger-zone.css';
|
||||
|
||||
const propTypes = {
|
||||
deleteAccount: PropTypes.func.isRequired,
|
||||
resetProgress: PropTypes.func.isRequired
|
||||
resetProgress: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = () => ({});
|
||||
@ -50,14 +52,14 @@ class DangerZone extends Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { deleteAccount, resetProgress } = this.props;
|
||||
const { deleteAccount, resetProgress, t } = this.props;
|
||||
return (
|
||||
<div className='danger-zone text-center'>
|
||||
<FullWidthRow>
|
||||
<Panel bsStyle='danger'>
|
||||
<Panel.Heading>Danger Zone</Panel.Heading>
|
||||
<Panel.Heading>{t('settings.danger.heading')}</Panel.Heading>
|
||||
<Spacer />
|
||||
<p>Please be careful. Changes in this section are permanent.</p>
|
||||
<p>{t('settings.danger.be-careful')}</p>
|
||||
<FullWidthRow>
|
||||
<Button
|
||||
block={true}
|
||||
@ -67,7 +69,7 @@ class DangerZone extends Component {
|
||||
onClick={() => this.toggleResetModal()}
|
||||
type='button'
|
||||
>
|
||||
Reset all of my progress
|
||||
{t('settings.danger.reset')}
|
||||
</Button>
|
||||
<ButtonSpacer />
|
||||
<Button
|
||||
@ -78,7 +80,7 @@ class DangerZone extends Component {
|
||||
onClick={() => this.toggleDeleteModal()}
|
||||
type='button'
|
||||
>
|
||||
Delete my account
|
||||
{t('settings.danger.delete')}
|
||||
</Button>
|
||||
<Spacer />
|
||||
</FullWidthRow>
|
||||
@ -106,4 +108,4 @@ DangerZone.propTypes = propTypes;
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(DangerZone);
|
||||
)(withTranslation()(DangerZone));
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
import { ButtonSpacer } from '../helpers';
|
||||
import { Button, Modal } from '@freecodecamp/react-bootstrap';
|
||||
@ -14,6 +15,8 @@ const propTypes = {
|
||||
|
||||
function DeleteModal(props) {
|
||||
const { show, onHide } = props;
|
||||
const email = 'team@freecodecamp.org';
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Modal
|
||||
aria-labelledby='modal-title'
|
||||
@ -25,23 +28,19 @@ function DeleteModal(props) {
|
||||
show={show}
|
||||
>
|
||||
<Modal.Header closeButton={true}>
|
||||
<Modal.Title id='modal-title'>Delete My Account</Modal.Title>
|
||||
<Modal.Title id='modal-title'>
|
||||
{t('settings.danger.delete')}
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<p>{t('settings.danger.delete-p1')}</p>
|
||||
<p>{t('settings.danger.delete-p2')}</p>
|
||||
<p>
|
||||
This will really delete all your data, including all your progress and
|
||||
account information.
|
||||
</p>
|
||||
<p>
|
||||
We won't be able to recover any of it for you later, even if you
|
||||
change your mind.
|
||||
</p>
|
||||
<p>
|
||||
If there's something we could do better, send us an email instead and
|
||||
we'll do our best:  
|
||||
<a href='mailto:team@freecodecamp.org' title='team@freecodecamp.org'>
|
||||
team@freecodecamp.org
|
||||
<Trans email={email} i18nKey='settings.danger.delete-p3'>
|
||||
<a href={`mailto:${email}`} title={email}>
|
||||
{{ email }}
|
||||
</a>
|
||||
</Trans>
|
||||
</p>
|
||||
<hr />
|
||||
<Button
|
||||
@ -52,7 +51,7 @@ function DeleteModal(props) {
|
||||
onClick={props.onHide}
|
||||
type='button'
|
||||
>
|
||||
Nevermind, I don't want to delete my account
|
||||
{t('settings.danger.nevermind')}
|
||||
</Button>
|
||||
<ButtonSpacer />
|
||||
<Button
|
||||
@ -63,11 +62,11 @@ function DeleteModal(props) {
|
||||
onClick={props.delete}
|
||||
type='button'
|
||||
>
|
||||
I am 100% certain. Delete everything related to this account
|
||||
{t('settings.danger.certain')}
|
||||
</Button>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button onClick={props.onHide}>Close</Button>
|
||||
<Button onClick={props.onHide}>{t('buttons.close')}</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
Button
|
||||
} from '@freecodecamp/react-bootstrap';
|
||||
import isEmail from 'validator/lib/isEmail';
|
||||
import { Trans, withTranslation } from 'react-i18next';
|
||||
|
||||
import { updateMyEmail } from '../../redux/settings';
|
||||
import { maybeEmailRE } from '../../utils';
|
||||
@ -30,15 +31,17 @@ const propTypes = {
|
||||
email: PropTypes.string,
|
||||
isEmailVerified: PropTypes.bool,
|
||||
sendQuincyEmail: PropTypes.bool,
|
||||
t: PropTypes.func.isRequired,
|
||||
updateMyEmail: PropTypes.func.isRequired,
|
||||
updateQuincyEmail: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export function UpdateEmailButton() {
|
||||
const { t } = this.props;
|
||||
return (
|
||||
<Link style={{ textDecoration: 'none' }} to='/update-email'>
|
||||
<Button block={true} bsSize='lg' bsStyle='primary'>
|
||||
Edit
|
||||
{t('buttons.edit')}
|
||||
</Button>
|
||||
</Link>
|
||||
);
|
||||
@ -83,6 +86,7 @@ class EmailSettings extends Component {
|
||||
const {
|
||||
emailForm: { newEmail, currentEmail }
|
||||
} = this.state;
|
||||
const { t } = this.props;
|
||||
|
||||
if (!maybeEmailRE.test(newEmail)) {
|
||||
return {
|
||||
@ -93,7 +97,7 @@ class EmailSettings extends Component {
|
||||
if (newEmail === currentEmail) {
|
||||
return {
|
||||
state: 'error',
|
||||
message: 'This email is the same as your current email'
|
||||
message: t('validation.msg-2')
|
||||
};
|
||||
}
|
||||
if (isEmail(newEmail)) {
|
||||
@ -101,9 +105,7 @@ class EmailSettings extends Component {
|
||||
} else {
|
||||
return {
|
||||
state: 'error',
|
||||
message:
|
||||
'We could not validate your email correctly, ' +
|
||||
'please ensure it is correct'
|
||||
message: t('validation.msg-3')
|
||||
};
|
||||
}
|
||||
};
|
||||
@ -112,6 +114,7 @@ class EmailSettings extends Component {
|
||||
const {
|
||||
emailForm: { confirmNewEmail, newEmail }
|
||||
} = this.state;
|
||||
const { t } = this.props;
|
||||
|
||||
if (!maybeEmailRE.test(newEmail)) {
|
||||
return {
|
||||
@ -123,7 +126,7 @@ class EmailSettings extends Component {
|
||||
if (maybeEmailRE.test(confirmNewEmail)) {
|
||||
return {
|
||||
state: isMatch ? 'success' : 'error',
|
||||
message: isMatch ? '' : 'Both new email addresses must be the same'
|
||||
message: isMatch ? '' : t('validation.msg-4')
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
@ -137,7 +140,13 @@ class EmailSettings extends Component {
|
||||
const {
|
||||
emailForm: { newEmail, confirmNewEmail, currentEmail, isPristine }
|
||||
} = this.state;
|
||||
const { isEmailVerified, updateQuincyEmail, sendQuincyEmail } = this.props;
|
||||
|
||||
const {
|
||||
isEmailVerified,
|
||||
updateQuincyEmail,
|
||||
sendQuincyEmail,
|
||||
t
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
state: newEmailValidation,
|
||||
@ -148,13 +157,12 @@ class EmailSettings extends Component {
|
||||
state: confirmEmailValidation,
|
||||
message: confirmEmailValidationMessage
|
||||
} = this.getValidationForConfirmEmail();
|
||||
|
||||
if (!currentEmail) {
|
||||
return (
|
||||
<div>
|
||||
<FullWidthRow>
|
||||
<p className='large-p text-center'>
|
||||
You do not have an email associated with this account.
|
||||
</p>
|
||||
<p className='large-p text-center'>{t('settings.email.missing')}</p>
|
||||
</FullWidthRow>
|
||||
<FullWidthRow>
|
||||
<UpdateEmailButton />
|
||||
@ -164,18 +172,16 @@ class EmailSettings extends Component {
|
||||
}
|
||||
return (
|
||||
<div className='email-settings'>
|
||||
<SectionHeader>Email Settings</SectionHeader>
|
||||
<SectionHeader>{t('settings.email.heading')}</SectionHeader>
|
||||
{isEmailVerified ? null : (
|
||||
<FullWidthRow>
|
||||
<HelpBlock>
|
||||
<Alert bsStyle='info' className='text-center'>
|
||||
Your email has not been verified.
|
||||
{t('settings.email.not-verified')}
|
||||
<br />
|
||||
Please check your email, or{' '}
|
||||
<Link to='/update-email'>
|
||||
request a new verification email here
|
||||
</Link>
|
||||
.
|
||||
<Trans i18nKey='settings.email.check'>
|
||||
<Link to='/update-email' />
|
||||
</Trans>
|
||||
</Alert>
|
||||
</HelpBlock>
|
||||
</FullWidthRow>
|
||||
@ -183,14 +189,14 @@ class EmailSettings extends Component {
|
||||
<FullWidthRow>
|
||||
<form id='form-update-email' onSubmit={this.handleSubmit}>
|
||||
<FormGroup controlId='current-email'>
|
||||
<ControlLabel>Current Email</ControlLabel>
|
||||
<ControlLabel>{t('settings.email.current')}</ControlLabel>
|
||||
<FormControl.Static>{currentEmail}</FormControl.Static>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
controlId='new-email'
|
||||
validationState={newEmailValidation}
|
||||
>
|
||||
<ControlLabel>New Email</ControlLabel>
|
||||
<ControlLabel>{t('settings.email.new')}</ControlLabel>
|
||||
<FormControl
|
||||
onChange={this.createHandleEmailFormChange('newEmail')}
|
||||
type='email'
|
||||
@ -204,7 +210,7 @@ class EmailSettings extends Component {
|
||||
controlId='confirm-email'
|
||||
validationState={confirmEmailValidation}
|
||||
>
|
||||
<ControlLabel>Confirm New Email</ControlLabel>
|
||||
<ControlLabel>{t('settings.email.confirm')}</ControlLabel>
|
||||
<FormControl
|
||||
onChange={this.createHandleEmailFormChange('confirmNewEmail')}
|
||||
type='email'
|
||||
@ -227,11 +233,11 @@ class EmailSettings extends Component {
|
||||
<FullWidthRow>
|
||||
<form id='form-quincy-email' onSubmit={this.handleSubmit}>
|
||||
<ToggleSetting
|
||||
action="Send me Quincy's weekly email"
|
||||
action={t('settings.email.weekly')}
|
||||
flag={sendQuincyEmail}
|
||||
flagName='sendQuincyEmail'
|
||||
offLabel='No thanks'
|
||||
onLabel='Yes please'
|
||||
offLabel={t('buttons.no-thanks')}
|
||||
onLabel={t('buttons.yes-please')}
|
||||
toggleFlag={() => updateQuincyEmail(!sendQuincyEmail)}
|
||||
/>
|
||||
</form>
|
||||
@ -247,4 +253,4 @@ EmailSettings.propTypes = propTypes;
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(EmailSettings);
|
||||
)(withTranslation()(EmailSettings));
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Panel } from '@freecodecamp/react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { FullWidthRow } from '../helpers';
|
||||
import SectionHeader from './SectionHeader';
|
||||
@ -14,6 +15,7 @@ const propTypes = {
|
||||
};
|
||||
|
||||
const Honesty = ({ isHonest, updateIsHonest }) => {
|
||||
const { t } = useTranslation();
|
||||
const button = isHonest ? (
|
||||
<Button
|
||||
block={true}
|
||||
@ -21,7 +23,7 @@ const Honesty = ({ isHonest, updateIsHonest }) => {
|
||||
className='disabled-agreed'
|
||||
disabled={true}
|
||||
>
|
||||
<p>You have accepted our Academic Honesty Policy.</p>
|
||||
<p>{t('buttons.accepted-honesty')}</p>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
@ -29,12 +31,12 @@ const Honesty = ({ isHonest, updateIsHonest }) => {
|
||||
bsStyle='primary'
|
||||
onClick={() => updateIsHonest({ isHonest: true })}
|
||||
>
|
||||
Agree
|
||||
{t('buttons.agree')}
|
||||
</Button>
|
||||
);
|
||||
return (
|
||||
<section className='honesty-policy'>
|
||||
<SectionHeader>Academic Honesty Policy</SectionHeader>
|
||||
<SectionHeader>{t('settings.headings.honesty')}</SectionHeader>
|
||||
<FullWidthRow>
|
||||
<Panel className='honesty-panel'>
|
||||
<HonestyPolicy />
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
ControlLabel
|
||||
} from '@freecodecamp/react-bootstrap';
|
||||
import isURL from 'validator/lib/isURL';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
|
||||
import { maybeUrlRE } from '../../utils';
|
||||
|
||||
@ -19,6 +20,7 @@ import BlockSaveButton from '../helpers/form/BlockSaveButton';
|
||||
const propTypes = {
|
||||
githubProfile: PropTypes.string,
|
||||
linkedin: PropTypes.string,
|
||||
t: PropTypes.func.isRequired,
|
||||
twitter: PropTypes.string,
|
||||
updateInternetSettings: PropTypes.func.isRequired,
|
||||
website: PropTypes.string
|
||||
@ -65,6 +67,7 @@ class InternetSettings extends Component {
|
||||
}
|
||||
|
||||
getValidationStateFor(maybeURl = '') {
|
||||
const { t } = this.props;
|
||||
if (!maybeURl || !maybeUrlRE.test(maybeURl)) {
|
||||
return {
|
||||
state: null,
|
||||
@ -79,9 +82,7 @@ class InternetSettings extends Component {
|
||||
}
|
||||
return {
|
||||
state: 'error',
|
||||
message:
|
||||
'We could not validate your URL correctly, ' +
|
||||
'please ensure it is correct'
|
||||
message: t('validation.msg-8')
|
||||
};
|
||||
}
|
||||
|
||||
@ -154,6 +155,7 @@ class InternetSettings extends Component {
|
||||
) : null;
|
||||
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
const {
|
||||
formValues: { githubProfile, linkedin, twitter, website }
|
||||
} = this.state;
|
||||
@ -180,7 +182,7 @@ class InternetSettings extends Component {
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<SectionHeader>Your Internet Presence</SectionHeader>
|
||||
<SectionHeader>{t('settings.headings.internet')}</SectionHeader>
|
||||
<FullWidthRow>
|
||||
<form id='internet-presence' onSubmit={this.handleSubmit}>
|
||||
<FormGroup
|
||||
@ -229,7 +231,7 @@ class InternetSettings extends Component {
|
||||
controlId='internet-website'
|
||||
validationState={websiteValidation}
|
||||
>
|
||||
<ControlLabel>Personal Website</ControlLabel>
|
||||
<ControlLabel>{t('settings.labels.personal')}</ControlLabel>
|
||||
<FormControl
|
||||
onChange={this.createHandleChange('website')}
|
||||
placeholder='https://example.com'
|
||||
@ -252,4 +254,4 @@ class InternetSettings extends Component {
|
||||
InternetSettings.displayName = 'InternetSettings';
|
||||
InternetSettings.propTypes = propTypes;
|
||||
|
||||
export default InternetSettings;
|
||||
export default withTranslation()(InternetSettings);
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
} from '@freecodecamp/react-bootstrap';
|
||||
import { findIndex, find, isEqual } from 'lodash';
|
||||
import isURL from 'validator/lib/isURL';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
|
||||
import { hasProtocolRE } from '../../utils';
|
||||
|
||||
@ -27,6 +28,7 @@ const propTypes = {
|
||||
url: PropTypes.string
|
||||
})
|
||||
),
|
||||
t: PropTypes.func.isRequired,
|
||||
updatePortfolio: PropTypes.func.isRequired,
|
||||
username: PropTypes.string
|
||||
};
|
||||
@ -130,21 +132,19 @@ class PortfolioSettings extends Component {
|
||||
};
|
||||
|
||||
getDescriptionValidation(description) {
|
||||
const { t } = this.props;
|
||||
const len = description.length;
|
||||
const charsLeft = 288 - len;
|
||||
if (charsLeft < 0) {
|
||||
return {
|
||||
state: 'error',
|
||||
message: 'There is a maximum limit of 288 characters, you have 0 left'
|
||||
message: t('validation.msg-1', { charsLeft: 0 })
|
||||
};
|
||||
}
|
||||
if (charsLeft < 41 && charsLeft > 0) {
|
||||
return {
|
||||
state: 'warning',
|
||||
message:
|
||||
'There is a maximum limit of 288 characters, you have ' +
|
||||
charsLeft +
|
||||
' left'
|
||||
message: t('validation.msg-1', { charsLeft: charsLeft })
|
||||
};
|
||||
}
|
||||
if (charsLeft === 288) {
|
||||
@ -154,23 +154,25 @@ class PortfolioSettings extends Component {
|
||||
}
|
||||
|
||||
getTitleValidation(title) {
|
||||
const { t } = this.props;
|
||||
if (!title) {
|
||||
return { state: 'error', message: 'A title is required' };
|
||||
return { state: 'error', message: t('validation.msg-5') };
|
||||
}
|
||||
const len = title.length;
|
||||
if (len < 2) {
|
||||
return { state: 'error', message: 'Title is too short' };
|
||||
return { state: 'error', message: t('validation.msg-6') };
|
||||
}
|
||||
if (len > 144) {
|
||||
return { state: 'error', message: 'Title is too long' };
|
||||
return { state: 'error', message: t('validation.msg-7') };
|
||||
}
|
||||
return { state: 'success', message: '' };
|
||||
}
|
||||
|
||||
getUrlValidation(maybeUrl, isImage) {
|
||||
const { t } = this.props;
|
||||
const len = maybeUrl.length;
|
||||
if (len >= 4 && !hasProtocolRE.test(maybeUrl)) {
|
||||
return { state: 'error', message: 'URL must start with http or https' };
|
||||
return { state: 'error', message: t('validation.msg-9') };
|
||||
}
|
||||
if (isImage && !maybeUrl) {
|
||||
return { state: null, message: '' };
|
||||
@ -178,15 +180,16 @@ class PortfolioSettings extends Component {
|
||||
if (isImage && !/\.(png|jpg|jpeg|gif)$/.test(maybeUrl)) {
|
||||
return {
|
||||
state: 'error',
|
||||
message: 'URL must link directly to an image file'
|
||||
message: t('validation.msg-10')
|
||||
};
|
||||
}
|
||||
return isURL(maybeUrl)
|
||||
? { state: 'success', message: '' }
|
||||
: { state: 'warning', message: 'Please use a valid URL' };
|
||||
: { state: 'warning', message: t('validation.msg-11') };
|
||||
}
|
||||
|
||||
renderPortfolio = (portfolio, index, arr) => {
|
||||
const { t } = this.props;
|
||||
const { id, title, description, url, image } = portfolio;
|
||||
const pristine = this.isFormPristine(id);
|
||||
const {
|
||||
@ -213,7 +216,7 @@ class PortfolioSettings extends Component {
|
||||
pristine || (!pristine && !title) ? null : titleState
|
||||
}
|
||||
>
|
||||
<ControlLabel>Title</ControlLabel>
|
||||
<ControlLabel>{t('settings.labels.title')}</ControlLabel>
|
||||
<FormControl
|
||||
onChange={this.createOnChangeHandler(id, 'title')}
|
||||
required={true}
|
||||
@ -228,7 +231,7 @@ class PortfolioSettings extends Component {
|
||||
pristine || (!pristine && !url) ? null : urlState
|
||||
}
|
||||
>
|
||||
<ControlLabel>URL</ControlLabel>
|
||||
<ControlLabel>{t('settings.labels.url')}</ControlLabel>
|
||||
<FormControl
|
||||
onChange={this.createOnChangeHandler(id, 'url')}
|
||||
required={true}
|
||||
@ -241,7 +244,7 @@ class PortfolioSettings extends Component {
|
||||
controlId={`${id}-image`}
|
||||
validationState={pristine ? null : imageState}
|
||||
>
|
||||
<ControlLabel>Image</ControlLabel>
|
||||
<ControlLabel>{t('settings.labels.image')}</ControlLabel>
|
||||
<FormControl
|
||||
onChange={this.createOnChangeHandler(id, 'image')}
|
||||
type='url'
|
||||
@ -253,7 +256,7 @@ class PortfolioSettings extends Component {
|
||||
controlId={`${id}-description`}
|
||||
validationState={pristine ? null : descriptionState}
|
||||
>
|
||||
<ControlLabel>Description</ControlLabel>
|
||||
<ControlLabel>{t('settings.labels.description')}</ControlLabel>
|
||||
<FormControl
|
||||
componentClass='textarea'
|
||||
onChange={this.createOnChangeHandler(id, 'description')}
|
||||
@ -276,7 +279,7 @@ class PortfolioSettings extends Component {
|
||||
})
|
||||
}
|
||||
>
|
||||
Save this portfolio item
|
||||
{t('buttons.save-portfolio')}
|
||||
</BlockSaveButton>
|
||||
<ButtonSpacer />
|
||||
<Button
|
||||
@ -286,7 +289,7 @@ class PortfolioSettings extends Component {
|
||||
onClick={() => this.handleRemoveItem(id)}
|
||||
type='button'
|
||||
>
|
||||
Remove this portfolio item
|
||||
{t('buttons.remove-portfolio')}
|
||||
</Button>
|
||||
</form>
|
||||
{index + 1 !== arr.length && (
|
||||
@ -302,16 +305,14 @@ class PortfolioSettings extends Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
const { portfolio = [] } = this.state;
|
||||
return (
|
||||
<section id='portfolio-settings'>
|
||||
<SectionHeader>Portfolio Settings</SectionHeader>
|
||||
<SectionHeader>{t('settings.headings.portfolio')}</SectionHeader>
|
||||
<FullWidthRow>
|
||||
<div className='portfolio-settings-intro'>
|
||||
<p className='p-intro'>
|
||||
Share your non-freeCodeCamp projects, articles or accepted pull
|
||||
requests.
|
||||
</p>
|
||||
<p className='p-intro'>{t('settings.share-projects')}</p>
|
||||
</div>
|
||||
</FullWidthRow>
|
||||
<FullWidthRow>
|
||||
@ -323,7 +324,7 @@ class PortfolioSettings extends Component {
|
||||
onClick={this.handleAdd}
|
||||
type='button'
|
||||
>
|
||||
Add a new portfolio Item
|
||||
{t('buttons.add-portfolio')}
|
||||
</Button>
|
||||
</FullWidthRow>
|
||||
<Spacer size={2} />
|
||||
@ -336,4 +337,4 @@ class PortfolioSettings extends Component {
|
||||
PortfolioSettings.displayName = 'PortfolioSettings';
|
||||
PortfolioSettings.propTypes = propTypes;
|
||||
|
||||
export default PortfolioSettings;
|
||||
export default withTranslation()(PortfolioSettings);
|
||||
|
@ -4,6 +4,7 @@ import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { Button, Form } from '@freecodecamp/react-bootstrap';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
|
||||
import { userSelector } from '../../redux';
|
||||
import { submitProfileUI } from '../../redux/settings';
|
||||
@ -25,6 +26,7 @@ const mapDispatchToProps = dispatch =>
|
||||
|
||||
const propTypes = {
|
||||
submitProfileUI: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
user: PropTypes.shape({
|
||||
profileUI: PropTypes.shape({
|
||||
isLocked: PropTypes.bool,
|
||||
@ -52,7 +54,7 @@ class PrivacySettings extends Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { user } = this.props;
|
||||
const { t, user } = this.props;
|
||||
const {
|
||||
isLocked = true,
|
||||
showAbout = false,
|
||||
@ -68,104 +70,98 @@ class PrivacySettings extends Component {
|
||||
|
||||
return (
|
||||
<div className='privacy-settings'>
|
||||
<SectionHeader>Privacy Settings</SectionHeader>
|
||||
<SectionHeader>{t('settings.headings.privacy')}</SectionHeader>
|
||||
<FullWidthRow>
|
||||
<p>
|
||||
The settings in this section enable you to control what is shown on
|
||||
your freeCodeCamp public portfolio.
|
||||
</p>
|
||||
<p>{t('settings.privacy')}</p>
|
||||
<Form inline={true} onSubmit={this.handleSubmit}>
|
||||
<ToggleSetting
|
||||
action='My profile'
|
||||
explain='Your certifications will be disabled, if set to private.'
|
||||
action={t('settings.labels.my-profile')}
|
||||
explain={t('settings.disabled')}
|
||||
flag={isLocked}
|
||||
flagName='isLocked'
|
||||
offLabel='Public'
|
||||
onLabel='Private'
|
||||
offLabel={t('buttons.public')}
|
||||
onLabel={t('buttons.private')}
|
||||
toggleFlag={this.toggleFlag('isLocked')}
|
||||
/>
|
||||
<ToggleSetting
|
||||
action='My name'
|
||||
action={t('settings.labels.my-name')}
|
||||
flag={!showName}
|
||||
flagName='name'
|
||||
offLabel='Public'
|
||||
onLabel='Private'
|
||||
offLabel={t('buttons.public')}
|
||||
onLabel={t('buttons.private')}
|
||||
toggleFlag={this.toggleFlag('showName')}
|
||||
/>
|
||||
<ToggleSetting
|
||||
action='My location'
|
||||
action={t('settings.labels.my-location')}
|
||||
flag={!showLocation}
|
||||
flagName='showLocation'
|
||||
offLabel='Public'
|
||||
onLabel='Private'
|
||||
offLabel={t('buttons.public')}
|
||||
onLabel={t('buttons.private')}
|
||||
toggleFlag={this.toggleFlag('showLocation')}
|
||||
/>
|
||||
<ToggleSetting
|
||||
action='My "about me"'
|
||||
action={t('settings.labels.my-about')}
|
||||
flag={!showAbout}
|
||||
flagName='showAbout'
|
||||
offLabel='Public'
|
||||
onLabel='Private'
|
||||
offLabel={t('buttons.public')}
|
||||
onLabel={t('buttons.private')}
|
||||
toggleFlag={this.toggleFlag('showAbout')}
|
||||
/>
|
||||
<ToggleSetting
|
||||
action='My points'
|
||||
action={t('settings.labels.my-points')}
|
||||
flag={!showPoints}
|
||||
flagName='showPoints'
|
||||
offLabel='Public'
|
||||
onLabel='Private'
|
||||
offLabel={t('buttons.public')}
|
||||
onLabel={t('buttons.private')}
|
||||
toggleFlag={this.toggleFlag('showPoints')}
|
||||
/>
|
||||
<ToggleSetting
|
||||
action='My heat map'
|
||||
action={t('settings.labels.my-heatmap')}
|
||||
flag={!showHeatMap}
|
||||
flagName='showHeatMap'
|
||||
offLabel='Public'
|
||||
onLabel='Private'
|
||||
offLabel={t('buttons.public')}
|
||||
onLabel={t('buttons.private')}
|
||||
toggleFlag={this.toggleFlag('showHeatMap')}
|
||||
/>
|
||||
<ToggleSetting
|
||||
action='My certifications'
|
||||
explain='Your certifications will be disabled, if set to private.'
|
||||
action={t('settings.labels.my-certs')}
|
||||
explain={t('settings.disabled')}
|
||||
flag={!showCerts}
|
||||
flagName='showCerts'
|
||||
offLabel='Public'
|
||||
onLabel='Private'
|
||||
offLabel={t('buttons.public')}
|
||||
onLabel={t('buttons.private')}
|
||||
toggleFlag={this.toggleFlag('showCerts')}
|
||||
/>
|
||||
<ToggleSetting
|
||||
action='My portfolio'
|
||||
action={t('settings.labels.my-portfolio')}
|
||||
flag={!showPortfolio}
|
||||
flagName='showPortfolio'
|
||||
offLabel='Public'
|
||||
onLabel='Private'
|
||||
offLabel={t('buttons.public')}
|
||||
onLabel={t('buttons.private')}
|
||||
toggleFlag={this.toggleFlag('showPortfolio')}
|
||||
/>
|
||||
<ToggleSetting
|
||||
action='My time line'
|
||||
explain='Your certifications will be disabled, if set to private.'
|
||||
action={t('settings.labels.my-timeline')}
|
||||
explain={t('settings.disabled')}
|
||||
flag={!showTimeLine}
|
||||
flagName='showTimeLine'
|
||||
offLabel='Public'
|
||||
onLabel='Private'
|
||||
offLabel={t('buttons.public')}
|
||||
onLabel={t('buttons.private')}
|
||||
toggleFlag={this.toggleFlag('showTimeLine')}
|
||||
/>
|
||||
<ToggleSetting
|
||||
action='My donations'
|
||||
action={t('settings.labels.my-donations')}
|
||||
flag={!showDonation}
|
||||
flagName='showPortfolio'
|
||||
offLabel='Public'
|
||||
onLabel='Private'
|
||||
offLabel={t('buttons.public')}
|
||||
onLabel={t('buttons.private')}
|
||||
toggleFlag={this.toggleFlag('showDonation')}
|
||||
/>
|
||||
</Form>
|
||||
</FullWidthRow>
|
||||
<FullWidthRow>
|
||||
<Spacer />
|
||||
<p>
|
||||
To see what data we hold on your account, click the 'Download your
|
||||
data' button below
|
||||
</p>
|
||||
<p>{t('settings.data')}</p>
|
||||
<Button
|
||||
block={true}
|
||||
bsSize='lg'
|
||||
@ -175,7 +171,7 @@ class PrivacySettings extends Component {
|
||||
JSON.stringify(user)
|
||||
)}`}
|
||||
>
|
||||
Download your data
|
||||
{t('buttons.download-data')}
|
||||
</Button>
|
||||
</FullWidthRow>
|
||||
</div>
|
||||
@ -189,4 +185,4 @@ PrivacySettings.propTypes = propTypes;
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(PrivacySettings);
|
||||
)(withTranslation()(PrivacySettings));
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { ButtonSpacer } from '../helpers';
|
||||
import { Button, Modal } from '@freecodecamp/react-bootstrap';
|
||||
@ -11,7 +12,9 @@ const propTypes = {
|
||||
};
|
||||
|
||||
function ResetModal(props) {
|
||||
const { t } = useTranslation();
|
||||
const { show, onHide } = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
aria-labelledby='modal-title'
|
||||
@ -23,18 +26,13 @@ function ResetModal(props) {
|
||||
show={show}
|
||||
>
|
||||
<Modal.Header closeButton={true}>
|
||||
<Modal.Title id='modal-title'>Reset My Progress</Modal.Title>
|
||||
<Modal.Title id='modal-title'>
|
||||
{t('settings.danger.reset-heading')}
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<p>
|
||||
This will really delete all of your progress, points, completed
|
||||
challenges, our records of your projects, any certifications you have,
|
||||
everything.
|
||||
</p>
|
||||
<p>
|
||||
We won't be able to recover any of it for you later, even if you
|
||||
change your mind.
|
||||
</p>
|
||||
<p>{t('settings.danger.reset-p1')}</p>
|
||||
<p>{t('settings.danger.reset-p2')}</p>
|
||||
<hr />
|
||||
<Button
|
||||
block={true}
|
||||
@ -44,7 +42,7 @@ function ResetModal(props) {
|
||||
onClick={props.onHide}
|
||||
type='button'
|
||||
>
|
||||
Nevermind, I don't want to delete all of my progress
|
||||
{t('settings.danger.nevermind-2')}
|
||||
</Button>
|
||||
<ButtonSpacer />
|
||||
<Button
|
||||
@ -55,11 +53,11 @@ function ResetModal(props) {
|
||||
onClick={props.reset}
|
||||
type='button'
|
||||
>
|
||||
Reset everything. I want to start from the beginning
|
||||
{t('settings.danger.reset-confirm')}
|
||||
</Button>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button onClick={props.onHide}>Close</Button>
|
||||
<Button onClick={props.onHide}>{t('buttons.close')}</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
|
@ -1,6 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { Form } from '@freecodecamp/react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import ToggleSetting from './ToggleSetting';
|
||||
|
||||
@ -10,14 +11,16 @@ const propTypes = {
|
||||
};
|
||||
|
||||
export default function ThemeSettings({ currentTheme, toggleNightMode }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Form inline={true} onSubmit={e => e.preventDefault()}>
|
||||
<ToggleSetting
|
||||
action='Night Mode'
|
||||
action={t('settings.labels.night-mode')}
|
||||
flag={currentTheme === 'night'}
|
||||
flagName='currentTheme'
|
||||
offLabel='Off'
|
||||
onLabel='On'
|
||||
offLabel={t('buttons.off')}
|
||||
onLabel={t('buttons.on')}
|
||||
toggleFlag={() =>
|
||||
toggleNightMode(currentTheme === 'night' ? 'default' : 'night')
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user