diff --git a/.vscode/settings.json b/.vscode/settings.json index c72b809d0..5a762b3b8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,5 +12,23 @@ "eslint.format.enable": true, "eslint.packageManager": "yarn", "eslint.lintTask.enable": true, - "eslint.codeActionsOnSave.mode": "problems" + "eslint.codeActionsOnSave.mode": "problems", + "i18n-ally.localesPaths": [ + "public/locales" + ], + "i18n-ally.enabledFrameworks": [ + "react", + "i18next", + "general" + ], + "i18n-ally.namespace": true, + "i18n-ally.sourceLanguage": "en", + "i18n-ally.displayLanguage": "en", + "i18n-ally.sortKeys": true, + "i18n-ally.disablePathParsing": true, + "i18n-ally.pathMatcher": "{locale}.json", + "i18n-ally.extract.ignored": [ + "A role must have a valid emoji and name to be updated" + ], + "i18n-ally.keystyle": "flat" } diff --git a/README.md b/README.md index 2fb4b0526..7c76dd8f6 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,15 @@ For users that are not logged in can see public posts and groups via routes like `/groups/:groupSlug/:view/post/:postId` => `/post/:postId` `/public/post/:postId` => `/post/:postId` +### Translations +We use i18next to handle translations of platform strings (no translations of user content at this time). + +The `i18next-extract` config details in the `babel.config.js` file determines how all language keys are extracted and what languages are supported. Whenever babel runs, it will comb the entire repo for js/jsx files, and anytime it finds a t('example-key') function, it will pull every key into each of the translation files. It is disabled in the babel.config by default: when it runs, it generates keys for missing translations. It also sometimes overwrites newly added translations, causing a lot of finicky editing if its on by default. When you add a new translation, uncomment the config, restart webpack (yarn start) and it should pick up the key for every locale file. + +If using vscode, there is a really fantastic extension for working with i18n; [i18n-ally](https://github.com/lokalise/i18n-ally) +They have a 8 minute setup and feature [demo video](https://www.youtube.com/watch?v=kowM-MoGVns). + + ### Sockets Chat, in-app notifications and comments use sockets to update users in real-time. In the future, new posts will also use sockets to show up in real-time. diff --git a/babel.config.js b/babel.config.js index cb388ae31..4396e4120 100644 --- a/babel.config.js +++ b/babel.config.js @@ -39,6 +39,20 @@ module.exports = function (api) { extensions: ['.graphql'] } ], + /* + Disabling this by default: when it runs, it generates keys for missing translations + It also sometimes overwrites newly added translations, causing a lot of finicky editing. + When you add a new translation, uncomment the below config, restart webpack (yarn start) and it should pick up the key for every locale file + */ + // [ + // 'i18next-extract', + // { + // keySeparator: null, + // locales: ['es', 'en'], + // nsSeparator: null, + // outputPath: 'public/locales/{{locale}}.json' + // } + // ], 'import-graphql', 'inline-import', 'lodash' diff --git a/package.json b/package.json index 613e6fcba..8deb4ab22 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ }, "browserslist": [ "defaults" - ], + ], "proxy": "localhost:3001", "scripts": { "dev": "yarn start", @@ -49,11 +49,11 @@ "@babel/plugin-transform-runtime": "^7.17.0", "@babel/preset-env": "7.16.11", "@babel/preset-react": "^7.16.7", - "@deck.gl/core": "^8.7", - "@deck.gl/geo-layers": "^8.7", - "@deck.gl/layers": "^8.7", - "@deck.gl/mesh-layers": "^8.7", - "@deck.gl/react": "^8.7", + "@deck.gl/core": "8.8.27", + "@deck.gl/geo-layers": "8.8.27", + "@deck.gl/layers": "8.8.27", + "@deck.gl/mesh-layers": "8.8.27", + "@deck.gl/react": "8.8.27", "@emoji-mart/react": "^1.0.1", "@faker-js/faker": "^6.0.0-alpha.5", "@loaders.gl/core": "^3.2.0", @@ -64,13 +64,14 @@ "@testing-library/jest-dom": "^5.16.2", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0-beta.14", - "@tiptap/extension-highlight": "^2.0.0-beta.199", - "@tiptap/extension-link": "^2.0.0-beta.199", - "@tiptap/extension-mention": "^2.0.0-beta.199", - "@tiptap/extension-placeholder": "^2.0.0-beta.199", - "@tiptap/react": "^2.0.0-beta.199", - "@tiptap/starter-kit": "^2.0.0-beta.199", - "@tiptap/suggestion": "^2.0.0-beta.199", + "@tiptap/extension-highlight": "2.0.1", + "@tiptap/extension-link": "2.0.1", + "@tiptap/extension-mention": "2.0.1", + "@tiptap/extension-placeholder": "2.0.1", + "@tiptap/pm": "2.0.1", + "@tiptap/react": "2.0.1", + "@tiptap/starter-kit": "2.0.1", + "@tiptap/suggestion": "2.0.1", "@turf/area": "^6.0.1", "@turf/bbox": "^6.0.1", "@turf/bbox-polygon": "^6.0.1", @@ -89,6 +90,7 @@ "babel-jest": "^27.5.1", "babel-loader": "8.2.3", "babel-plugin-data-stylename": "0.1.1", + "babel-plugin-i18next-extract": "^0.9.0", "babel-plugin-import-graphql": "^2.8.1", "babel-plugin-inline-import": "^3.0.0", "babel-plugin-lodash": "^3.3.4", @@ -98,7 +100,7 @@ "babel-preset-react-app": "^10.0.1", "bfj": "6.1.1", "bootstrap": "4.0.0-alpha.6", - "browserslist": "^4.21.2", + "browserslist": "^4.21.5", "case-sensitive-paths-webpack-plugin": "2.2.0", "chalk": "2.3.0", "cheerio": "^1.0.0-rc.10", @@ -151,6 +153,9 @@ "http-proxy-middleware": "0.17.4", "husky": "^7.0.4", "hylo-shared": "5.2.2", + "i18next": "^22.0.5", + "i18next-browser-languagedetector": "^7.0.1", + "i18next-http-backend": "^2.0.1", "identity-obj-proxy": "3.0.0", "immutability-helper": "^3.1.1", "immutable": "~3.7.4", @@ -167,7 +172,7 @@ "json-loader": "0.5.7", "lodash": "~4.17.21", "lru-cache": "^4.1.1", - "mapbox-gl": "^2.7.1", + "mapbox-gl": "2.10.0", "mime": "^2.0.3", "mini-css-extract-plugin": "0.5.0", "mixpanel-browser": "^2.45.0", @@ -210,6 +215,7 @@ "react-dom": "^16.8.6", "react-flip-move": "^3.0.4", "react-helmet": "^6.1.0", + "react-i18next": "^12.0.0", "react-icons": "^4.4.0", "react-joyride": "^2.3.2", "react-map-gl": "^6", diff --git a/public/locales/en.json b/public/locales/en.json new file mode 100644 index 000000000..bdcd04699 --- /dev/null +++ b/public/locales/en.json @@ -0,0 +1,963 @@ +{ + " app to receive push notifications.": " app to receive push notifications.", + "(Remember to save your changes before leaving this form)": "(Remember to save your changes before leaving this form)", + "(explanation required)": "(explanation required)", + "+ Create Group": "+ Create Group", + "+ Create an event": "+ Create an event", + "+ New": "+ New", + "+ New post": "+ New post", + "+ New project": "+ New project", + "1 Group is a part of {{group.name}}": "1 Group is a part of {{group.name}}", + "1 other": "1 other", + "404 Not Found": "404 Not Found", + "A": "A", + "A role must have a valid emoji and name to be saved": "A role must have a valid emoji and name to be saved", + "A role must have a valid emoji and name to be updated": "A role must have a valid emoji and name to be updated", + "About": "About", + "About Me": "About Me", + "About Video URL": "About Video URL", + "About us": "About us", + "About {{name}}": "About {{name}}", + "Abusive": "Abusive", + "Accept Contributions": "Accept Contributions", + "Access": "Access", + "Access to your email address.": "Access to your email address.", + "Access to your phone number.": "Access to your phone number.", + "Access to your physical address.": "Access to your physical address.", + "Access your profile, including your name and image.": "Access your profile, including your name and image.", + "Account": "Account", + "Account already exists": "Account already exists", + "Active": "Active", + "Add": "Add", + "Add Affiliation": "Add Affiliation", + "Add File": "Add File", + "Add Member to Role": "Add Member to Role", + "Add New": "Add New", + "Add Preview": "Add Preview", + "Add Topic": "Add Topic", + "Add a Skill or Interest": "Add a Skill or Interest", + "Add a Topic": "Add a Topic", + "Add a comment.": "Add a comment.", + "Add a comment...": "Add a comment...", + "Add a description": "Add a description", + "Add a description, location, suggested topics and more in your group settings": "Add a description, location, suggested topics and more in your group settings", + "Add a donation link (must be valid URL)": "Add a donation link (must be valid URL)", + "Add a group description": "Add a group description", + "Add a new question": "Add a new question", + "Add a project management link (must be valid URL)": "Add a project management link (must be valid URL)", + "Add a relevant skill or interest": "Add a relevant skill or interest", + "Add a skill you want to learn": "Add a skill you want to learn", + "Add a suggested topic": "Add a suggested topic", + "Add custom links or filtered post views to your group's navigation": "Add custom links or filtered post views to your group's navigation", + "Add new affiliation": "Add new affiliation", + "Add someone": "Add someone", + "Add your location to see more relevant content, and find people and projects around you": "Add your location to see more relevant content, and find people and projects around you", + "Affiliations": "Affiliations", + "All": "All", + "All Groups": "All Groups", + "All My Groups": "All My Groups", + "All Posts": "All Posts", + "All Posts Marked Public": "All Posts Marked Public", + "All Topics": "All Topics", + "All topics": "All topics", + "Allow": "Allow", + "Almost done setting up your profile! Click the above profile icon to upload a custom profile image. Your profile image will be visible when you post or comment in groups.": "Almost done setting up your profile! Click the above profile icon to upload a custom profile image. Your profile image will be visible when you post or comment in groups.", + "Already Invited": "Already Invited", + "Already have an account?": "Already have an account?", + "Already logged in, redirecting...": "Already logged in, redirecting...", + "Amount": "Amount", + "An icon needs to be selected for the view.": "An icon needs to be selected for the view.", + "Animal welfare": "Animal welfare", + "Announcements": "Announcements", + "Anyone can join ": "Anyone can join", + "Anyone can join this group!": "Anyone can join this group!", + "Anyone on Hylo can see this post": "Anyone on Hylo can see this post", + "Anyone who can see this group can join it": "Anyone who can see this group can join it", + "Approve": "Approve", + "Are you sure you want to ": "Are you sure you want to ", + "Are you sure you want to cancel your request to join {{groupName}}?": "Are you sure you want to cancel your request to join {{groupName}}?", + "Are you sure you want to create a new join link? The current link won't work anymore if you do.": "Are you sure you want to create a new join link? The current link won't work anymore if you do.", + "Are you sure you want to decline the invitation to join {{groupName}}?": "Are you sure you want to decline the invitation to join {{groupName}}?", + "Are you sure you want to delete the group {{name}}?": "Are you sure you want to delete the group {{name}}?", + "Are you sure you want to delete this comment": "Are you sure you want to delete this comment", + "Are you sure you want to delete this custom view?": "Are you sure you want to delete this custom view?", + "Are you sure you want to delete this groupTopic?": "Are you sure you want to delete this groupTopic?", + "Are you sure you want to delete this post?": "Are you sure you want to delete this post?", + "Are you sure you want to delete this unsaved role/badge?": "Are you sure you want to delete this unsaved role/badge?", + "Are you sure you want to delete your affiliation as {{affiliation.role}} {{affiliation.preposition}} {{affiliation.orgName}}?": "Are you sure you want to delete your affiliation as {{affiliation.role}} {{affiliation.preposition}} {{affiliation.orgName}}?", + "Are you sure you want to leave {{groupName}}?": "Are you sure you want to leave {{groupName}}?", + "Are you sure you want to remove this comment?": "Are you sure you want to remove this comment?", + "Are you sure you want to remove {{item.name}}?": "Are you sure you want to remove {{item.name}}?", + "Are you sure you want to resend all Pending Invitations": "Are you sure you want to resend all Pending Invitations", + "Are you sure you wish to remove this moderator?": "Are you sure you wish to remove this moderator?", + "Ask new members whether they have these skills and interests?": "Ask new members whether they have these skills and interests?", + "At A Glance": "At A Glance", + "Available": "Available", + "Back": "Back", + "Base Layer:": "Base Layer:", + "Be the first to comment": "Be the first to comment", + "Below is a list of every topic that any member of your group has used to date. You can choose to hide\n topics that you would prefer members of your group don't use, or pin topics to the top of the list\n to make sure people pay attention to posts in those topics.": "Below is a list of every topic that any member of your group has used to date. You can choose to hide\n topics that you would prefer members of your group don't use, or pin topics to the top of the list\n to make sure people pay attention to posts in those topics.", + "Benchmarking": "Benchmarking", + "Big Grid": "Big Grid", + "Biodiversity": "Biodiversity", + "Block this Member": "Block this Member", + "Blocked Users": "Blocked Users", + "Both": "Both", + "Bring your group together": "Bring your group together", + "Buy from us": "Buy from us", + "Buy from {{group.name}}": "Buy from {{group.name}}", + "CLOSE": "CLOSE", + "Can you go?": "Can you go?", + "Cancel": "Cancel", + "Cancel Invite": "Cancel Invite", + "Cancel Request": "Cancel Request", + "Canceled": "Canceled", + "Carbon markets": "Carbon markets", + "Card view": "Card view", + "Cards": "Cards", + "Certifications": "Certifications", + "Change Map Layers": "Change Map Layers", + "Changes not saved": "Changes not saved", + "Changes won't be saved. Are you sure you want to cancel?": "Changes won't be saved. Are you sure you want to cancel?", + "Check your email": "Check your email", + "Child Groups": "Child Groups", + "Claims:": "Claims:", + "Click or press on it to copy it": "Click or press on it to copy it", + "Click the button below to create a free Stripe account (or connect an existing account). Once you've done that you will be able to accept contributions to Projects.": "Click the button below to create a free Stripe account (or connect an existing account). Once you've done that you will be able to accept contributions to Projects.", + "Click to Copy": "Click to Copy", + "Close": "Close", + "Close Drawer": "Close Drawer", + "Closed": "Closed", + "Collection": "Collection", + "Comments": "Comments", + "Communication & marketing": "Communication & marketing", + "Community Topics": "Community Topics", + "Community map": "Community map", + "Complete your profile": "Complete your profile", + "Completed": "Completed", + "Completed: {{endTime}}": "Completed: {{endTime}}", + "Confirm Password": "Confirm Password", + "Connect": "Connect", + "Connect Stripe Account": "Connect Stripe Account", + "Contact Email": "Contact Email", + "Contact Phone": "Contact Phone", + "Contact {{group.name}} to learn about their practices": "Contact {{group.name}} to learn about their practices", + "Continue": "Continue", + "Continue with Facebook": "Continue with Facebook", + "Continue with Google": "Continue with Google", + "Contribute": "Contribute", + "Contribute to Hylo": "Contribute to Hylo", + "Contributing Via Stripe": "Contributing Via Stripe", + "Contributions so far: {{totalContributions}}": "Contributions so far: {{totalContributions}}", + "Cooperatives": "Cooperatives", + "Copied!": "Copied!", + "Copy": "Copy", + "Copy Link": "Copy Link", + "Create": "Create", + "Create & navigate": "Create & navigate", + "Create Group": "Create Group", + "Create Role": "Create Role", + "Create a Topic": "Create a Topic", + "Create a group": "Create a group", + "Create a new movement, network, community or group!": "Create a new movement, network, community or group!", + "Create a project that people can help with": "", + "Create a request or offer!": "Create a request or offer!", + "Create additional roles or badges for the group": "Create additional roles or badges for the group", + "Create new custom view": "Create new custom view", + "Create new role/badge": "Create new role/badge", + "Create topic \"#{{item.value}}\"": "Create topic \"#{{item.value}}\"", + "Create topic \"{{item.value}}\"": "Create topic \"{{item.value}}\"", + "Created topic #{{topicName}}": "Created topic #{{topicName}}", + "Current settings up to date": "Current settings up to date", + "Currently, only groups you specify above will see this post": "Currently, only groups you specify above will see this post", + "Custom View": "Custom View", + "Custom Views": "Custom Views", + "Customize the invite email message (optional):": "Customize the invite email message (optional):", + "DELETE: CAUTION": "DELETE: CAUTION", + "Daily": "Daily", + "Deactivate": "Deactivate", + "Deactivate Account": "Deactivate Account", + "Decline": "Decline", + "Declined": "Declined", + "Declined Invitations & Requests": "Declined Invitations & Requests", + "Default Sort": "Default Sort", + "Default Style": "Default Style", + "Default Topics": "Default Topics", + "Delete": "Delete", + "Delete Account": "Delete Account", + "Delete Group": "Delete Group", + "Delete {{groupName}}": "Delete {{groupName}}", + "Description": "Description", + "Disconnect": "Disconnect", + "Discussions": "Discussions", + "Display exact location": "Display exact location", + "Display only nearest city and dont show on the map": "Display only nearest city and dont show on the map", + "Display only nearest city and show nearby location on the map": "Display only nearest city and show nearby location on the map", + "Displaying": "Displaying", + "Donation Link": "Donation Link", + "Download App": "Download App", + "Download our": "Download our", + "Ecosystem services": "Ecosystem services", + "Edit": "Edit", + "Edit Profile": "Edit Profile", + "Edit welcome message": "Edit welcome message", + "Email": "Email", + "Email address is not in a valid format": "Email address is not in a valid format", + "Email address not found": "Email address not found", + "Email addresses of those you'd like to invite:": "Email addresses of those you'd like to invite:", + "End Time must be after Start Time": "End Time must be after Start Time", + "Ended: {{endTime}}": "Ended: {{endTime}}", + "Ends: {{endTime}}": "Ends: {{endTime}}", + "Enter a title": "Enter a title", + "Enter a topic name:": "Enter a topic name:", + "Enter up to three topics...": "Enter up to three topics...", + "Enter your email address and we'll send you an email that lets you reset your password.": "Enter your email address and we'll send you an email that lets you reset your password.", + "Enter your email to get started:": "Enter your email to get started:", + "Enter your message here": "Enter your message here", + "Environmental impact": "Environmental impact", + "Equipment sharing": "Equipment sharing", + "Event collaboration": "Event collaboration", + "Events": "Events", + "Expire": "Expire", + "Explanation for Flagging": "Explanation for Flagging", + "Explore": "Explore", + "Export Data": "Export Data", + "Export Members": "Export Members", + "External Link": "External Link", + "External link": "External link", + "External link has to be a valid URL.": "External link has to be a valid URL.", + "Farm Details": "Farm Details", + "Farm Surrounds & Posts": "Farm Surrounds & Posts", + "Farm Type: ": "Farm Type: ", + "Farm support": "Farm support", + "Farm valuation": "Farm valuation", + "Farms": "Farms", + "Featured:": "Featured:", + "Features:": "Features:", + "Feedback & Support": "Feedback & Support", + "Files": "Files", + "Filter by topics and keywords": "Filter by topics and keywords", + "Filters": "Filters", + "Find a member": "Find a member", + "Find out what's happening around you, and groups you can join": "Find out what's happening around you, and groups you can join", + "Find/add a topic": "Find/add a topic", + "Flag": "Flag", + "For place based groups, draw the area where your group is active (or paste in GeoJSON here)": "For place based groups, draw the area where your group is active (or paste in GeoJSON here)", + "Forgot password?": "Forgot password?", + "GLOBAL": "GLOBAL", + "GROUP NOTIFICATIONS": "GROUP NOTIFICATIONS", + "Gather your collaborators & people who share your interests": "Gather your collaborators & people who share your interests", + "Generate a Link": "Generate a Link", + "Get updates about this map view": "Get updates about this map view", + "Go Back": "Go Back", + "Go back": "Go back", + "Going": "Going", + "Group": "Group", + "Group Access Questions": "Group Access Questions", + "Group Explorer": "Group Explorer", + "Group Invitations & Join Requests": "Group Invitations & Join Requests", + "Group Name": "Group Name", + "Group Requesting to Join": "Group Requesting to Join", + "Group Settings": "Group Settings", + "Group Shape": "Group Shape", + "Group Suggested Topics": "Group Suggested Topics", + "Group menu": "Group menu", + "Group {{locationDescriptor}}": "Group {{locationDescriptor}}", + "Groups": "Groups", + "Groups & Affiliations": "Groups & Affiliations", + "Hazard risk mitigation": "Hazard risk mitigation", + "Here you can switch between types of content and create new content for people in your group or everyone on Hylo!": "Here you can switch between types of content and create new content for people in your group or everyone on Hylo!", + "Hi there {{groupName}}, I'd like to talk about {{prompt}}.": "Hi there {{groupName}}, I'd like to talk about {{prompt}}.", + "Hi {{email}} we just need to know your name and password and you": "Hi {{email}} we just need to know your name and password and you're in.", + "Hi {{email}} we just need to know your name and password and you're in.": "Hi {{email}} we just need to know your name and password and you're in.", + "Hi {{firstName}}, want to create an event?": "Hi {{firstName}}, want to create an event?", + "Hi {{firstName}}, what are you looking for?": "Hi {{firstName}}, what are you looking for?", + "Hi {{firstName}}, what would you like to create?": "Hi {{firstName}}, what would you like to create?", + "Hi {{firstName}}, what would you like to share?": "Hi {{firstName}}, what would you like to share?", + "Hi {{firstName}}, what's on your mind?": "Hi {{firstName}}, what's on your mind?", + "Hi!\n\nI'm inviting you to join {{name}} on Hylo.\n\n{{name}} is using Hylo for our online community: this is our dedicated space for communication & collaboration.": "Hi!\n\nI'm inviting you to join {{name}} on Hylo.\n\n{{name}} is using Hylo for our online community: this is our dedicated space for communication & collaboration.", + "Hidden": "Hidden", + "Hide posts from child groups": "Hide posts from child groups", + "Hide posts from child groups you are a member of": "Hide posts from child groups you are a member of", + "Hide {{postType}} Data": "Hide {{postType}} Data", + "Hide {{postType}} data for this group": "Hide {{postType}} data for this group", + "Home": "Home", + "How can people become members of": "How can people become members of", + "How do we use cookies?": "How do we use cookies?", + "How often would you like to receive email digests for new posts in your groups and saved searches?": "How often would you like to receive email digests for new posts in your groups and saved searches?", + "How would you like to receive notifications about": "How would you like to receive notifications about", + "Hylo Groups": "Hylo Groups", + "Hylo login & session": "Hylo login & session", + "Hylo logo": "Hylo logo", + "Hylo uses cookies to enhance the user experience.": "Hylo uses cookies to enhance the user experience.", + "INVITE": "INVITE", + "IS THIS GROUP A MEMBER OF OTHER GROUPS?": "IS THIS GROUP A MEMBER OF OTHER GROUPS?", + "Icon": "Icon", + "If you deactivate your account:": "If you deactivate your account:", + "If you delete this group, it will no longer be visible to you or any of the members. All posts will also be deleted.": "If you delete this group, it will no longer be visible to you or any of the members. All posts will also be deleted.", + "If you delete your account:": "If you delete your account:", + "If you don't want to display the detailed {{postType}} specific data on your group's profile": "If you don't want to display the detailed {{postType}} specific data on your group's profile", + "If you select a prerequisite group that has a visibility setting of": "If you select a prerequisite group that has a visibility setting of", + "If you turn 'Accept Contributions' on, people will be able\n to send money to your Stripe connected account to support\n this project.": "If you turn 'Accept Contributions' on, people will be able\n to send money to your Stripe connected account to support\n this project.", + "If your email address matched an account in our system, we sent you an email. Please check your inbox.": "If your email address matched an account in our system, we sent you an email. Please check your inbox.", + "Illegal": "Illegal", + "Images": "Images", + "Import Posts by CSV": "Import Posts by CSV", + "Import started!": "Import started!", + "In the meantime, click a topic from an individual\ngroup to see posts from that group.": "In the meantime, click a topic from an individual\ngroup to see posts from that group.", + "Inappropriate Content": "Inappropriate Content", + "Include only active posts?": "Include only active posts?", + "Include only posts that match any of these topics:": "Include only posts that match any of these topics:", + "Included Posts": "Included Posts", + "Increasing sales": "Increasing sales", + "Insetting": "Insetting", + "Interactions": "Interactions", + "Intercom": "Intercom", + "Interested": "Interested", + "Invalid code, please try again": "Invalid code, please try again", + "Invalid email address": "Invalid email address", + "Invalid url. Please enter the full url for your {{network}} page.": "Invalid url. Please enter the full url for your {{network}} page.", + "Invitations to Join New Groups": "Invitations to Join New Groups", + "Invite": "Invite", + "Invite 1 person": "Invite 1 person", + "Invite People": "Invite People", + "Invite a group to join": "Invite a group to join", + "Invite people to your event": "", + "Invite {{invitedIds.length}} people": "Invite {{invitedIds.length}} people", + "Invites & Requests": "Invites & Requests", + "Is this offer still available?": "Is this offer still available?", + "Is this project still active?": "Is this project still active?", + "Is this request still needed?": "Is this request still needed?", + "Is this resource still available?": "Is this resource still available?", + "I’d love to show you how things work, would you like a quick tour?": "I’d love to show you how things work, would you like a quick tour?", + "Join": "Join", + "Join Hylo": "Join Hylo", + "Join Project": "Join Project", + "Join Request Approved": "Join Request Approved", + "Join Requests": "Join Requests", + "Join group was unsuccessful": "Join group was unsuccessful", + "Join to see": "Join to see", + "Join {{group.name}} to another group": "Join {{group.name}} to another group", + "Jump in to Hylo!": "Jump in to Hylo!", + "Jump in!": "Jump in!", + "Label": "Label", + "Large Grid": "Large Grid", + "Latest activity": "Latest activity", + "Leave": "Leave", + "Leave Parent": "Leave Parent", + "Leave Project": "Leave Project", + "Let people know about available resources": "", + "Link": "Link", + "Link Stripe Account": "Link Stripe Account", + "Link expired, please start over": "Link expired, please start over", + "List": "List", + "List view": "List view", + "Loading...": "Loading...", + "Local storage & cache": "Local storage & cache", + "Location": "Location", + "Location & Hours": "Location & Hours", + "Location Privacy:": "Location Privacy:", + "Log In": "Log In", + "Log out": "Log out", + "Low-cost loans": "Low-cost loans", + "MAKE AN ANNOUNCEMENT": "MAKE AN ANNOUNCEMENT", + "Make Public:": "Make Public:", + "Make sure you trust {{name}} with your information.": "Make sure you trust {{name}} with your information.", + "Management Techniques: ": "Management Techniques: ", + "Manual": "Manual", + "Map": "Map", + "Mark all as read": "Mark all as read", + "Member": "Member", + "Member Count": "Member Count", + "Member Profile": "Member Profile", + "Member_plural": "Member_plural", + "Members": "Members", + "Membership Requested": "Membership Requested", + "Mentions": "Mentions", + "Mentorship & advice": "Mentorship & advice", + "Message Member": "Message Member", + "Messages": "Messages", + "Messages, notifications & profile": "Messages, notifications & profile", + "Mixpanel": "Mixpanel", + "Mobile App": "Mobile App", + "Moderator": "Moderator", + "Moderators": "Moderators", + "Multiple people are typing...": "Multiple people are typing...", + "Must be a valid URL!": "Must be a valid URL!", + "My Groups": "My Groups", + "My Home": "My Home", + "My Posts": "My Posts", + "My Skills & Interests": "My Skills & Interests", + "NO MORE RECENT ACTIVITY": "NO MORE RECENT ACTIVITY", + "NOT": "NOT", + "NOTIFICATIONS": "NOTIFICATIONS", + "Name": "Name", + "Name must not be blank": "Name must not be blank", + "Name of organization": "Name of organization", + "Name of role": "Name of role", + "Name this view": "Name this view", + "Native Territories": "Native Territories", + "Nearby Relevant Events": "Nearby Relevant Events", + "Nearby Relevant Groups": "Nearby Relevant Groups", + "Nearby Relevant Offers and Requests": "Nearby Relevant Offers and Requests", + "Nearest": "Nearest", + "Never": "Never", + "New": "New", + "New Comment on ": "New Comment on ", + "New Group Joined": "New Group Joined", + "New Join Request": "New Join Request", + "New Message": "New Message", + "New Password": "New Password", + "New Password (Confirm)": "New Password (Confirm)", + "New Post at this location:": "New Post at this location:", + "New Post in ": "New Post in ", + "New markets": "New markets", + "New methods & practices": "New methods & practices", + "Next": "Next", + "Next: Welcome to Hylo!": "Next: Welcome to Hylo!", + "Next: Where are you from?": "Next: Where are you from?", + "No groups are members of {{group.name}} yet": "No groups are members of {{group.name}} yet", + "No icon selected": "No icon selected", + "No longer needed": "No longer needed", + "No messages found": "No messages found", + "No more results": "No more results", + "No new join requests": "No new join requests", + "No notifications": "No notifications", + "No one is attending yet": "No one is attending yet", + "No project members": "No project members", + "No result": "No result", + "No results for this search": "No results for this search", + "No thanks": "No thanks", + "No unread notifications": "No unread notifications", + "No {{timeFrame}} events": "No {{timeFrame}} events", + "None": "None", + "Not Going": "Not Going", + "Not a member of Hylo?": "Not a member of Hylo?", + "Not answered": "Not answered", + "Note: as a moderator you will always see the exact location displayed": "Note: as a moderator you will always see the exact location displayed", + "Nothing to see here": "Nothing to see here", + "Notifications": "Notifications", + "Notify me via": "Notify me via", + "Nutrient density": "Nutrient density", + "OFF": "OFF", + "ON": "ON", + "Off": "Off", + "Offensive": "Offensive", + "Offer": "Offer", + "Offers": "Offers", + "Oh no, something went wrong! Check your internet connection and try again": "Oh no, something went wrong! Check your internet connection and try again", + "Ok": "Ok", + "On": "On", + "One more step!": "One more step!", + "Only members of this group can see posts": "Only members of this group can see posts", + "Only parent groups can be added as prerequisite groups.": "Only parent groups can be added as prerequisite groups.", + "Oops! Something went wrong. Try reloading the page.": "Oops! Something went wrong. Try reloading the page.", + "Oops, there's nothing to see here.": "Oops, there's nothing to see here.", + "Open": "Open", + "Open Drawer": "Open Drawer", + "Open Invitations to Join Other Groups": "Open Invitations to Join Other Groups", + "Open Messages": "Open Messages", + "Open Requests & Offers": "Open Requests & Offers", + "Operation: ": "Operation: ", + "Opportunities to Collaborate": "Opportunities to Collaborate", + "Optimizely": "Optimizely", + "Optimizely helps us to test improvements to Hylo by showing different users different sets of features. Optimizely tracks who has seen what and how successful the feature is in accomplishing it's goal": "Optimizely helps us to test improvements to Hylo by showing different users different sets of features. Optimizely tracks who has seen what and how successful the feature is in accomplishing it's goal", + "Or log in with": "Or log in with", + "Or sign in with an existing account": "Or sign in with an existing account", + "Other": "Other", + "Other Affiliations": "Other Affiliations", + "Other Layers": "Other Layers", + "Other Roles & Badges": "Other Roles & Badges", + "Overview": "Overview", + "PERSONAL": "PERSONAL", + "Parent Groups": "Parent Groups", + "Password": "Password", + "Password (at least 9 characters)": "Password (at least 9 characters)", + "Passwords don't match": "Passwords don't match", + "Passwords must be at least 9 characters long": "Passwords must be at least 9 characters long", + "Passwords must be at least 9 characters long, and should be a mix of lower and upper case letters, numbers and symbols.": "Passwords must be at least 9 characters long, and should be a mix of lower and upper case letters, numbers and symbols.", + "Past Events": "Past Events", + "Payment": "Payment", + "Pending": "Pending", + "Pending Invites": "Pending Invites", + "Pending invites to join {{group.name}}": "Pending invites to join {{group.name}}", + "Pending requests to join other groups": "Pending requests to join other groups", + "People": "People", + "People can apply to join this group and must be approved": "People can apply to join this group and must be approved", + "People want to join your group!": "People want to join your group!", + "Pin": "Pin", + "Please enter a URL slug": "Please enter a URL slug", + "Please enter a group name": "Please enter a group name", + "Please enter a valid email address": "Please enter a valid email address", + "Please enter the full url for your {{network}} page.": "Please enter the full url for your {{network}} page.", + "Please enter your password": "Please enter your password", + "Please enter your twitter name.": "Please enter your twitter name.", + "Please provide either a `token` query string parameter or `accessCode` route param": "Please provide either a `token` query string parameter or `accessCode` route param", + "Plural word used to describe group Moderators": "Plural word used to describe group Moderators", + "Popular": "Popular", + "Post": "Post", + "Post Collection": "Post Collection", + "Post Date": "Post Date", + "Post Stream": "Post Stream", + "Post from": "Post from", + "Post from child group": "Post from child group", + "Post in": "Post in", + "Posted In:": "Posted In:", + "Posting...": "Posting...", + "Posts": "Posts", + "Prerequisite Groups": "Prerequisite Groups", + "Press on the group name or icon to navigate within the current group or view. Discover events, discussions, resources & more!": "Press on the group name or icon to navigate within the current group or view. Discover events, discussions, resources & more!", + "Previous": "Previous", + "Privacy": "Privacy", + "Privacy & Access": "Privacy & Access", + "Privacy Policy": "Privacy Policy", + "Privacy settings": "Privacy settings", + "Product quality": "Product quality", + "Profile": "Profile", + "Project Management": "Project Management", + "Project Members": "Project Members", + "Projects": "Projects", + "Projects help you and your group accomplish shared goals.": "Projects help you and your group accomplish shared goals.", + "Protected": "Protected", + "Public": "Public", + "Public Groups": "Public Groups", + "Public Groups & Posts": "Public Groups & Posts", + "Public Map": "Public Map", + "Public Offerings": "Public Offerings", + "Public Stream": "Public Stream", + "Public stream": "Public stream", + "Push Notifications": "Push Notifications", + "Quick topic-based chats": "", + "REQUEST": "REQUEST", + "RSVP": "RSVP", + "Reactions": "Reactions", + "Reactivate": "Reactivate", + "Read More": "Read More", + "Recent": "Recent", + "Recent Posts": "Recent Posts", + "Recently Active Members": "Recently Active Members", + "Recently Active Projects": "Recently Active Projects", + "Related Groups": "Related Groups", + "Relevant skills & interests": "Relevant skills & interests", + "Remove": "Remove", + "Remove Child": "Remove Child", + "Remove From Group": "Remove From Group", + "Remove Post": "Remove Post", + "Remove from group as well": "Remove from group as well", + "Remove post?": "Remove post?", + "Reply to": "Reply to", + "Request": "Request", + "Request Membership in": "Request Membership in", + "Request expired, please start over": "Request expired, please start over", + "Request to Join": "Request to Join", + "Request to join pending": "Request to join pending", + "Requested": "Requested", + "Requests": "Requests", + "Requests to join {{group.name}}": "Requests to join {{group.name}}", + "Require groups to answer questions when requesting to join this group": "Require groups to answer questions when requesting to join this group", + "Require people to answer questions when requesting to join this group": "Require people to answer questions when requesting to join this group", + "Research projects": "Research projects", + "Resend": "Resend", + "Resend All": "Resend All", + "Resend code": "Resend code", + "Reset": "Reset", + "Reset Link": "Reset Link", + "Reset Your Password": "Reset Your Password", + "Resources": "Resources", + "Responses": "Responses", + "Restricted": "Restricted", + "Restricted group, no request pending": "Restricted group, no request pending", + "Return to All Groups": "Return to All Groups", + "Revert": "Revert", + "Roles & Badges": "Roles & Badges", + "STEP 1/3": "STEP 1/3", + "STEP 2/3": "STEP 2/3", + "Save": "Save", + "Save Changes": "Save Changes", + "Save this view": "Save this view", + "Saved Searches": "Saved Searches", + "Saved Views": "Saved Views", + "Search": "Search", + "Search by keyword for people, posts and groups": "Search by keyword for people, posts and groups", + "Search by name or skills & interests": "Search by name or skills & interests", + "Search for a location...": "Search for a location...", + "Search for people...": "Search for people...", + "Search for posts": "Search for posts", + "Search for posts & people. Send messages to group members or people you see on Hylo. Stay up to date with current events and edit your profile.": "Search for posts & people. Send messages to group members or people you see on Hylo. Stay up to date with current events and edit your profile.", + "Search groups by keyword": "Search groups by keyword", + "Search here for members to grant moderator powers": "Search here for members to grant moderator powers", + "Search here for members to grant this role too": "Search here for members to grant this role too", + "Search members": "Search members", + "Search posts": "Search posts", + "Search topics": "Search topics", + "Search {{count}} topics": "Search {{count}} topics", + "Search {{count}} topics_plural": "Search {{count}} topics_plural", + "Select End": "Select End", + "Select Start": "Select Start", + "Select a reason": "Select a reason", + "Select people to invite": "Select people to invite", + "Send Invite": "Send Invite", + "Send Invites via email": "Send Invites via email", + "Send It": "Send It", + "Send me a digest": "Send me a digest", + "Sent": "Sent", + "Sent {{numGood}} {{email}}": "Sent {{numGood}} {{email}}", + "Set default topics for your group which will be suggested first when\n members are creating a new post.\n Every new member will also be subscribed to these topics when they join.": "Set default topics for your group which will be suggested first when\n members are creating a new post.\n Every new member will also be subscribed to these topics when they join.", + "Set your password here.": "Set your password here.", + "Settings": "Settings", + "Share a Join Link": "Share a Join Link", + "Share about who you are, your skills & interests": "Share about who you are, your skills & interests", + "Show Less": "Show Less", + "Show me Hylo": "Show me Hylo", + "Show posts from child groups": "Show posts from child groups", + "Show posts from child groups you are a member of": "Show posts from child groups you are a member of", + "Sign Up": "Sign Up", + "Sign in": "Sign in", + "Sign in to Hylo": "Sign in to Hylo", + "Signup": "Signup", + "Signup or Login to connect with": "Signup or Login to connect with", + "Skills & Interests": "Skills & Interests", + "Small Grid": "Small Grid", + "Social Accounts": "Social Accounts", + "Social media link for {{group.name}}": "Social media link for {{group.name}}", + "Something weird happened during consent process": "Something weird happened during consent process", + "Sorry, that Email and Password combination didn't work.": "Sorry, that Email and Password combination didn't work.", + "Sorry, your invitation to this group is expired, has already been used, or is invalid. Please contact a group moderator for another one.": "Sorry, your invitation to this group is expired, has already been used, or is invalid. Please contact a group moderator for another one.", + "Sort by": "Sort by", + "Sort posts by:": "Sort posts by:", + "Spam": "Spam", + "Start a Group": "Start a Group", + "Start typing to add a topic": "Start typing to add a topic", + "Start typing to find/create a topic to add": "Start typing to find/create a topic to add", + "Started: {{from}}": "Started: {{from}}", + "Starts: {{from}}": "Starts: {{from}}", + "Stay connected, organized, and engaged with your group.": "Stay connected, organized, and engaged with your group.", + "Stream": "Stream", + "Subgroups": "Subgroups", + "Submit": "Submit", + "Subscribe": "Subscribe", + "Support this project": "Support this project", + "Support this project on": "Support this project on", + "Switching groups & viewing all": "Switching groups & viewing all", + "Tagline": "Tagline", + "Talk about whats important with others": "Talk about what's important with others", + "Terms & Privacy": "Terms & Privacy", + "Terms of Service": "Terms of Service", + "Thanks for your contribution!": "Thanks for your contribution!", + "The content of your posts and comments will be removed": "The content of your posts and comments will be removed", + "The moderators go here": "The moderators go here", + "The {{title}} section is not visible to members of this group. Click the three dots": "The {{title}} section is not visible to members of this group. Click the three dots", + "There was a problem processing your payment. Please check your card details and try again.": "There was a problem processing your payment. Please check your card details and try again.", + "There was a problem with your request. Please check your email and try again.": "There was a problem with your request. Please check your email and try again.", + "There was an error, please try again.": "There was an error, please try again.", + "There was an issue registering your stripe account. Please try again. If the problem persists, contact us.": "There was an issue registering your stripe account. Please try again. If the problem persists, contact us.", + "These are the {{length}} groups that {{group.name}} is a member of": "These are the {{length}} groups that {{group.name}} is a member of", + "These {{childGroups.length}} groups are members of {{group.name}}": "These {{childGroups.length}} groups are members of {{group.name}}", + "This URL already exists. Try another.": "This URL already exists. Try another.", + "This action is": "This action", + "This action is reversible, just log back in": "This action is reversible, just log back in", + "This function exports all member data for this group as a CSV file for import into other software.": "This function exports all member data for this group as a CSV file for import into other software.", + "This group has prerequisite groups you cannot see, you cannot join this group at this time": "This group has prerequisite groups you cannot see, you cannot join this group at this time", + "This group is a member": "This group is a member", + "This group is invitation only": "This group is invitation only", + "This is group is invitation only": "This is group is invitation only", + "This is still needed": "This is still needed", + "This is the one group": "This is the one group", + "This is where we show you which group or other view you are looking at. Click here to return to the home page.": "This is where we show you which group or other view you are looking at. Click here to return to the home page.", + "This list automatically shows which groups on Hylo you are a part of. You can also share your affiliations with organizations that are not currently on Hylo.": "This list automatically shows which groups on Hylo you are a part of. You can also share your affiliations with organizations that are not currently on Hylo.", + "This list contains all open requests and invitations to join groups.": "This list contains all open requests and invitations to join groups.", + "This means that all members of the {{groupCount}} groups selected will receive an instant email and push notification about this Post.": "This means that all members of the {{groupCount}} groups selected will receive an instant email and push notification about this Post.", + "This means that all members of the {{groupCount}} groups selected will receive instant email and push notifications about this Post. (This feature is available to moderators only.": "This means that all members of the {{groupCount}} groups selected will receive instant email and push notifications about this Post. (This feature is available to moderators only.", + "This means that all members of this group will receive an instant email and push notification about this Post. (This feature is available to moderators only.": "This means that all members of this group will receive an instant email and push notification about this Post. (This feature is available to moderators only.", + "This menu allows you to switch between groups, or see updates from all your groups at once.\n\nWant to see what else is out there? Navigate over to Public Groups & Posts to see!": "This menu allows you to switch between groups, or see updates from all your groups at once.\n\nWant to see what else is out there? Navigate over to Public Groups & Posts to see!", + "This project is being managed on": "This project is being managed on", + "This video will be full-width, displayed above the description, and playable.": "This video will be full-width, displayed above the description, and playable.", + "This will allow {{appName}} to:": "This will allow {{appName}} to:", + "This will only be sent as an Announcement to the groups where you are a Moderator. For other groups it will be shared as a regular Post.": "This will only be sent as an Announcement to the groups where you are a Moderator. For other groups it will be shared as a regular Post.", + "Timeframe": "Timeframe", + "Title can't have more than {{maxTitleLength}} characters": "Title can't have more than {{maxTitleLength}} characters", + "To": "To", + "To accept financial contributions for this project, you have\n to connect a Stripe account. Go to": "To accept financial contributions for this project, you have\n to connect a Stripe account. Go to", + "To follow the tour look for the pulsing beacons!": "To follow the tour look for the pulsing beacons!", + "To join": "To join", + "To view all groups you are a part of go to your": "To view all groups you are a part of go to your", + "Topic Created": "Topic Created", + "Topic List Editor": "Topic List Editor", + "Topic name is required.": "Topic name is required.", + "Topics": "Topics", + "Topics must be longer than 2 characters": "Topics must be longer than 2 characters", + "Type": "Type", + "Type email addresses (multiples should be separated by either a comma or new line)": "Type email addresses (multiples should be separated by either a comma or new line)", + "Type group name...": "Type group name...", + "Type persons name...": "Type persons name...", + "Type your answer here...": "Type your answer here...", + "Type...": "Type...", + "URL of organization": "URL of organization", + "URLs must have between 2 and 40 characters, and can only have lower case letters, numbers, and dashes.": "URLs must have between 2 and 40 characters, and can only have lower case letters, numbers, and dashes.", + "Unavailable": "Unavailable", + "Unblock": "Unblock", + "Unlink": "Unlink", + "Unpin": "Unpin", + "Unread": "Unread", + "Unsubscribe": "Unsubscribe", + "Upcoming Events": "Upcoming Events", + "Update Account": "Update Account", + "Upload CSV": "Upload CSV", + "Upload a profile image": "Upload a profile image", + "Upvotes": "Upvotes", + "Use your Hylo account to access {{name}}.": "Use your Hylo account to access {{name}}.", + "View": "View", + "View Current Members": "View Current Members", + "View all": "View all", + "View and participate in public discussions, projects, events & more": "View and participate in public discussions, projects, events & more", + "View details": "View details", + "View name needs to be at least two characters long.": "View name needs to be at least two characters long.", + "View project management tool": "View project management tool", + "View tasks": "View tasks", + "View the public map": "View the public map", + "Visibility": "Visibility", + "Visible": "Visible", + "Visit": "Visit", + "Volunteer opportunities": "Volunteer opportunities", + "WARNING: This is a beta feature that at this time will not inform you of import errors, use at your own risk.": "WARNING: This is a beta feature that at this time will not inform you of import errors, use at your own risk.", + "WHO CAN JOIN THIS GROUP?": "WHO CAN JOIN THIS GROUP?", + "WHO CAN SEE THIS GROUP?": "WHO CAN SEE THIS GROUP?", + "Warning:": "Warning:", + "We store images, icons and application data in your browser to improve performance and load times.": "We store images, icons and application data in your browser to improve performance and load times.", + "We use a service called Mixpanel to understand how people like you use Hylo. Your identity is anonymized but your behavior is recorded so that we can make improvements to Hylo based on how people are using it.": "We use a service called Mixpanel to understand how people like you use Hylo. Your identity is anonymized but your behavior is recorded so that we can make improvements to Hylo based on how people are using it.", + "We use cookies to help understand whether you are logged in and to understand your preferences and where you are in Hylo.": "We use cookies to help understand whether you are logged in and to understand your preferences and where you are in Hylo.", + "We'll notify you by email when someone wants to join": "We'll notify you by email when someone wants to join", + "We're almost done, are you sure you want to cancel?": "We're almost done, are you sure you want to cancel?", + "We're glad you're here, {{firstName}}. To get started, explore public groups and posts, or create your own group!": "We're glad you're here, {{firstName}}. To get started, explore public groups and posts, or create your own group!", + "We're happy you're here! Please take a moment to explore this page to see what's alive in our group. Introduce yourself by clicking Create to post a Discussion sharing who you are and what brings you to our group. And don't forget to fill out your profile - so likeminded new friends can connect with you!": "We're happy you're here! Please take a moment to explore this page to see what's alive in our group. Introduce yourself by clicking Create to post a Discussion sharing who you are and what brings you to our group. And don't forget to fill out your profile - so likeminded new friends can connect with you!", + "We're working on expanding\n#topics to more places": "We're working on expanding\n#topics to more places", + "We've sent a 6 digit code to {{email}}. The code will expire shortly, so please enter it here soon.": "We've sent a 6 digit code to {{email}}. The code will expire shortly, so please enter it here soon.", + "Website": "Website", + "Website for {{group.name}}": "Website for {{group.name}}", + "Weekly": "Weekly", + "Welcome": "Welcome", + "Welcome Message": "Welcome Message", + "Welcome to Hylo": "Welcome to Hylo", + "Welcome to Hylo!": "Welcome to Hylo!", + "Welcome to {{group.name}}!": "Welcome to {{group.name}}!", + "What I'm Learning": "What I'm Learning", + "What I'm learning": "What I'm learning", + "What are you doing together?": "What are you doing together?", + "What are you looking for help with?": "What are you looking for help with?", + "What area does your group cover?": "What area does your group cover?", + "What can people help you with?": "", + "What do you have for others?": "", + "What do you need? What are you offering?": "What do you need? What are you offering?", + "What do you want to see on the map?": "What do you want to see on the map?", + "What help can you offer?": "What help can you offer?", + "What is your event called?": "What is your event called?", + "What post types to display?": "What post types to display?", + "What questions are asked when a group requests to join this group?": "What questions are asked when a group requests to join this group?", + "What resource is available?": "What resource is available?", + "What skills and interests are most relevant to your group?": "What skills and interests are most relevant to your group?", + "What skills and interests are particularly relevant to this group?": "What skills and interests are particularly relevant to this group?", + "What skills and interests do you have?": "What skills and interests do you have?", + "What skills do you want to learn?": "What skills do you want to learn?", + "What was wrong?": "What was wrong?", + "What would you like to call your project?": "What would you like to call your project?", + "What would you like to create?": "What would you like to create?", + "What you will do at your next event?": "What you will do at your next event?", + "Whats on your mind?": "What's on your mind?", + "What’s on your mind?": "What’s on your mind?", + "When people on Hylo need help or want to report a bug, they are interacting with a service called intercom. Intercom stores cookies in your browser to keep track of conversations with us, the development team.": "When people on Hylo need help or want to report a bug, they are interacting with a service called intercom. Intercom stores cookies in your browser to keep track of conversations with us, the development team.", + "When you select a prerequisite group, people must join the selected groups before joining": "When you select a prerequisite group, people must join the selected groups before joining", + "Where do you call home?": "Where do you call home?", + "Where is your {{type}} located?": "Where is your {{type}} located?", + "Which of the following skills & interests are relevant to you?": "Which of the following skills & interests are relevant to you?", + "Who has access to group settings and moderation powers": "Who has access to group settings and moderation powers", + "Who is able to see": "Who is able to see", + "Why was this {{type}} '{{selectedCategory}}'{{required}}?": "Why was this {{type}} '{{selectedCategory}}'{{required}}?", + "Will open this URL in a new tab": "Will open this URL in a new tab", + "With:": "With:", + "Word used to describe a group Moderator": "Word used to describe a group Moderator", + "Write something...": "Write something...", + "You": "You", + "You already have a stripe account linked to this account. If you would like to link a different account, click the button below.": "You already have a stripe account linked to this account. If you would like to link a different account, click the button below.", + "You are a member of": "You are a member of", + "You are here!": "You are here!", + "You are requesting that": "You are requesting that", + "You can only select up to {{MAX_TOPICS}} topics": "You can only select up to {{MAX_TOPICS}} topics", + "You can select a CSV file to import posts into {{name}}. Posts will be created by you. The file must have columns with the following headers:": "You can select a CSV file to import posts into {{name}}. Posts will be created by you. The file must have columns with the following headers:", + "You don't have any messages yet": "You don't have any messages yet", + "You have no active messages": "You have no active messages", + "You have unsaved changes, are you sure you want to leave?": "You have unsaved changes, are you sure you want to leave?", + "You may add parent groups if you are a moderator of the group you wish to add, or if the group you wish to add has the Open access setting which allows any group to join it": "You may add parent groups if you are a moderator of the group you wish to add, or if the group you wish to add has the Open access setting which allows any group to join it", + "You should receive an email with the member export in a few minutes": "You should receive an email with the member export in a few minutes", + "You wish to turn {{onOrOff}} {{type}} for all groups? This will affect {{numGroups}} {{groups}}.": "You wish to turn {{onOrOff}} {{type}} for all groups? This will affect {{numGroups}} {{groups}}.", + "You won't be able to use Hylo unless you create a brand new account": "You won't be able to use Hylo unless you create a brand new account", + "You won't be able to use Hylo unless you log back in": "You won't be able to use Hylo unless you log back in", + "You won't receive platform notifications": "You won't receive platform notifications", + "Your Open Requests to Join Groups": "Your Open Requests to Join Groups", + "Your Settings": "Your Settings", + "Your account and its details will be deleted": "Your account and its details will be deleted", + "Your account has no password set.": "Your account has no password set.", + "Your account is registered, you're ready to accept contributions to projects.": "Your account is registered, you're ready to accept contributions to projects.", + "Your affiliations with organizations": "Your affiliations with organizations", + "Your comments and posts will REMAIN as they are": "Your comments and posts will REMAIN as they are", + "Your email address": "Your email address", + "Your group doesn't have a description": "Your group doesn't have a description", + "Your group has been invited": "Your group has been invited", + "Your group's name": "Your group's name", + "Your profile won't show up in any member searches or group memberships": "Your profile won't show up in any member searches or group memberships", + "above this box to change the visibility settings. Only moderators can see this message": "above this box to change the visibility settings. Only moderators can see this message", + "and": "and", + "and become a member": "and become a member", + "animal welfare": "animal welfare", + "approved your request to join": "approved your request to join", + "are a member": "are a member", + "are attending": "are attending", + "are members": "are members", + "are you sure you want to remove {{name}}?": "are you sure you want to remove {{name}}?", + "asked to join": "asked to join", + "at": "at", + "attachments": "attachments", + "attending": "attending", + "back": "back", + "become a member of": "become a member of", + "benchmarking your farm": "benchmarking your farm", + "biodiversity": "biodiversity", + "carbon markets": "carbon markets", + "certifications": "certifications", + "chat": "Chat", + "child group": "child group", + "commented": "commented", + "commented on": "commented on", + "contributed to a project": "contributed to a project", + "contributed to your project": "contributed to your project", + "contributed {{amount}} to \"{{postSummary}}\"": "contributed {{amount}} to \"{{postSummary}}\"", + "cooperatives": "cooperatives", + "deactivate": "deactivate", + "description: text": "description: text", + "discusion": "Discussion", + "discussion": "Discusion", + "ecosystem services": "ecosystem services", + "end_date (optional): e.g. 20200731-12:23:12.000+00 (other date formats may work)": "end_date (optional): e.g. 20200731-12:23:12.000+00 (other date formats may work)", + "environmental impact": "environmental impact", + "equipment sharing": "equipment sharing", + "event": "Event", + "event collaboration": "event collaboration", + "farm support": "farm support", + "farm valuation": "farm valuation", + "filtered by topics:": "filtered by topics:", + "for": "for", + "from": "from", + "group": "group", + "group privacy settings": "group privacy settings", + "groups": "groups", + "has invited": "has invited", + "has joined": "has joined", + "has requested to join": "has requested to join", + "have a valid emoji and name to be updated": "A role must have a valid emoji and name to be updated", + "hazard risk mitigation": "hazard risk mitigation", + "helping with your communication & marketing": "helping with your communication & marketing", + "image_urls: 1 or more image URLs separated by spaces and/or commas": "image_urls: 1 or more image URLs separated by spaces and/or commas", + "increasing sales": "increasing sales", + "insetting": "insetting", + "introducing new methods & practices": "introducing new methods & practices", + "invited you to an event": "invited you to an event", + "invited you to join": "invited you to join", + "invited you to: \"{{postSummary}}\"": "invited you to: \"{{postSummary}}\"", + "is a member": "is a member", + "is attending": "is attending", + "is only accessible to members of": "is only accessible to members of", + "is typing...": "is typing...", + "is_public: true or false": "is_public: true or false", + "location: text": "location: text", + "low-cost loans": "low-cost loans", + "member": "member", + "member_plural": "member_plural", + "mentioned you": "mentioned you", + "mentioned you in a comment on": "mentioned you in a comment on", + "mentorship & advice": "mentorship & advice", + "new comments on posts you're following?": "new comments on posts you're following?", + "new markets": "new markets", + "no more than {{maxTags}} allowed": "no more than {{maxTags}} allowed", + "nutrient density": "nutrient density", + "of": "of", + "offer": "Offer", + "only members of those groups will be able to join this group. Because of these settings, people who find your group will not be able to see the prerequisite group.": "only members of those groups will be able to join this group. Because of these settings, people who find your group will not be able to see the prerequisite group.", + "or": "or", + "or press Enter": "or press Enter", + "others": "others", + "past": "past", + "posts": "posts", + "product quality": "product quality", + "project": "Project", + "purchasing from you": "purchasing from you", + "reactivate": "reactivate", + "reason-required": "reason-required", + "request": "Request", + "research projects": "research projects", + "resource": "Resource", + "reversible": "reversible", + "sent an announcement": "sent an announcement", + "start_date (optional): e.g. 20200730-12:23:12.000+00 (other date formats may work)": "start_date (optional): e.g. 20200730-12:23:12.000+00 (other date formats may work)", + "subscribers": "subscribers", + "this application": "this application", + "this role/badge?": "this role/badge?", + "title: text": "title: text", + "to join it": "to join it", + "to set it up.": "to set it up.", + "topics: up to 3 topic names separated by spaces and/or commas e.g. “food organic”": "topics: up to 3 topic names separated by spaces and/or commas e.g. “food organic”", + "type here": "type here", + "type: one of discussion, request, offer, resource, event, project": "type: one of discussion, request, offer, resource, event, project", + "upcoming": "upcoming", + "useLayoutFlags must be used within a LayoutFlagsProvider": "useLayoutFlags must be used within a LayoutFlagsProvider", + "visit": "visit", + "volunteer opportunities": "volunteer opportunities", + "with this link": "with this link", + "wrote:": "wrote:", + "wrote: ": "wrote: ", + "you're subscribed to #{{topicName}}": "you're subscribed to #{{topicName}}", + "{{action}} was canceled or no user data was found.": "{{action}} was canceled or no user data was found.", + "{{appName}} is asking to have offline access to Hylo": "{{appName}} is asking to have offline access to Hylo", + "{{appName}} is asking you to confirm previously given authorization": "{{appName}} is asking you to confirm previously given authorization", + "{{appName}} wants access to your Hylo account": "{{appName}} wants access to your Hylo account", + "{{childGroups.length}} groups are a part of {{group.name}}": "{{childGroups.length}} groups are a part of {{group.name}}", + "{{count}} Groups": "{{count}} Groups", + "{{count}} Groups_plural": "{{count}} Groups_plural", + "{{currentGroup.name}} topics": "{{currentGroup.name}} topics", + "{{group.name}} has some volunteering opportunities": "{{group.name}} has some volunteering opportunities", + "{{group.name}} is a part of 1 Group": "{{group.name}} is a part of 1 Group", + "{{group.name}} is a part of {{parentGroups.length}} Groups": "{{group.name}} is a part of {{parentGroups.length}} Groups", + "{{group.name}} is available to participate in research": "{{group.name}} is available to participate in research", + "{{group.name}} is curious about better understanding their farm through data (soil, environmental impacts, etc.)": "{{group.name}} is curious about better understanding their farm through data (soil, environmental impacts, etc.)", + "{{group.name}} is curious about further certification (organic, ISO, regenerative, etc.)": "{{group.name}} is curious about further certification (organic, ISO, regenerative, etc.)", + "{{group.name}} is curious about understanding the nutrient density of their product": "{{group.name}} is curious about understanding the nutrient density of their product", + "{{group.name}} is interested in carbon markets": "{{group.name}} is interested in carbon markets", + "{{group.name}} is interested in equipment sharing": "{{group.name}} is interested in equipment sharing", + "{{group.name}} is interested in forming a cooperative": "{{group.name}} is interested in forming a cooperative", + "{{group.name}} is interested in increasing their sales": "{{group.name}} is interested in increasing their sales", + "{{group.name}} is interested in low-cost loans": "{{group.name}} is interested in low-cost loans", + "{{group.name}} is not a member of any groups yet": "{{group.name}} is not a member of any groups yet", + "{{group.name}} is only accessible to members of the following groups:": "{{group.name}} is only accessible to members of the following groups:", + "{{group.name}} is open to co-hosting events": "{{group.name}} is open to co-hosting events", + "{{group.name}} is seeking ecosystem services": "{{group.name}} is seeking ecosystem services", + "{{group.name}} is seeking farm support": "{{group.name}} is seeking farm support", + "{{group.name}} is seeking new markets": "{{group.name}} is seeking new markets", + "{{group.name}} seeks better communication / marketing to their buyers": "{{group.name}} seeks better communication / marketing to their buyers", + "{{group.name}} wants help focusing on biodiversity (protecting species, improving ecology, markets)": "{{group.name}} wants help focusing on biodiversity (protecting species, improving ecology, markets)", + "{{group.name}} wants to comparing my farm to others": "{{group.name}} wants to comparing my farm to others", + "{{group.name}} wants to easily provide data to supply chain partners": "{{group.name}} wants to easily provide data to supply chain partners", + "{{group.name}} wants to ensure animal welfare": "{{group.name}} wants to ensure animal welfare", + "{{group.name}} wants to improve product quality": "{{group.name}} wants to improve product quality", + "{{group.name}} wants to increase their farm valuation": "{{group.name}} wants to increase their farm valuation", + "{{group.name}} wants to learn about new methods or practices": "{{group.name}} wants to learn about new methods or practices", + "{{group.name}} wants to reduce hazard risk (fire, flood, drought, etc.)": "{{group.name}} wants to reduce hazard risk (fire, flood, drought, etc.)", + "{{groupName}} Topics": "{{groupName}} Topics", + "{{memberCount}} Total Members": "{{memberCount}} Total Members", + "{{name}}s comments": "{{name}}s comments", + "{{name}}s posts": "{{name}}s posts", + "{{name}}s reactions": "{{name}}s reaction", + "{{name}}s recent activity": "{{name}}s recent activity", + "{{numBad}} invalid email address/es found (see above)).": "{{numBad}} invalid email address/es found (see above)).", + "{{othersCount}} others": "{{othersCount}} others", + "{{parentGroup.name}} requires groups to answer the following questions before joining": "{{parentGroup.name}} requires groups to answer the following questions before joining", + "{{personOne}} and {{personTwo}}": "{{personOne}} and {{personTwo}}", + "{{totalTopicsCached}} Total Topics": "{{totalTopicsCached}} Total Topics" +} diff --git a/public/locales/es.json b/public/locales/es.json new file mode 100644 index 000000000..e3908cb84 --- /dev/null +++ b/public/locales/es.json @@ -0,0 +1,982 @@ +{ + " app to receive push notifications.": "app para recibir notificaciones push", + "(Remember to save your changes before leaving this form)": "(Recuerda guardar tus cambios antes de salir de este formulario)", + "(explanation required)": "(explicación requerida)", + "+ Create Group": "+ Crear Grupo", + "+ Create an event": "+ Crear un evento", + "+ New": "+ Nuevo", + "+ New post": "+ Nueva publicación", + "+ New project": "+ Nuevo proyecto", + "1 Group is a part of {{group.name}}": "1 Grupo es parte de {{group.name}}", + "1 other": "1 otro", + "404 Not Found": "404 No encontrado", + "A": "A", + "A role must have a valid emoji and name to be saved": "Un rol debe tener un emoji y un nombre válidos para ser guardado", + "A role must have a valid emoji and name to be updated": "Un rol debe tener un emoji y un nombre válidos para ser actualizado", + "About": "Acerca de", + "About Me": "Acerca de mí", + "About Video URL": "Acerca de la URL del video", + "About us": "Acerca de nosotros", + "About {{name}}": "Acerca de {{name}}", + "Abusive": "Abusivo", + "Accept Contributions": "Aceptar contribuciones", + "Access": "Acceso", + "Access to your email address": "Acceso a su dirección de correo electrónico", + "Access to your email address.": "Acceso a su dirección de correo electrónico", + "Access to your phone number": "Acceso a tu número de teléfono", + "Access to your phone number.": "Acceso a tu número de teléfono", + "Access to your physical address": "Acceso a tu dirección física", + "Access to your physical address.": "Acceso a tu dirección física", + "Access your profile, including your name and image": "Acceso a tu perfil, incluyendo tu nombre e imagen", + "Access your profile, including your name and image.": "Acceso a tu perfil, incluyendo tu nombre e imagen", + "Account": "Cuenta", + "Account already exists": "la cuenta ya existe", + "Active": "Activo", + "Add": "Agregar", + "Add Affiliation": "Agregar afiliación", + "Add File": "Agregar archivo", + "Add Member to Role": "Agregar miembro al rol", + "Add New": "Agregar nuevo", + "Add Preview": "Agregar vista previa", + "Add Topic": "Agregar tema", + "Add a Skill or Interest": "Agregar una habilidad o interés", + "Add a Topic": "Agregar un tema", + "Add a comment": "Agregar un comentario", + "Add a comment...": "Agregar un comentario...", + "Add a description": "Agregar una descripción", + "Add a description, location, suggested topics and more in your group settings": "Agregar una descripción, ubicación, temas sugeridos y más en la configuración de tu grupo", + "Add a donation link (must be valid URL)": "Agregar un enlace de donación (debe ser una URL válida)", + "Add a group description": "Agregar una descripción del grupo", + "Add a new question": "Agregar una nueva pregunta", + "Add a project management link (must be valid URL)": "Agregar un enlace de administración de proyectos (debe ser una URL válida)", + "Add a relevant skill or interest": "Agregar una habilidad o interés relevante", + "Add a skill you want to learn": "Agregar una habilidad que quieras aprender", + "Add a suggested topic": "Agregar un tema sugerido", + "Add custom links or filtered post views to your group's navigation": "Agregue enlaces personalizados o vistas de publicaciones filtradas a la navegación de su grupo", + "Add new affiliation": "Agregar nueva afiliación", + "Add someone": "Agregar a alguien", + "Add your location to see more relevant content, and find people and projects around you": "Agregue su ubicación para ver contenido más relevante y encontrar personas y proyectos a su alrededor", + "Affiliations": "Afiliaciones", + "All": "Todo", + "All Groups": "Todos los grupos", + "All My Groups": "Todos mis grupos", + "All Posts": "Todas las publicaciones", + "All Posts Marked Public": "Todas las publicaciones públicas", + "All Topics": "Todos los temas", + "All topics": "Todos los temas", + "Allow": "Permitir", + "Almost done setting up your profile! Click the above profile icon to upload a custom profile image. Your profile image will be visible when you post or comment in groups.": "¡Ya casi has terminado de configurar tu perfil! Haga clic en el ícono de perfil anterior para cargar una imagen de perfil personalizada. Su imagen de perfil será visible cuando publique o comente en grupos", + "Already Invited": "ya invitado", + "Already have an account?": "¿Ya tienes una cuenta?", + "Already logged in, redirecting...": "Ya logueado, redirigiendo", + "Amount": "Cantidad", + "An icon needs to be selected for the view": "Es necesario seleccionar un icono para la vista.", + "An icon needs to be selected for the view.": "Es necesario seleccionar un icono para la vista.", + "Animal welfare": "Bienestar animal", + "Announcements": "Anuncios", + "Anyone can join ": "Cualquiera puede unirse a", + "Anyone can join this group!": "¡Cualquiera puede unirse a este grupo!", + "Anyone on Hylo can see this post": "Cualquier persona en Hylo puede ver esta publicación.", + "Anyone who can see this group can join it": "Cualquiera que pueda ver este grupo puede unirse a él.", + "Approve": "Aprobar", + "Are you sure you want to ": "Estás seguro que quieres", + "Are you sure you want to cancel your request to join {{groupName}}?": "¿Estás seguro de que deseas cancelar tu solicitud para unirte a {{groupName}}?", + "Are you sure you want to create a new join link? The current link won't work anymore if you do.": "¿Estás seguro de que deseas crear un nuevo enlace para unirse? El enlace actual ya no funcionará si lo haces", + "Are you sure you want to decline the invitation to join {{groupName}}?": "¿Estás seguro de que deseas rechazar la invitación para unirse a {{groupName}}?", + "Are you sure you want to delete the group {{name}}?": "¿Estás seguro de que deseas eliminar el grupo {{name}}?", + "Are you sure you want to delete this comment": "¿Estás seguro de que quieres eliminar este comentario?", + "Are you sure you want to delete this custom view?": "¿Estás seguro de que desea eliminar esta vista personalizada?", + "Are you sure you want to delete this groupTopic?": "¿Estás seguro de que desea eliminar este tema de grupo?", + "Are you sure you want to delete this post?": "¿Seguro que quieres eliminar esta publicación?", + "Are you sure you want to delete this unsaved role/badge?": "¿Estás seguro de que desea eliminar esta función/insignia no guardada?", + "Are you sure you want to delete your affiliation as {{affiliation.role}} {{affiliation.preposition}} {{affiliation.orgName}}?": "¿Estás seguro de que desea eliminar su afiliación como {{affiliation.role}} {{affiliation.preposition}} {{affiliation.orgName}}?", + "Are you sure you want to leave {{groupName}}?": "¿Estás seguro que quieres salir de {{groupName}}?", + "Are you sure you want to remove this comment?": "¿Estas seguro de que quieres eliminar este comentario?", + "Are you sure you want to remove {{item.name}}?": "¿Estás seguro de que desea eliminar {{item.name}}?", + "Are you sure you want to resend all Pending Invitations": "¿Estás seguro de que desea reenviar todas las invitaciones pendientes?", + "Are you sure you wish to remove this moderator?": "¿Estás seguro de que desea eliminar a este moderador?", + "Ask new members whether they have these skills and interests?": "Pregunte a los nuevos miembros si tienen estas habilidades e intereses.", + "At A Glance": "De un vistazo", + "Available": "Disponible", + "Back": "Atrás", + "Base Layer:": "Capa base:", + "Be the first to comment": "Sé el primero en comentar", + "Below is a list of every topic that any member of your group has used to date. You can choose to hide\n topics that you would prefer members of your group don't use, or pin topics to the top of the list\n to make sure people pay attention to posts in those topics": "A continuación se muestra una lista de todos los temas que cualquier miembro de su grupo ha utilizado hasta la fecha. Puede optar por ocultar\n los temas que preferiría que los miembros de su grupo no usen, o fijar temas en la parte superior de la lista\n para asegurarse de que las personas presten atención a las publicaciones en esos temas.", + "Below is a list of every topic that any member of your group has used to date. You can choose to hide\n topics that you would prefer members of your group don't use, or pin topics to the top of the list\n to make sure people pay attention to posts in those topics.": "A continuación se muestra una lista de todos los temas que cualquier miembro de su grupo ha utilizado hasta la fecha. Puede optar por ocultar\n los temas que preferiría que los miembros de su grupo no usen, o fijar temas en la parte superior de la lista\n para asegurarse de que las personas presten atención a las publicaciones en esos temas.", + "Benchmarking": "Evaluación comparativa", + "Big Grid": "Cuadrícula grande", + "Biodiversity": "Biodiversidad", + "Block this Member": "Bloquear a este miembro", + "Blocked Users": "Usuarios Bloqueados", + "Both": "Ambos", + "Bring your group together": "Reúne a tu grupo", + "Buy from us": "Compra con nosotros", + "Buy from {{group.name}}": "Comprar de {{group.name}}", + "CLOSE": "CERRAR", + "Can you go?": "¿Puedes ir?", + "Cancel": "Cancelar", + "Cancel Invite": "Cancelar invitación", + "Cancel Request": "Cancelar solicitud", + "Canceled": "Cancelado", + "Carbon markets": "Mercados de carbono", + "Card view": "Vista de tarjeta", + "Cards": "Tarjetas", + "Certifications": "Certificaciones", + "Change Map Layers": "Cambiar capas de mapa", + "Changes not saved": "Cambios no guardados", + "Changes won't be saved. Are you sure you want to cancel?": "Los cambios no se guardarán. \n¿Estas seguro que quieres cancelar?", + "Check your email": "Consultar su correo electrónico", + "Child Groups": "Subgrupos", + "Claims:": "Declaraciónes:", + "Click or press on it to copy it": "Haz clic o presione sobre él para copiarlo", + "Click the button below to create a free Stripe account (or connect an existing account). Once you've done that you will be able to accept contributions to Projects.": "Haz clic en el botón a continuación para crear una cuenta gratuita de Stripe (o conectar una cuenta existente). Una vez que hayas hecho eso, podrá aceptar contribuciones a Proyectos.", + "Click to Copy": "Haga clic para copiar", + "Close": "Cerrar", + "Close Drawer": "Cerrar cajón", + "Closed": "Cerrado", + "Collection": "Recopilación", + "Comments": "Comentarios", + "Communication & marketing": "Comunicación y Marketing", + "Community Topics": "Temas de la comunidad", + "Community map": "Mapa de la comunidad", + "Complete your profile": "Completa tu perfil", + "Completed": "Terminado", + "Completed: {{endTime}}": "Terminado: {{endTime}}", + "Confirm Password": "Confirmar Contraseña", + "Connect": "Conectar", + "Connect Stripe Account": "Conectar cuenta de Stripe", + "Contact Email": "Correo electrónico de contacto", + "Contact Phone": "Teléfono de contacto", + "Contact {{group.name}} to learn about their practices": "Contacta a {{group.name}} para conocer sus prácticas", + "Continue": "Continuar", + "Continue with Facebook": "Continuar con Facebook", + "Continue with Google": "Continuar con Google", + "Contribute": "Contribuir", + "Contribute to Hylo": "Contribuir a Hylo", + "Contributing Via Stripe": "Contribuir a través de Stripe", + "Contributions so far: {{totalContributions}}": "Contribuciones hasta ahora: {{totalContributions}}", + "Cooperatives": "Cooperativas", + "Copied!": "¡Copiado!", + "Copy": "Copiar", + "Copy Link": "Copiar enlace", + "Create": "Crear", + "Create & navigate": "Crear y navegar", + "Create Group": "Crear un grupo", + "Create Role": "Crear Rol", + "Create a Topic": "Crear un tema", + "Create a group": "Crear un grupo", + "Create a new movement, network, community or group!": "¡Crea un nuevo movimiento, red, comunidad o grupo!", + "Create a project that people can help with": "Crear un proyecto en el que la gente pueda ayudar", + "Create a request or offer!": "¡Crea una solicitud u oferta!", + "Create additional roles or badges for the group": "Crear roles o insignias adicionales para el grupo", + "Create new custom view": "Crear nueva vista personalizada", + "Create new role/badge": "Crear nuevo rol/insignia", + "Create topic \"#{{item.value}}\"": "Crear tema \"#{{item.value}}\"", + "Create topic \"{{item.value}}\"": "Crear tema \"{{item.value}}\"", + "Created topic #{{topicName}}": "tema creado #{{topicName}}", + "Current settings up to date": "Configuración actual actualizada", + "Currently, only groups you specify above will see this post": "Actualmente, solo los grupos que especifique arriba verán esta publicación", + "Custom View": "Vista personalizada", + "Custom Views": "Vistas personalizadas", + "Customize the invite email message (optional):": "Personaliza el mensaje de correo electrónico de invitación (opcional):", + "DELETE: CAUTION": "ELIMINAR: PRECAUCIÓN", + "Daily": "Diario", + "Deactivate": "Desactivar", + "Deactivate Account": "Desactivar cuenta", + "Decline": "Rechazar", + "Declined": "Rechazado", + "Declined Invitations & Requests": "Invitaciones rechazadas", + "Default Sort": "Clasificación predeterminada", + "Default Style": "Estilo predeterminado", + "Default Topics": "Temas predeterminados", + "Delete": "Eliminar", + "Delete Account": "Eliminar cuenta", + "Delete Group": "Eliminar grupo", + "Delete {{groupName}}": "Eliminar {{groupName}}", + "Description": "Descripción", + "Disconnect": "Desconectar", + "Discussions": "Discusiones", + "Display exact location": "Mostrar ubicación exacta", + "Display only nearest city and dont show on the map": "Mostrar solo la ciudad más cercana y no mostrar la ubicación sobre el mapa", + "Display only nearest city and show nearby location on the map": "Mostrar solo la ciudad más cercana y una ubicación aproximada en el mapa", + "Displaying": "Mostrando", + "Donation Link": "Enlace de donación", + "Download App": "Descargar aplicación", + "Download our": "Descarga nuestro", + "Ecosystem services": "Servicios de ecosistema", + "Edit": "Editar", + "Edit Profile": "Editar perfil", + "Edit welcome message": "Editar mensaje de bienvenida", + "Email": "Correo electrónico", + "Email address is not in a valid format": "La dirección de correo electrónico no tiene un formato válido", + "Email address not found": "Dirección de correo electrónico no encontrado", + "Email addresses of those you'd like to invite:": "Direcciones de correo electrónico de las personas a las que te gustaría invitar:", + "End Time must be after Start Time": "La hora de finalización debe ser posterior a la hora de inicio", + "Ended: {{endTime}}": "Terminado: {{endTime}}", + "Ends: {{endTime}}": "Finaliza: {{endTime}}", + "Enter a title": "Introduce un título", + "Enter a topic name:": "Introduce un nombre de tema:", + "Enter up to three topics...": "Introduce hasta tres temas...", + "Enter your email address and we'll send you an email that lets you reset your password.": "Introduce tu dirección de correo electrónico y te enviaremos un correo electrónico que te permitirá restablecer tu contraseña.", + "Enter your email to get started:": "Introduce tu correo electrónico para comenzar:", + "Enter your message here": "Introduce su mensaje aquí", + "Environmental impact": "Impacto medioambiental", + "Equipment sharing": "Compartir equipamiento", + "Event collaboration": "Colaboración en eventos", + "Events": "Eventos", + "Expire": "Expira", + "Explanation for Flagging": "Explicación para marcar", + "Explore": "Explorar", + "Export Data": "Exportar datos", + "Export Members": "Exportar miembros", + "External Link": "Enlace externo", + "External link": "Enlace externo", + "External link has to be a valid URL.": "El enlace externo debe ser una URL válida", + "Farm Details": "Detalles de la granja", + "Farm Surrounds & Posts": "Alrededores y publicaciones de la granja", + "Farm Type: ": "Tipo de granja:", + "Farm support": "Apoyo a la granja", + "Farm valuation": "Valoración de granjas", + "Farms": "Granjas", + "Featured:": "Presentado:", + "Features:": "Características:", + "Feedback & Support": "Sugerencias y Soporte", + "Files": "Archivos", + "Filter by topics and keywords": "Filtrar por temas y palabras clave", + "Filters": "Filtros", + "Find a member": "Encuentra un miembro", + "Find out what's happening around you, and groups you can join": "Descubre lo que sucede a tu alrededor y los grupos a los que puedes unirte", + "Find/add a topic": "Buscar/añadir un tema", + "Flag": "Reportar", + "For place based groups, draw the area where your group is active (or paste in GeoJSON here)": "Para grupos basados ​​en lugares, dibuje el área donde su grupo está activo (o pegue GeoJSON aquí)", + "Forgot password?": "¿Has olvidado tu contraseña?", + "GLOBAL": "GLOBAL", + "GROUP NOTIFICATIONS": "NOTIFICACIONES DE GRUPO", + "Gather your collaborators & people who share your interests": "Reúne a tus colaboradores y personas que comparten tus intereses", + "Generate a Link": "Generar un enlace", + "Get updates about this map view": "Recibe actualizaciones sobre esta vista de mapa", + "Go Back": "Regresa", + "Go back": "Regresa", + "Going": "Yendo", + "Group": "Grupo", + "Group Access Questions": "Preguntas de acceso grupal", + "Group Explorer": "Explorador de Grupos", + "Group Invitations & Join Requests": "Invitaciones de grupo y solicitudes de ingreso", + "Group Name": "Nombre del grupo", + "Group Requesting to Join": "Grupo que solicita unirse", + "Group Settings": "Configuración de grupo", + "Group Shape": "Forma de grupo", + "Group Suggested Topics": "Grupo de temas sugeridos", + "Group menu": "Menú de grupo", + "Group {{locationDescriptor}}": "Grupo {{locationDescriptor}}", + "Groups": "Grupos", + "Groups & Affiliations": "Grupos y Afiliaciones", + "Hazard risk mitigation": "Mitigación del riesgo de peligro", + "Here you can switch between types of content and create new content for people in your group or everyone on Hylo!": "¡Aquí puedes cambiar entre los tipos de contenido y crear contenido nuevo para las personas de tu grupo o para todos en Hylo!", + "Hi there {{groupName}}, I'd like to talk about {{prompt}}.": "Hola {{groupName}}, me gustaría hablar sobre {{prompt}}.", + "Hi {{email}} we just need to know your name and password and you": "Hola, {{email}}, solo necesitamos saber tu nombre y contraseña y ya estás dentro.", + "Hi {{email}} we just need to know your name and password and you're in": "Hola, {{email}}, solo necesitamos saber tu nombre y contraseña y estarás dentro", + "Hi {{email}} we just need to know your name and password and you're in.": "Hola, {{email}}, solo necesitamos saber tu nombre y contraseña y estarás dentro.", + "Hi {{firstName}}, want to create an event?": "Hola, {{firstName}}, ¿quieres crear un evento?", + "Hi {{firstName}}, what are you looking for?": "Hola {{firstName}}, ¿qué estás buscando?", + "Hi {{firstName}}, what would you like to create?": "Hola, {{firstName}}, ¿qué te gustaría crear?", + "Hi {{firstName}}, what would you like to share?": "Hola, {{firstName}}, ¿qué te gustaría compartir?", + "Hi {{firstName}}, what's on your mind?": "Hola, {{firstName}}, ¿qué tienes en mente?", + "Hi!\n\nI'm inviting you to join {{name}} on Hylo.\n\n{{name}} is using Hylo for our online community: this is our dedicated space for communication & collaboration": "¡Hola!\n\nTe invito a unirte a {{name}} en Hylo.\n\n{{name}} está usando Hylo para nuestra comunidad en línea: este es nuestro espacio dedicado a la comunicación y la colaboración.", + "Hi!\n\nI'm inviting you to join {{name}} on Hylo.\n\n{{name}} is using Hylo for our online community: this is our dedicated space for communication & collaboration.": "¡Hola!\n\nTe invito a unirte a {{name}} en Hylo.\n\n{{name}} está usando Hylo para nuestra comunidad en línea: este es nuestro espacio dedicado a la comunicación y la colaboración.", + "Hidden": "Oculto", + "Hide posts from child groups": "Ocultar publicaciones de subgrupos", + "Hide posts from child groups you are a member of": "Ocultar publicaciones de subgrupos de los que eres miembro", + "Hide {{postType}} Data": "Ocultar datos de {{postType}}", + "Hide {{postType}} data for this group": "Ocultar datos de {{postType}} para este grupo", + "Home": "Hogar", + "How can people become members of": "¿Cómo pueden las personas convertirse en miembros de", + "How do we use cookies?": "Cómo usamos las cookies?", + "How often would you like to receive email digests for new posts in your groups and saved searches?": "¿Con qué frecuencia le gustaría recibir resúmenes por correo electrónico de nuevas publicaciones en sus grupos y búsquedas guardadas?", + "How would you like to receive notifications about": "¿Cómo le gustaría recibir notificaciones sobre", + "Hylo Groups": "Grupos Hylo", + "Hylo login & session": "Acceso y sesión de Hylo", + "Hylo logo": "logotipo de Hylo", + "Hylo uses cookies to enhance the user experience": "Hylo utiliza cookies para mejorar la experiencia del usuario", + "Hylo uses cookies to enhance the user experience.": "Hylo utiliza cookies para mejorar la experiencia del usuario", + "INVITE": "INVITAR", + "IS THIS GROUP A MEMBER OF OTHER GROUPS?": "¿ESTE GRUPO ES MIEMBRO DE OTROS GRUPOS?", + "Icon": "Icono", + "If you deactivate your account:": "Si desactivas tu cuenta:", + "If you delete this group, it will no longer be visible to you or any of the members. All posts will also be deleted": "Si elimina este grupo, ya no será visible para usted ni para ninguno de los miembros. Todas las publicaciones también serán eliminadas.", + "If you delete this group, it will no longer be visible to you or any of the members. All posts will also be deleted.": "Si elimina este grupo, ya no será visible para usted ni para ninguno de los miembros. Todas las publicaciones también serán eliminadas.", + "If you delete your account:": "Si eliminas tu cuenta:", + "If you don't want to display the detailed {{postType}} specific data on your group's profile": "Si no deseas mostrar los datos específicos detallados de {{postType}} en el perfil de su grupo", + "If you select a prerequisite group that has a visibility setting of": "Si selecciona un grupo de requisitos previos que tiene una configuración de visibilidad de", + "If you turn 'Accept Contributions' on, people will be able\n to send money to your Stripe connected account to support\n this project": "Si activa 'Aceptar contribuciones', las personas podrán\n enviar dinero a su cuenta conectada a Stripe para apoyar\n este proyecto", + "If you turn 'Accept Contributions' on, people will be able\n to send money to your Stripe connected account to support\n this project.": "Si activa 'Aceptar contribuciones', las personas podrán\n enviar dinero a su cuenta conectada a Stripe para apoyar\n este proyecto.", + "If your email address matched an account in our system, we sent you an email. Please check your inbox": "Si tu dirección de correo electrónico coincidía con una cuenta en nuestro sistema, le enviamos un correo electrónico. Por favor revise tu bandeja de entrada", + "If your email address matched an account in our system, we sent you an email. Please check your inbox.": "Si tu dirección de correo electrónico coincidía con una cuenta en nuestro sistema, le enviamos un correo electrónico. Por favor revise tu bandeja de entrada.", + "Illegal": "Ilegal", + "Images": "Imágenes", + "Import Posts by CSV": "Importar publicaciones por CSV", + "Import started!": "¡Importación iniciada!", + "In the meantime, click a topic from an individual\ngroup to see posts from that group": "Mientras tanto, haz clic en un tema de un grupo\nindividual para ver las publicaciones de ese grupo.", + "In the meantime, click a topic from an individual\ngroup to see posts from that group.": "Mientras tanto, haz clic en un tema de un grupo\nindividual para ver las publicaciones de ese grupo.", + "Inappropriate Content": "Contenido inapropiado", + "Include only active posts?": "¿Incluir solo publicaciones activas?", + "Include only posts that match any of these topics:": "Incluye solo publicaciones que coincidan con cualquiera de estos temas:", + "Included Posts": "Publicaciones incluidas", + "Increasing sales": "Aumento de ventas", + "Insetting": "En opciones", + "Interactions": "Interacciones", + "Intercom": "Intercom", + "Interested": "Interesado", + "Invalid code, please try again": "Código no válido, por favor vuelve a intentarlo", + "Invalid email address": "Dirección de correo electrónico no válida", + "Invalid url. Please enter the full url for your {{network}} page": "URL no válida. Ingrese la URL completa para su página de {{network}}.", + "Invalid url. Please enter the full url for your {{network}} page.": "URL no válida. Ingrese la URL completa para su página de {{network}}.", + "Invitations to Join New Groups": "Invitaciones para unirse a nuevos grupos", + "Invite": "Invitar", + "Invite 1 person": "invitar a 1 persona", + "Invite People": "Invitar a la gente", + "Invite a group to join": "Invitar a un grupo a unirse", + "Invite people to your event": "Invita a personas a tu evento", + "Invite {{invitedIds.length}} people": "Invita a {{invitedIds.length}} personas", + "Invites & Requests": "Invitaciones y Solicitudes", + "Is this offer still available?": "¿Sigue estando disponible esta oferta?", + "Is this project still active?": "¿Sigue activo este proyecto?", + "Is this request still needed?": "¿Sigue siendo necesaria esta solicitud?", + "Is this resource still available?": "¿Sigue disponible este recurso?", + "I’d love to show you how things work, would you like a quick tour?": "Me encantaría mostrarte cómo funcionan las cosas, ¿te gustaría un recorrido rápido?", + "Join": "Unirse", + "Join Hylo": "Únete a Hylo", + "Join Project": "Únete al proyecto", + "Join Request Approved": "Solicitud de ingreso aprobada", + "Join Requests": "Solicitudes de unión", + "Join group was unsuccessful": "Unirse al grupo no tuvo éxito", + "Join to see": "Únete para ver", + "Join {{group.name}} to another group": "Unirse {{group.name}} a otro grupo", + "Jump in to Hylo!": "¡Salta a Hylo!", + "Jump in!": "¡Saltar!", + "Label": "Etiqueta", + "Large Grid": "Cuadrícula grande", + "Latest activity": "Última actividad", + "Leave": "Salir", + "Leave Parent": "Salir padre", + "Leave Project": "Salir del proyecto", + "Let people know about available resources": "Informar a las personas sobre los recursos disponibles.", + "Link": "Enlace", + "Link Stripe Account": "Vincular cuenta de Stripe", + "Link expired, please start over": "Enlace caducado, por favor comience de nuevo", + "List": "Lista", + "List view": "Vista de la lista", + "Loading...": "Cargando...", + "Local storage & cache": "Almacenamiento local", + "Location": "Ubicación", + "Location & Hours": "Ubicación y horarios", + "Location Privacy:": "Privacidad de ubicación:", + "Log In": "Iniciar sesión", + "Log out": "Cerrar sesión", + "Low-cost loans": "Préstamos a bajo costo", + "MAKE AN ANNOUNCEMENT": "HACER UN ANUNCIO", + "Make Public:": "Hacer público:", + "Make sure you trust {{name}} with your information": "Asegúrate de confiar en {{name}} con tu información", + "Make sure you trust {{name}} with your information.": "Asegúrate de confiar en {{name}} con tu información", + "Management Techniques: ": "Técnicas de gestión:", + "Manual": "Manual", + "Map": "Mapa", + "Mark all as read": "Marcar todo como leido", + "Member": "Miembro", + "Member Count": "Cuenta de miembro", + "Member Profile": "Perfil de miembro", + "Member_plural": "Miembro_plural", + "Members": "Miembros", + "Membership Requested": "Membresía solicitada", + "Mentions": "Menciones", + "Mentorship & advice": "Tutoría", + "Message Member": "Enviar mensaje a miembro", + "Messages": "Mensajes", + "Messages, notifications & profile": "Mensajes, notificaciones y perfil", + "Mixpanel": "Mixpanel", + "Mobile App": "Aplicación movil", + "Moderator": "Moderador", + "Moderators": "Moderadores", + "Multiple people are typing...": "Varias personas están escribiendo...", + "Must be a valid URL!": "¡Debe ser una URL válida!", + "My Groups": "Mis grupos", + "My Home": "Mi hogar", + "My Posts": "Mis Publicaciones", + "My Skills & Interests": "Mis habilidades e intereses", + "NO MORE RECENT ACTIVITY": "NO MAS ACTIVIDAD RECIENTE", + "NOT": "NO ES", + "NOTIFICATIONS": "NOTIFICACIONES", + "Name": "Nombre", + "Name must not be blank": "El nombre no debe estar en blanco", + "Name of organization": "Nombre de la organización", + "Name of role": "Nombre del rol", + "Name this view": "Asigne un nombre a esta vista", + "Native Territories": "Territorios Nativos", + "Nearby Relevant Events": "Eventos relevantes cercanos", + "Nearby Relevant Groups": "Grupos relevantes cercanos", + "Nearby Relevant Offers and Requests": "Ofertas y solicitudes relevantes cercanas", + "Nearest": "Más cercano", + "Never": "Nunca", + "New": "Nuevo", + "New Comment on ": "Nuevo comentario sobre", + "New Group Joined": "Nuevo grupo unido", + "New Join Request": "Nueva Solicitud de Ingreso", + "New Message": "Nuevo mensaje", + "New Password": "Nueva contraseña", + "New Password (Confirm)": "Nueva contraseña (confirmar)", + "New Post at this location:": "Nueva publicación en esta ubicación:", + "New Post in ": "Nueva publicación en", + "New markets": "Nuevos mercados", + "New methods & practices": "Nuevos métodos y prácticas", + "Next": "Próximo", + "Next: Welcome to Hylo!": "Siguiente: ¡Bienvenido a Hylo!", + "Next: Where are you from?": "Siguiente: ¿De dónde eres?", + "No groups are members of {{group.name}} yet": "Ningún grupos son miembros de {{group.name}} todavía", + "No icon selected": "Ningún icono seleccionado", + "No longer needed": "Ya no es necesario", + "No messages found": "No se han encontrado mensajes", + "No more results": "No hay más resultados", + "No new join requests": "No hay nuevas solicitudes de unión", + "No notifications": "No notificaciones", + "No one is attending yet": "nadie asiste aun", + "No project members": "Ningún miembro del proyecto", + "No result": "Sin resultados", + "No results for this search": "No hay resultados para esta búsqueda", + "No thanks": "No, gracias", + "No unread notifications": "Sin notificaciones no leídas", + "No {{timeFrame}} events": "No hay {{timeFrame}} eventos", + "None": "Ninguno", + "Not Going": "No voy", + "Not a member of Hylo?": "¿No eres miembro de Hylo?", + "Not answered": "No contestado", + "Note: as a moderator you will always see the exact location displayed": "Nota: como moderador, siempre verá la ubicación exacta que se muestra", + "Nothing to see here": "No hay nada que ver aquí", + "Notifications": "Notificaciones", + "Notify me via": "Notifícame vía", + "Nutrient density": "Densidad de nutrientes", + "OFF": "APAGADO", + "ON": "ENCENDIDO", + "Off": "Apagado", + "Offensive": "Ofensivo", + "Offer": "Oferta", + "Offers": "Ofertas", + "Oh no, something went wrong! Check your internet connection and try again": "¡Oh no, algo salió mal! \nComprueba tu conexión a Internet e inténtalo de nuevo", + "Ok": "De acuerdo", + "On": "Encendido", + "One more step!": "¡Un paso más!", + "Only members of this group can see posts": "Solo los miembros de este grupo pueden ver las publicaciones.", + "Only parent groups can be added as prerequisite groups": "Solo se pueden agregar grupos principales como grupos de requisitos previos.", + "Only parent groups can be added as prerequisite groups.": "Solo se pueden agregar grupos principales como grupos de requisitos previos.", + "Oops! Something went wrong. Try reloading the page": "¡Ups! Algo salió mal. Intenta recargar la página", + "Oops! Something went wrong. Try reloading the page.": "¡Ups! Algo salió mal. Intenta recargar la página", + "Oops, there's nothing to see here": "Uy, no hay nada que ver aquí", + "Oops, there's nothing to see here.": "Uy, no hay nada que ver aquí", + "Open": "Abierto", + "Open Drawer": "Cajón abierto", + "Open Invitations to Join Other Groups": "Invitaciones abiertas para unirse a otros grupos", + "Open Messages": "Mensajes abiertos", + "Open Requests & Offers": "Solicitudes abiertas y ofertas", + "Operation: ": "Operación:", + "Opportunities to Collaborate": "Oportunidades para colaborar", + "Optimizely": "Optimizely", + "Optimizely helps us to test improvements to Hylo by showing different users different sets of features. Optimizely tracks who has seen what and how successful the feature is in accomplishing it's goal": "Optimizely nos ayuda a probar las mejoras de Hylo mostrando diferentes conjuntos de características a diferentes usuarios. Optimizely realiza un seguimiento de quién ha visto qué y qué tan exitosa es la función para lograr su objetivo", + "Or log in with": "O inicia sesión con", + "Or sign in with an existing account": "O inicie sesión con una cuenta existente", + "Other": "Otro", + "Other Affiliations": "Otras afiliaciones", + "Other Layers": "Otras capas", + "Other Roles & Badges": "Otros roles", + "Overview": "Descripción", + "PERSONAL": "PERSONAL", + "Parent Groups": "Grupos principales", + "Password": "Contraseña", + "Password (at least 9 characters)": "Contraseña (al menos 9 caracteres)", + "Passwords don't match": "Las contraseñas no coinciden", + "Passwords must be at least 9 characters long": "Las contraseñas deben tener al menos 9 caracteres", + "Passwords must be at least 9 characters long, and should be a mix of lower and upper case letters, numbers and symbols.": "Las contraseñas deben tener al menos 9 caracteres y deben ser una combinación de letras mayúsculas y minúsculas, números y símbolos.", + "Past Events": "Eventos pasados", + "Payment": "Pago", + "Pending": "Pendiente", + "Pending Invites": "Invitaciones pendientes", + "Pending invites to join {{group.name}}": "Invitaciones pendientes para unirse a {{group.name}}", + "Pending requests to join other groups": "Solicitudes pendientes para unirse a otros grupos", + "People": "Gente", + "People can apply to join this group and must be approved": "Las personas pueden solicitar unirse a este grupo y deben ser aprobadas", + "People want to join your group!": "¡La gente quiere unirse a tu grupo!", + "Pin": "Fijar", + "Please enter a URL slug": "Introduzca un slug de URL", + "Please enter a group name": "Por favor, introduzca un nombre de grupo", + "Please enter a valid email address": "Por favor, introduce una dirección de correo electrónico válida", + "Please enter the full url for your {{network}} page.": "Introduce la URL completa para tu página {{network}}", + "Please enter your password": "Por favor, introduzca tu contraseña", + "Please enter your twitter name": "Por favor introduce tu nombre de Twitter", + "Please enter your twitter name.": "Por favor introduce tu nombre de Twitter.", + "Please provide either a `token` query string parameter or `accessCode` route param": "Proporcione un parámetro de cadena de consulta `token` o un parámetro de ruta `accessCode`", + "Plural word used to describe group Moderators": "Palabra plural utilizada para describir moderadores de grupo", + "Popular": "Popular", + "Post": "Publicación", + "Post Collection": "Colección de publicaciones", + "Post Date": "Fecha de Publicación", + "Post Stream": "Flujo de Publicaciones", + "Post from": "Publicacion de", + "Post from child group": "Publicacion de subgrupo", + "Post in": "Publica en", + "Posted In:": "Publicado en:", + "Posting...": "Publicando...", + "Posts": "Publicaciones", + "Prerequisite Groups": "Grupos de requisitos previos", + "Press on the group name or icon to navigate within the current group or view. Discover events, discussions, resources & more!": "Pulse en el nombre o icono del grupo para navegar dentro del grupo o vista actual. ¡Descubre eventos, discusiones, recursos y más!", + "Previous": "Anterior", + "Privacy": "Privacidad", + "Privacy & Access": "Privacidad y Acceso", + "Privacy Policy": "Política de privacidad", + "Privacy settings": "La configuración de privacidad", + "Product quality": "Calidad del producto", + "Profile": "Perfil", + "Project Management": "Gestión de proyectos", + "Project Members": "Miembros del proyecto", + "Projects": "Proyectos", + "Projects help you and your group accomplish shared goals.": "Los proyectos te ayudan a ti y a tu grupo a lograr objetivos compartidos.", + "Protected": "Protegido", + "Public": "Público", + "Public Groups": "Grupos Públicos", + "Public Groups & Posts": "Grupos Públicos y Publicaciones", + "Public Map": "Mapa Público", + "Public Offerings": "Ofertas Públicas", + "Public Stream": "Frujo Público", + "Public stream": "Transmisión pública", + "Push Notifications": "Empujar notificaciones", + "Quick topic-based chats": "Charlas basadas en temas", + "REQUEST": "PEDIDO", + "RSVP": "Responda, por favor", + "Reactions": "Reacciones", + "Reactivate": "Reactivar", + "Read More": "Leer más", + "Recent": "Reciente", + "Recent Posts": "Publicaciones Recientes", + "Recently Active Members": "Miembros activos recientemente", + "Recently Active Projects": "Proyectos activos recientemente", + "Related Groups": "Grupos relacionados", + "Relevant skills & interests": "Habilidades relevantes", + "Remove": "Eliminar", + "Remove Child": "Eliminar subgrupos", + "Remove From Group": "Sacar del grupo", + "Remove Post": "Eliminar publicación", + "Remove from group as well": "Eliminar del grupo también", + "Remove post?": "¿Quitar publicación?", + "Reply to": "Responder a", + "Request": "Petición", + "Request Membership in": "Solicitar Membresía en", + "Request expired, please start over": "Solicitud caducada, vuelve a empezar", + "Request to Join": "Solicitud de ingreso", + "Request to join pending": "Solicitud de ingreso pendiente", + "Requested": "Solicitado", + "Requests": "Peticiones", + "Requests to join {{group.name}}": "Solicitudes para unirse a {{group.name}}", + "Require groups to answer questions when requesting to join this group": "Requerir que los grupos respondan preguntas cuando soliciten unirse a este grupo", + "Require people to answer questions when requesting to join this group": "Requerir que las personas respondan preguntas cuando soliciten unirse a este grupo", + "Research projects": "Proyectos de investigación", + "Resend": "Reenviar", + "Resend All": "Reenviar todo", + "Resend code": "Reenviar codigo", + "Reset": "Reiniciar", + "Reset Link": "Restablecer enlace", + "Reset Your Password": "Restablecer su contraseña", + "Resources": "Recursos", + "Responses": "Respuestas", + "Restricted": "Restringido", + "Restricted group, no request pending": "Grupo restringido, ninguna solicitud pendiente", + "Return to All Groups": "Volver a Todos los grupos", + "Revert": "Revertir", + "Roles & Badges": "Roles y insignias", + "STEP 1/3": "PASO 1/3", + "STEP 2/3": "PASO 2/3", + "Save": "Guardar", + "Save Changes": "Guardar cambios", + "Save this view": "Guardar esta vista", + "Saved Searches": "Búsquedas guardadas", + "Saved Views": "Vistas guardadas", + "Search": "Buscar", + "Search by keyword for people, posts and groups": "Buscar personas, publicaciones y grupos por palabra clave", + "Search by name or skills & interests": "Buscar por nombre o habilidades e intereses", + "Search for a location...": "Buscar una ubicación...", + "Search for people...": "Búsqueda de personas", + "Search for posts": "Buscar publicaciones", + "Search for posts & people. Send messages to group members or people you see on Hylo. Stay up to date with current events and edit your profile.": "Buscar publicaciones y personas. Envía mensajes a miembros del grupo o personas que vea en Hylo. Mantente al día con los eventos actuales y edita tu perfil.", + "Search groups by keyword": "Buscar grupos por palabra clave", + "Search here for members to grant moderator powers": "Busque aquí miembros para otorgar poderes de moderador", + "Search here for members to grant this role too": "Busque aquí miembros para otorgar este rol también", + "Search members": "Buscar miembros", + "Search posts": "Buscar publicaciones", + "Search topics": "Buscar temas", + "Search {{count}} topics": "Buscar {{count}} temas", + "Search {{count}} topics_plural": "Buscar {{count}} temas_plural", + "Select End": "Fin", + "Select Start": "Inicio", + "Select a reason": "Seleccione una razón", + "Select people to invite": "Seleccionar personas para invitar", + "Send Invite": "Enviar invitación", + "Send Invites via email": "Enviar invitaciones por correo electrónico", + "Send It": "Envíalo", + "Send me a digest": "Envíame un resumen", + "Sent": "Enviado", + "Sent {{numGood}} {{email}}": "Enviado {{numGood}} {{email}}", + "Set default topics for your group which will be suggested first when\n members are creating a new post.\n Every new member will also be subscribed to these topics when they join.": "Establece temas predeterminados para tu grupo que se sugerirán primero cuando\n los miembros estén creando una nueva publicación.\n Cada nuevo miembro también se suscribirá a estos temas cuando se una.", + "Set your password here.": "Establece tu contraseña aquí.", + "Settings": "Ajustes", + "Share a Join Link": "Compartir un enlace para unirse", + "Share about who you are, your skills & interests": "Comparte quién eres, tus habilidades e intereses", + "Show Less": "Muestra menos", + "Show me Hylo": "Muéstrame Hylo", + "Show posts from child groups": "Mostrar publicaciones de subgrupos", + "Show posts from child groups you are a member of": "Mostrar publicaciones de los subgrupos que eres miembro", + "Sign Up": "Inscribirse", + "Sign in": "Iniciar sesión", + "Sign in to Hylo": "Iniciar sesión en Hylo", + "Signup": "Inscribirse", + "Signup or Login to connect with": "Regístrate o inicia sesión para conectarte con", + "Skills & Interests": "Habilidades e intereses", + "Small Grid": "Cuadrícula pequeña", + "Social Accounts": "Cuentas redes sociales", + "Social media link for {{group.name}}": "Enlace de redes sociales para {{group.name}}", + "Something weird happened during consent process": "Algo extraño sucedió durante el proceso de consentimiento", + "Sorry, that Email and Password combination didn't work.": "Lo sentimos, esa combinación de correo electrónico y contraseña no funcionó.", + "Sorry, your invitation to this group is expired, has already been used, or is invalid. Please contact a group moderator for another one.": "Lo sentimos, tu invitación a este grupo caducó, ya se usó o no es válida. Pónte en contacto con un moderador de grupo para obtener otra nueva.", + "Sort by": "Ordenar por", + "Sort posts by:": "Ordenar publicaciones por:", + "Spam": "Correo basura", + "Start a Group": "iniciar un grupo", + "Start typing to add a topic": "Comienza a escribir para agregar un tema", + "Start typing to find/create a topic to add": "Comience a escribir para encontrar/crear un tema para agregar", + "Started: {{from}}": "Empezó desde: {{from}}", + "Starts: {{from}}": "Comienza: {{desde}}", + "Stay connected, organized, and engaged with your group.": "Manténte conectado, organizado y comprometido con tu grupo.", + "Stream": "Flujo", + "Subgroups": "Subgrupos", + "Submit": "Enviar", + "Subscribe": "Suscribir", + "Support this project": "Apoya este proyecto", + "Support this project on": "Apoya este proyecto en", + "Switching groups & viewing all": "Cambio de grupos y vistas", + "Tagline": "Lema", + "Talk about what's important with others": "Hablar de lo importante con los demás.", + "Talk about whats important with others": "Hablar de lo importante con los demás.", + "Terms & Privacy": "Términos y Privacidad", + "Terms of Service": "Términos de servicio", + "Thanks for your contribution!": "¡Gracias por tu contribución!", + "The content of your posts and comments will be removed": "El contenido de tus publicaciones y comentarios será eliminado.", + "The moderators go here": "Los moderadores van aquí.", + "The {{title}} section is not visible to members of this group. Click the three dots": "La sección {{title}} no es visible para los miembros de este grupo. \nHaz clic en los tres puntos", + "There was a problem processing your payment. Please check your card details and try again.": "Hubo un problema al procesar su pago. Comprueba los datos de tu tarjeta e inténtalo de nuevo.", + "There was a problem with your request. Please check your email and try again.": "Hubo un problema con su solicitud. Por favor revise su correo electrónico y vuelva a intentarlo.", + "There was an error, please try again.": "Hubo un error, por favor intente de nuevo.", + "There was an issue registering your stripe account. Please try again. If the problem persists, contact us.": "Hubo un problema al registrar su cuenta de Stripe. Inténtalo de nuevo. Si el problema persiste, contáctenos.", + "These are the {{length}} groups that {{group.name}} is a member of": "Estos son los {{length}} grupos de los que {{group.name}} es miembro", + "These {{childGroups.length}} groups are members of {{group.name}}": "Estos {{childGroups.length}} grupos son miembros de {{group.name}}", + "This URL already exists. Try another.": "Esta URL ya existe. Intenta otra.", + "This action is": "Esta acción", + "This action is reversible, just log back in": "Esta acción es reversible, simplemente vuelve a iniciar sesión", + "This function exports all member data for this group as a CSV file for import into other software.": "Esta función exporta todos los datos de los miembros de este grupo como un archivo CSV para importarlo a otro software.", + "This group has prerequisite groups you cannot see, you cannot join this group at this time": "Este grupo tiene grupos de requisitos previos que no puedes ver, no puedes unirse a este grupo en este momento", + "This group is a member": "Este grupo es miembro", + "This group is invitation only": "Este grupo es solo por invitación", + "This is group is invitation only": "Este es un grupo solo por invitación", + "This is still needed": "esto todavía es necesario", + "This is the one group": "Este es el único grupo", + "This is where we show you which group or other view you are looking at. Click here to return to the home page.": "Aquí es donde le mostramos qué grupo u otra vista está viendo. Haz clic aquí para volver a la página de inicio.", + "This list automatically shows which groups on Hylo you are a part of. You can also share your affiliations with organizations that are not currently on Hylo.": "Esta lista muestra automáticamente a qué grupos de Hylo perteneces. También puedes compartir tus afiliaciones con organizaciones que no están actualmente en Hylo.", + "This list contains all open requests and invitations to join groups.": "Esta lista contiene todas las solicitudes abiertas e invitaciones para unirse a grupos.", + "This means that all members of the {{groupCount}} groups selected will receive an instant email and push notification about this Post.": "Esto significa que todos los miembros de los {{groupCount}} grupos seleccionados recibirán un correo electrónico instantáneo y una notificación automática sobre esta publicación.", + "This means that all members of the {{groupCount}} groups selected will receive instant email and push notifications about this Post. (This feature is available to moderators only.": "Esto significa que todos los miembros de los {{groupCount}} grupos seleccionados recibirán un correo electrónico instantáneo y una notificación automática sobre esta publicación (función solo disponible para moderadores).", + "This means that all members of this group will receive an instant email and push notification about this Post. (This feature is available to moderators only.": "Esto significa que todos los miembros de los {{groupCount}} grupos seleccionados recibirán un correo electrónico instantáneo y una notificación automática sobre esta publicación (función solo disponible para moderadores).", + "This menu allows you to switch between groups, or see updates from all your groups at once.\n\nWant to see what else is out there? Navigate over to Public Groups & Posts to see!": "Este menú le permite cambiar entre grupos o ver actualizaciones de todos sus grupos a la vez.\n\n\n¿Quieres ver qué más hay por ahí? \nNavega a Grupos públicos", + "This project is being managed on": "Este proyecto está siendo gestionado en", + "This video will be full-width, displayed above the description, and playable.": "Este video será de ancho completo, se mostrará encima de la descripción y se podrá reproducir.", + "This will allow {{appName}} to:": "Esto permitirá que {{appName}}:", + "This will only be sent as an Announcement to the groups where you are a Moderator. For other groups it will be shared as a regular Post.": "Esto solo se enviará como un Anuncio a los grupos en los que sea Moderador. Para otros grupos, se compartirá como una publicación normal.", + "Timeframe": "Periodo de tiempo", + "Title can't have more than {{maxTitleLength}} characters": "El título no puede tener más de {{maxTitleLength}} caracteres", + "To": "A", + "To accept financial contributions for this project, you have\n to connect a Stripe account. Go to": "Para aceptar contribuciones financieras para este proyecto, debes\n conectar una cuenta de Stripe. Ve a \npara conectar una cuenta de Stripe. \nIr a", + "To follow the tour look for the pulsing beacons!": "¡Para seguir el recorrido busca las balizas pulsantes!", + "To join": "Unir", + "To view all groups you are a part of go to your": "Para ver todos los grupos de los que eres parte, ve a tus", + "Topic Created": "Tema creado", + "Topic List Editor": "Editor de listas de temas", + "Topic name is required.": "El nombre del tema es obligatorio.", + "Topics": "Temas", + "Topics must be longer than 2 characters": "Los temas deben tener más de 2 caracteres", + "Type": "Tipo", + "Type email addresses (multiples should be separated by either a comma or new line)": "Escribe las direcciones de correo electrónico (si son varias, deben estar separadas por una coma o una nueva línea)", + "Type group name...": "Escribe el nombre del grupo...", + "Type persons name...": "Escribe su nombre...", + "Type your answer here...": "Escribe su respuesta aquí...", + "Type...": "Tipo...", + "URL of organization": "URL de la organización", + "URLs must have between 2 and 40 characters, and can only have lower case letters, numbers, and dashes.": "Las URL deben tener entre 2 y 40 caracteres y solo pueden tener letras minúsculas, números y guiones.", + "Unavailable": "No disponible", + "Unblock": "Desbloquear", + "Unlink": "Desvincular", + "Unpin": "Desprender", + "Unread": "No leído", + "Unsubscribe": "Darse de baja", + "Upcoming Events": "Próximos Eventos", + "Update Account": "Actualizar cuenta", + "Upload CSV": "Subir CSV", + "Upload a profile image": "Subir una imagen de perfil", + "Upvotes": "Votos a favor", + "Use your Hylo account to access {{name}}.": "Usa tu cuenta de Hylo para acceder a {{name}}", + "View": "Vista", + "View Current Members": "Ver miembros actuales", + "View all": "Ver todo", + "View and participate in public discussions, projects, events & more": "Ver y participar en discusiones públicas, proyectos, eventos", + "View details": "Ver detalles", + "View name needs to be at least two characters long.": "El nombre de la vista debe tener al menos dos caracteres.", + "View project management tool": "Ver herramienta de gestión de proyectos", + "View tasks": "Ver tareas", + "View the public map": "Ver el mapa público", + "Visibility": "Visibilidad", + "Visible": "Visible", + "Visit": "Visita", + "Volunteer opportunities": "Oportunidades para voluntarios", + "WARNING: This is a beta feature that at this time will not inform you of import errors, use at your own risk.": "ADVERTENCIA: Esta es una función beta que en este momento no le informará sobre errores de importación, utilícela bajo su propio riesgo.", + "WHO CAN JOIN THIS GROUP?": "¿QUIÉN PUEDE UNIRSE A ESTE GRUPO?", + "WHO CAN SEE THIS GROUP?": "¿QUIÉN PUEDE VER ESTE GRUPO?", + "Warning:": "Advertencia:", + "We store images, icons and application data in your browser to improve performance and load times.": "Almacenamos imágenes, íconos y datos de aplicaciones en su navegador para mejorar el rendimiento y los tiempos de carga.", + "We use a service called Mixpanel to understand how people like you use Hylo. Your identity is anonymized but your behavior is recorded so that we can make improvements to Hylo based on how people are using it.": "Usamos un servicio llamado Mixpanel para comprender cómo las personas como usted usan Hylo. Su identidad se anonimiza, pero su comportamiento se registra para que podamos realizar mejoras en Hylo en función de cómo las personas lo usan.", + "We use cookies to help understand whether you are logged in and to understand your preferences and where you are in Hylo.": "Usamos cookies para ayudar a comprender si ha iniciado sesión y para comprender sus preferencias y dónde se encuentra en Hylo.", + "We'll notify you by email when someone wants to join": "Te avisaremos por correo electrónico cuando alguien quiera unirse", + "We're almost done, are you sure you want to cancel?": "Ya casi terminamos, ¿estás seguro de que deseas cancelar?", + "We're glad you're here, {{firstName}}. To get started, explore public groups and posts, or create your own group!": "Nos alegra que estés aquí, {{firstName}}. \nPara comenzar, explora los grupos públicos y las publicaciones, ¡o crea tu propio grupo!", + "We're happy you're here! Please take a moment to explore this page to see what's alive in our group. Introduce yourself by clicking Create to post a Discussion sharing who you are and what brings you to our group. And don't forget to fill out your profile - so likeminded new friends can connect with you!": "¡Estamos felices de que estés aquí! Tómate un momento para explorar esta página y ver lo que está sucediendo en nuestro grupo. Preséntate haciendo clic en Crear para publicar una discusión compartiendo quién eres y qué te trae a nuestro grupo. Y no olvides completar tu perfil, ¡para que nuevos amigos con ideas afines puedan conectarse contigo!", + "We're working on expanding\n#topics to more places": "Estamos trabajando en ampliar\n #temas a más lugares", + "We've sent a 6 digit code to {{email}}. The code will expire shortly, so please enter it here soon.": "Hemos enviado un código de 6 dígitos a {{email}}. El código caducará en breve, así que ingrésalo aquí pronto.", + "Website": "Sitio web", + "Website for {{group.name}}": "Sitio web para {{group.name}}", + "Weekly": "Semanalmente", + "Welcome": "Bienvenido", + "Welcome Message": "Mensaje de bienvenida", + "Welcome to Hylo": "Bienvenido a Hylo", + "Welcome to Hylo!": "¡Bienvenido a Hylo!", + "Welcome to {{group.name}}!": "¡Bienvenido a {{group.name}}!", + "What I'm Learning": "lo que estoy aprendiendo", + "What I'm learning": "lo que estoy aprendiendo", + "What are you doing together?": "¿Qué están haciendo juntos?", + "What are you looking for help with?": "¿Para qué estás buscando ayuda?", + "What area does your group cover?": "¿Qué área cubre tu grupo?", + "What can people help you with?": "¿En qué te puede ayudar la gente?", + "What do you have for others?": "¿Qué tienes para los demás?", + "What do you need? What are you offering?": "¿Qué necesitas? \n¿Qué estás ofreciendo?", + "What do you want to see on the map?": "¿Qué quieres ver en el mapa?", + "What help can you offer?": "¿Qué ayuda puedes ofrecer?", + "What is your event called?": "¿Cómo se llama tu evento?", + "What post types to display?": "¿Qué tipos de publicaciones mostrar?", + "What questions are asked when a group requests to join this group?": "¿Qué preguntas se hacen cuando un grupo solicita unirse a este grupo?", + "What resource is available?": "¿Qué recurso está disponible?", + "What skills and interests are most relevant to your group?": "¿Qué habilidades e intereses son más relevantes para tu grupo?", + "What skills and interests are particularly relevant to this group?": "¿Qué habilidades e intereses son particularmente relevantes para este grupo?", + "What skills and interests do you have?": "¿Qué habilidades e intereses tienes?", + "What skills do you want to learn?": "¿Qué habilidades quieres aprender?", + "What was wrong?": "¿Que está mal?", + "What would you like to call your project?": "¿Cómo te gustaría llamar a tu proyecto?", + "What would you like to create?": "¿Qué te gustaría crear?", + "What you will do at your next event?": "¿Qué harás en tu próximo evento?", + "Whats on your mind?": "¿Qué tienes en mente?", + "What’s on your mind?": "¿Qué tienes en mente?", + "When people on Hylo need help or want to report a bug, they are interacting with a service called intercom. Intercom stores cookies in your browser to keep track of conversations with us, the development team.": "Cuando las personas en Hylo necesitan ayuda o quieren informar un error, interactúan con un servicio llamado Intercom. Intercom almacena cookies en tu navegador para realizar un seguimiento de las conversaciones con nosotros, el equipo de desarrollo.", + "When you select a prerequisite group, people must join the selected groups before joining": "Cuando seleccionas un grupo de requisitos previos, las personas deben unirse a los grupos seleccionados antes de unirse", + "Where do you call home?": "¿A dónde llamas hogar?", + "Where is your {{type}} located?": "¿Dónde se encuentra tu {{type}}?", + "Which of the following skills & interests are relevant to you?": "¿Cuál de las siguientes habilidades e intereses son relevantes para ti?", + "Who has access to group settings and moderation powers": "Quién tiene acceso a la configuración del grupo y derechos de moderación", + "Who is able to see": "quien es capaz de ver", + "Why was this {{type}} '{{selectedCategory}}'{{required}}?": "¿Por qué {{type}} '{{selectedCategory}}'{{required}}?", + "Will open this URL in a new tab": "Abrirá esta URL en una nueva pestaña", + "With:": "Con:", + "Word used to describe a group Moderator": "Palabra utilizada para describir a un moderador de grupo", + "Write something...": "Escribe algo...", + "You": "Tú", + "You already have a stripe account linked to this account. If you would like to link a different account, click the button below.": "Ya tienes una cuenta de Stripe vinculada a esta cuenta. Si deseas vincular una cuenta diferente, haz clic en el siguiente botón.", + "You are a member of": "Eres miembro de", + "You are here!": "¡Estás aquí!", + "You are requesting that": "Estas pidiendo eso", + "You can only select up to {{MAX_TOPICS}} topics": "Solo puedes seleccionar hasta {{MAX_TOPICS}} temas", + "You can select a CSV file to import posts into {{name}}. Posts will be created by you. The file must have columns with the following headers:": "Puede seleccionar un archivo CSV para importar publicaciones en {{name}}. \nLas publicaciones serán creadas por usted. \nEl archivo debe tener columnas con los siguientes encabezados:", + "You don't have any messages yet": "Aún no tienes ningún mensaje.", + "You have no active messages": "No tienes mensajes activos", + "You have unsaved changes, are you sure you want to leave?": "Tienes cambios sin guardar, ¿estás seguro de que quieres salir?", + "You may add parent groups if you are a moderator of the group you wish to add, or if the group you wish to add has the Open access setting which allows any group to join it": "Puede agregar grupos principales si eres moderador del grupo que deseas agregar, o si el grupo que deseas agregar tiene la configuración de acceso abierto que permite que cualquier grupo se una.", + "You should receive an email with the member export in a few minutes": "Deberás recibir un correo electrónico con la exportación de miembros en unos minutos.", + "You wish to turn {{onOrOff}} {{type}} for all groups? This will affect {{numGroups}} {{groups}}.": "¿Desea activar {{onOrOff}} {{type}} para todos los grupos? Esto afectará a {{numGroups}} {{groups}}", + "You won't be able to use Hylo unless you create a brand new account": "No podrás usar Hylo a menos que creas una cuenta nueva", + "You won't be able to use Hylo unless you log back in": "No podrás usar Hylo a menos que vuelvas a iniciar sesión", + "You won't receive platform notifications": "No recibirás notificaciones de la plataforma", + "Your Open Requests to Join Groups": "Tus solicitudes abiertas para unirse a grupos", + "Your Settings": "Tu configuración", + "Your account and its details will be deleted": "Tu cuenta y sus detalles serán eliminados", + "Your account has no password set.": "Tu cuenta no tiene contraseña establecida", + "Your account is registered, you're ready to accept contributions to projects.": "Your account is registered, you're ready to accept contributions to projects.", + "Your affiliations with organizations": "Tus afiliaciones con organizaciones", + "Your comments and posts will REMAIN as they are": "Tus comentarios y publicaciones PERMANECERÁN como están", + "Your email address": "Tu dirección de correo electrónico", + "Your group doesn't have a description": "Tu grupo no tiene descripción", + "Your group has been invited": "Tu grupo ha sido invitado.", + "Your group's name": "El nombre de tu grupo", + "Your profile won't show up in any member searches or group memberships": "Tu perfil no aparecerá en ninguna búsqueda de miembros o membresías grupales", + "above this box to change the visibility settings. Only moderators can see this message": "encima de este cuadro para poder cambiar la configuración de visibilidad. \nSolo los moderadores pueden ver este mensaje.", + "and": "y", + "and become a member": "y hazte miembro", + "animal welfare": "bienestar animal", + "approved your request to join": "aprobada tu solicitud para unirte", + "are a member": "eres un miembro", + "are attending": "están asistiendo", + "are members": "son miembros", + "are you sure you want to remove {{name}}?": "¿Estás seguro de que desea eliminar {{name}}?", + "asked to join": "pidió unirse", + "at": "a", + "attachments": "archivos adjuntos", + "attending": "asistiendo", + "back": "atrás", + "become a member of": "convertirse en miembro de", + "benchmarking your farm": "evaluación comparativa de tu granja", + "biodiversity": "biodiversidad", + "carbon markets": "mercados de carbono", + "certifications": "certificaciones", + "child group": "subgroupo", + "commented": "comentado", + "commented on": "comentado", + "contributed to a project": "contribuido a un proyecto", + "contributed to your project": "contribuido a tu proyecto", + "contributed {{amount}} to \"{{postSummary}}\"": "contribuyó {{cantidad}} a \"{{postSummary}}\"", + "cooperatives": "cooperativas", + "deactivate": "desactivar", + "description: text": "descripción: texto", + "discusion": "Discusión", + "discussion": "Discusión", + "ecosystem services": "servicios de ecosistema", + "end_date (optional): e.g. 20200731-12:23:12.000+00 (other date formats may work)": "end_date (opcional): p. \n20200731-12:23:12.000 00 (pueden funcionar otros formatos de fecha)", + "environmental impact": "impacto medioambiental", + "equipment sharing": "compartir equipmiento", + "event": "evento", + "event collaboration": "colaboración en eventos", + "farm support": "apoyo a la granja", + "farm valuation": "valoración de la granja", + "filtered by topics:": "filtrado por temas:", + "for": "para", + "from": "de", + "group": "grupo", + "group privacy settings": "configuración de privacidad del grupo", + "groups": "grupos", + "has invited": "ha invitado", + "has joined": "se ha unido", + "has requested to join": "ha solicitado unirse", + "have a valid emoji and name to be updated": "Un rol debe tener un emoji y un nombre válidos para ser actualizado", + "hazard risk mitigation": "mitigación de riesgos", + "helping with your communication & marketing": "ayudando con tu comunicación y marketing", + "image_urls: 1 or more image URLs separated by spaces and/or commas": "image_urls: 1 o más URL de imágenes separadas por espacios y/o comas", + "increasing sales": "aumentar las ventas", + "insetting": "en opciones", + "introducing new methods & practices": "introduciendo nuevos métodos y prácticas", + "invited you to an event": "te invitó a un evento", + "invited you to join": "te invitó a unirte", + "invited you to: \"{{postSummary}}\"": "te invitó a: \"{{postSummary}}\"", + "is a member": "es un miembro", + "is attending": "asiste", + "is only accessible to members of": "sólo es accesible para los miembros de", + "is typing...": "esta escribiendo...", + "is_public: true or false": "is_public: verdadero o falso", + "location: text": "ubicación: texto", + "low-cost loans": "préstamos de bajo costo", + "member": "miembro", + "member_plural": "miembro_plural", + "mentioned you": "te mencionó", + "mentioned you in a comment on": "te mencione en un comentario", + "mentorship & advice": "mentoría y consejo", + "new comments on posts you're following?": "¿nuevos comentarios en las publicaciones que estás siguiendo?", + "new markets": "nuevos mercados", + "no more than {{maxTags}} allowed": "no más de {{maxTags}} permitido", + "nutrient density": "densidad de nutrientes", + "of": "de", + "offer": "Oferta", + "only members of those groups will be able to join this group. Because of these settings, people who find your group will not be able to see the prerequisite group.": "solo los miembros de esos grupos podrán unirse a este grupo. Debido a esta configuración, las personas que encuentren su grupo no podrán ver el grupo de requisitos previos.", + "or": "o", + "or press Enter": "o presione Entrar", + "others": "otras", + "past": "pasado", + "posts": "publicaciones", + "product quality": "calidad del producto", + "project": "proyecto", + "purchasing from you": "comprando de tí", + "reactivate": "reactivar", + "reason-required": "motivación requerida", + "request": "Petición", + "research projects": "proyectos de investigación", + "resource": "Recurso", + "reversible": "reversible", + "sent an announcement": "envió un anuncio", + "start_date (optional): e.g. 20200730-12:23:12.000+00 (other date formats may work)": "start_date (opcional): p. \n20200730-12:23:12.000 00 (pueden funcionar otros formatos de fecha)", + "subscribers": "suscriptores", + "this application": "esta aplicación", + "this role/badge?": "este rol/insignia?", + "title: text": "título : texto", + "to join it": "unirse a él", + "to set it up.": "para configurarlo.", + "topics: up to 3 topic names separated by spaces and/or commas e.g. “food organic”": "temas: hasta 3 nombres de temas separados por espacios y/o comas, p. \n“comida organica”", + "type here": "escriba aquí", + "type: one of discussion, request, offer, resource, event, project": "tipo: uno de discusión, solicitud, oferta, recurso, evento, proyecto", + "upcoming": "próximo", + "useLayoutFlags must be used within a LayoutFlagsProvider": "useLayoutFlags debe usarse dentro de un LayoutFlagsProvider", + "visit": "visita", + "volunteer opportunities": "oportunidades para voluntarios", + "with this link": "con este enlace", + "wrote:": "escribió:", + "wrote: ": "escribió:", + "you're subscribed to #{{topicName}}": "estás suscrito", + "{{group.name}} is only accessible to members of the following groups:": "{{group.name}} solo es accesible para los miembros de los siguientes grupos:", + "{{action}} was canceled or no user data was found.": "{{action}} se canceló o no se encontraron datos de usuario.", + "{{appName}} is asking to have offline access to Hylo": "{{appName}} solicita acceso sin conexión a Hylo", + "{{appName}} is asking you to confirm previously given authorization": "{{appName}} te pide que confirmes la autorización otorgada previamente", + "{{appName}} wants access to your Hylo account": "{{appName}} quiere acceder a tu cuenta de Hylo", + "{{childGroups.length}} groups are a part of {{group.name}}": "{{childGroups.length}} grupos son parte de {{group.name}}", + "{{count}} Groups": "{{count}} Grupos", + "{{count}} Groups_plural": "{{count}} Grupos", + "{{currentGroup.name}} topics": "{{currentGroup.name}} temas", + "{{group.name}} has some volunteering opportunities": "{{group.name}} tiene algunas oportunidades de voluntariado", + "{{group.name}} is a part of 1 Group": "{{group.name}} es parte de 1 grupo", + "{{group.name}} is a part of {{parentGroups.length}} Groups": "{{group.name}} es parte de {{parentGroups.length}} Grupos", + "{{group.name}} is available to participate in research": "{{group.name}} está disponible para participar en la investigación", + "{{group.name}} is curious about better understanding their farm through data (soil, environmental impacts, etc.)": "{{group.name}} tiene curiosidad por comprender mejor su granja a través de datos (suelo, impactos ambientales, etc.)", + "{{group.name}} is curious about further certification (organic, ISO, regenerative, etc.)": "{{group.name}} tiene curiosidad por obtener más certificaciones (orgánica, ISO, regenerativa, etc.)", + "{{group.name}} is curious about understanding the nutrient density of their product": "{{group.name}} tiene curiosidad por comprender la densidad de nutrientes de su producto", + "{{group.name}} is interested in carbon markets": "{{group.name}} está interesado en los mercados de carbono", + "{{group.name}} is interested in equipment sharing": "{{group.name}} está interesado en compartir equipmientos", + "{{group.name}} is interested in forming a cooperative": "{{group.name}} está interesado en formar una cooperativa", + "{{group.name}} is interested in increasing their sales": "{{group.name}} está interesado en aumentar sus ventas", + "{{group.name}} is interested in low-cost loans": "{{group.name}} está interesado en préstamos de bajo costo", + "{{group.name}} is not a member of any groups yet": "{{group.name}} aún no es miembro de ningún grupo", + "{{group.name}} is open to co-hosting events": "{{group.name}} está abierto a coorganizar eventos", + "{{group.name}} is seeking ecosystem services": "{{group.name}} busca servicios ecosistémicos", + "{{group.name}} is seeking farm support": "{{group.name}} está buscando apoyo granja", + "{{group.name}} is seeking new markets": "{{group.name}} está buscando nuevos mercados", + "{{group.name}} seeks better communication / marketing to their buyers": "{{group.name}} busca una mejor comunicación/marketing para sus compradores", + "{{group.name}} wants help focusing on biodiversity (protecting species, improving ecology, markets)": "{{group.name}} quiere ayuda para centrarse en la biodiversidad (proteger especies, mejorar la ecología, mercados)", + "{{group.name}} wants to comparing my farm to others": "{{group.name}} quiere comparar mi granja con otras", + "{{group.name}} wants to easily provide data to supply chain partners": "{{group.name}} quiere proporcionar datos fácilmente a los socios de la cadena de suministro", + "{{group.name}} wants to ensure animal welfare": "{{group.name}} quiere garantizar el bienestar animal", + "{{group.name}} wants to improve product quality": "{{group.name}} quiere mejorar la calidad del producto", + "{{group.name}} wants to increase their farm valuation": "{{group.name}} quiere aumentar la valoración de su granja", + "{{group.name}} wants to learn about new methods or practices": "{{group.name}} quiere aprender sobre nuevos métodos o prácticas", + "{{group.name}} wants to reduce hazard risk (fire, flood, drought, etc.)": "{{group.name}} quiere reducir el riesgo de peligros (incendios, inundaciones, sequías, etc.)", + "{{groupName}} Topics": "{{groupName}} Temas", + "{{memberCount}} Total Members": "{{memberCount}} Total de miembros", + "{{name}}s comments": "Comentarios de {{name}}", + "{{name}}s posts": "Publicaciones de {{name}}", + "{{name}}s reactions": "Reacciones de {{name}}", + "{{name}}s recent activity": "Actividad reciente de {{name}}", + "{{numBad}} invalid email address/es found (see above)).": "Se encontraron {{numBad}} direcciones de correo electrónico no válidas (ver arriba)).", + "{{othersCount}} others": "{{othersCount}} otros", + "{{parentGroup.name}} requires groups to answer the following questions before joining": "{{parentGroup.name}} requiere que los grupos respondan las siguientes preguntas antes de unirse", + "{{personOne}} and {{personTwo}}": "{{personOne}} y {{personTwo}}", + "{{totalTopicsCached}} Total Topics": "{{totalTopicsCached}} Total de temas" +} diff --git a/src/components/Affiliation/Affiliation.js b/src/components/Affiliation/Affiliation.js index 99c3b9323..43e46416b 100644 --- a/src/components/Affiliation/Affiliation.js +++ b/src/components/Affiliation/Affiliation.js @@ -1,11 +1,13 @@ import React from 'react' +import { useTranslation } from 'react-i18next' import './Affiliation.scss' export default function Affiliation ({ affiliation, index, archive }) { const { role, preposition, orgName, url } = affiliation + const { t } = useTranslation() const leave = () => { - if (window.confirm(`Are you sure you want to delete your affiliation as ${role} ${preposition} ${orgName}?`)) { + if (window.confirm(t('Are you sure you want to delete your affiliation as {{affiliation.role}} {{affiliation.preposition}} {{affiliation.orgName}}?', { affiliation }))) { archive(affiliation.id) } } @@ -14,9 +16,9 @@ export default function Affiliation ({ affiliation, index, archive }) {
{role}
{preposition}
-
{url ? ({orgName}) : orgName }
+
{url ? ({orgName}) : orgName}
- { archive && Delete } + {archive && {t('Delete')}}
) } diff --git a/src/components/Affiliation/Affiliation.test.js b/src/components/Affiliation/Affiliation.test.js index 500b1adc8..270af3e53 100644 --- a/src/components/Affiliation/Affiliation.test.js +++ b/src/components/Affiliation/Affiliation.test.js @@ -2,6 +2,18 @@ import React from 'react' import { shallow } from 'enzyme' import Affiliation from './Affiliation' +jest.mock('react-i18next', () => ({ + ...jest.requireActual('react-i18next'), + useTranslation: (domain) => { + return { + t: (str) => str, + i18n: { + changeLanguage: () => new Promise(() => {}) + } + } + } +})) + describe('Affiliation', () => { it('matches last snapshot', () => { const props = { diff --git a/src/components/AttachmentManager/AttachmentManager.js b/src/components/AttachmentManager/AttachmentManager.js index 9db7e0b47..b713e8e45 100644 --- a/src/components/AttachmentManager/AttachmentManager.js +++ b/src/components/AttachmentManager/AttachmentManager.js @@ -2,6 +2,7 @@ import { isEmpty, filter } from 'lodash/fp' import path from 'path' import PropTypes from 'prop-types' import React, { useRef } from 'react' +import { useTranslation } from 'react-i18next' import { useDrag, useDrop } from 'react-dnd' import Icon from 'components/Icon' import Loading from 'components/Loading' @@ -86,13 +87,14 @@ export default class AttachmentManager extends React.Component { } export function ImageManager (props) { + const { t } = useTranslation() const { type, id, attachments, addAttachment, removeAttachment, uploadAttachmentPending, showLoading, showAddButton, showLabel } = props return
- {showLabel &&
Images
} + {showLabel &&
{t('Images')}
}
{attachments.map((attachment, i) => - {showLabel &&
Files
} + {showLabel &&
{t('Files')}
}
{attachments.map((attachment, i) => removeAttachment(type, id, attachment)} key={i} />)} - {showLoading && uploadAttachmentPending &&
Loading...
} + {showLoading && uploadAttachmentPending &&
{t('Loading...')}
} {showAddButton && addAttachment(type, id, attachment)} styleName='add-file-row'>
- + Add File
+ + {t('Add File')}
}
diff --git a/src/components/AttachmentManager/AttachmentManager.test.js b/src/components/AttachmentManager/AttachmentManager.test.js index 048a0f3c5..65caf0ba4 100644 --- a/src/components/AttachmentManager/AttachmentManager.test.js +++ b/src/components/AttachmentManager/AttachmentManager.test.js @@ -4,6 +4,18 @@ import { shallow } from 'enzyme' import { render } from 'util/testing/reactTestingLibraryExtended' import AttachmentManager, { ImageManager, ImagePreview, FileManager, FilePreview } from './AttachmentManager' +jest.mock('react-i18next', () => ({ + ...jest.requireActual('react-i18next'), + useTranslation: (domain) => { + return { + t: (str) => str, + i18n: { + changeLanguage: () => new Promise(() => {}) + } + } + } +})) + const minDefaultProps = { type: 'anything', loadAttachments: () => {}, diff --git a/src/components/AttachmentManager/__snapshots__/AttachmentManager.test.js.snap b/src/components/AttachmentManager/__snapshots__/AttachmentManager.test.js.snap index 8c389ba6a..3eafb9543 100644 --- a/src/components/AttachmentManager/__snapshots__/AttachmentManager.test.js.snap +++ b/src/components/AttachmentManager/__snapshots__/AttachmentManager.test.js.snap @@ -414,7 +414,8 @@ exports[`FileManager matches last snapshot 1`] = ` > + - Add File + + Add File diff --git a/src/components/BadgeEmoji/BadgeEmoji.js b/src/components/BadgeEmoji/BadgeEmoji.js index 736fd3260..29f4d6dd5 100644 --- a/src/components/BadgeEmoji/BadgeEmoji.js +++ b/src/components/BadgeEmoji/BadgeEmoji.js @@ -5,20 +5,22 @@ import './badgeEmoji.scss' export default function Badge ({ emoji, expanded, className, border, onClick, isModerator, name, id }) { if (!emoji) return null - return (<> - - {emoji} - - - ) + return ( + <> + + {emoji} + + + + ) } diff --git a/src/components/CommentCard/CommentCard.js b/src/components/CommentCard/CommentCard.js index 56ac8b18b..41c280d4a 100644 --- a/src/components/CommentCard/CommentCard.js +++ b/src/components/CommentCard/CommentCard.js @@ -1,4 +1,5 @@ import React from 'react' +import { useTranslation } from 'react-i18next' import cx from 'classnames' import RoundImage from 'components/RoundImage' import { TextHelpers } from 'hylo-shared' @@ -19,6 +20,7 @@ export default function CommentCard ({ const postTitle = post.title ? TextHelpers.truncateText(post.title, 25) : TextHelpers.truncateHTML(post.details, 25) const commentText = expanded ? comment.text : TextHelpers.truncateHTML(comment.text, 144) + const { t } = useTranslation() return ( showDetails(comment.post.id)} styleName='link'> @@ -27,7 +29,7 @@ export default function CommentCard ({
- {creator.name} commented on  + {creator.name} {t('commented on')}{' '} {postTitle}
diff --git a/src/components/CommentCard/CommentCard.test.js b/src/components/CommentCard/CommentCard.test.js index d17b89d31..56ce5d3db 100644 --- a/src/components/CommentCard/CommentCard.test.js +++ b/src/components/CommentCard/CommentCard.test.js @@ -2,6 +2,18 @@ import CommentCard from './CommentCard' import { shallow } from 'enzyme' import React from 'react' +jest.mock('react-i18next', () => ({ + ...jest.requireActual('react-i18next'), + useTranslation: (domain) => { + return { + t: (str) => str, + i18n: { + changeLanguage: () => new Promise(() => {}) + } + } + } +})) + const props = { comment: { text: '

text of the comment. a long one. text of the comment. a long one. text of the comment. a long one. text of the comment. a long one. text of the comment. a long one. text of the comment. a long one. Irrelevant Change

', diff --git a/src/components/CommentCard/__snapshots__/CommentCard.test.js.snap b/src/components/CommentCard/__snapshots__/CommentCard.test.js.snap index 563cf404d..d1e819493 100644 --- a/src/components/CommentCard/__snapshots__/CommentCard.test.js.snap +++ b/src/components/CommentCard/__snapshots__/CommentCard.test.js.snap @@ -28,7 +28,9 @@ exports[`displays image an image attachments 1`] = ` > Joe Smith
- commented on  + + commented on + @@ -112,7 +114,9 @@ exports[`matches last snapshot 1`] = ` > Joe Smith - commented on  + + commented on + @@ -182,7 +186,9 @@ exports[`matches last snapshot with different config 1`] = ` > Joe Smith - commented on  + + commented on + diff --git a/src/components/CreateGroup/CreateGroup.js b/src/components/CreateGroup/CreateGroup.js index 01e44a617..008ea8b97 100644 --- a/src/components/CreateGroup/CreateGroup.js +++ b/src/components/CreateGroup/CreateGroup.js @@ -1,5 +1,6 @@ import { trim } from 'lodash/fp' import React, { Component } from 'react' +import { withTranslation } from 'react-i18next' import Button from 'components/Button' import Dropdown from 'components/Dropdown' import GroupsSelector from 'components/GroupsSelector' @@ -19,7 +20,7 @@ import styles from './CreateGroup.scss' const slugValidatorRegex = /^[0-9a-z-]{2,40}$/ -export default class CreateGroup extends Component { +class CreateGroup extends Component { constructor (props) { super(props) @@ -47,7 +48,7 @@ export default class CreateGroup extends Component { componentDidUpdate (oldProps) { if (oldProps.groupSlugExists !== this.props.groupSlugExists) { - this.setState({ errors: { ...this.state.errors, slug: this.props.groupSlugExists ? 'This URL already exists. Try another.' : false } }) + this.setState({ errors: { ...this.state.errors, slug: this.props.groupSlugExists ? this.props.t('This URL already exists. Try another.') : false } }) } } @@ -62,9 +63,9 @@ export default class CreateGroup extends Component { validateSlug (val) { if (val === '') { - return 'Please enter a URL slug' + return this.props.t('Please enter a URL slug') } else if (!slugValidatorRegex.test(val)) { - return 'URLs must have between 2 and 40 characters, and can only have lower case letters, numbers, and dashes.' + return this.props.t('URLs must have between 2 and 40 characters, and can only have lower case letters, numbers, and dashes.') } else { this.props.fetchGroupExists(val) return false @@ -81,7 +82,7 @@ export default class CreateGroup extends Component { } if (field === 'name') { - updates.errors.name = newValue === '' ? 'Please enter a group name' : false + updates.errors.name = newValue === '' ? this.props.t('Please enter a group name') : false updates.characterCount = newValue.length } @@ -111,7 +112,7 @@ export default class CreateGroup extends Component { if (error) { // `state.error` doesn't appear to be displayed anywhere this.setState({ - error: 'There was an error, please try again.' + error: this.props.t('There was an error, please try again.') }) } else { this.props.goToGroup(slug) @@ -121,7 +122,7 @@ export default class CreateGroup extends Component { } render () { - const { goBack, match, parentGroupOptions } = this.props + const { goBack, match, parentGroupOptions, t } = this.props const { accessibility, characterCount, edited, errors, name, parentGroups, slug, visibility } = this.state if (!match) return null @@ -130,7 +131,7 @@ export default class CreateGroup extends Component {
- Create Group + {t('Create Group')}
-
WHO CAN SEE THIS GROUP?
+
{t('WHO CAN SEE THIS GROUP?')}
- {visibilityString(visibility)} - {visibilityDescription(visibility)} + {t(visibilityString(visibility))} + {t(visibilityDescription(visibility))}
@@ -194,8 +195,8 @@ export default class CreateGroup extends Component {
- {label} - {visibilityDescription(GROUP_VISIBILITY[label])} + {t(label)} + {t(visibilityDescription(GROUP_VISIBILITY[label]))}
), @@ -211,10 +212,10 @@ export default class CreateGroup extends Component {
-
WHO CAN JOIN THIS GROUP?
+
{t('WHO CAN JOIN THIS GROUP?')}
- {accessibilityString(accessibility)} - {accessibilityDescription(accessibility)} + {t(accessibilityString(accessibility))} + {t(accessibilityDescription(accessibility))}
@@ -227,8 +228,8 @@ export default class CreateGroup extends Component {
- {label} - {accessibilityDescription(GROUP_ACCESSIBILITY[label])} + {t(label)} + {t(accessibilityDescription(GROUP_ACCESSIBILITY[label]))}
), @@ -244,7 +245,7 @@ export default class CreateGroup extends Component {
*/} @@ -252,10 +253,10 @@ export default class CreateGroup extends Component { {parentGroupOptions && parentGroupOptions.length > 0 && (
- IS THIS GROUP A MEMBER OF OTHER GROUPS? + {t('IS THIS GROUP A MEMBER OF OTHER GROUPS?')}
? -
You may add parent groups if you are a moderator of the group you wish to add, or if the group you wish to add has the Open access setting which allows any group to join it
+
{t('You may add parent groups if you are a moderator of the group you wish to add, or if the group you wish to add has the Open access setting which allows any group to join it')}
{/* TODO: somehow show groups that are restricted and will be a join request differently */} - Create Group + {t('Create Group')}
) } } + +export default withTranslation()(CreateGroup) diff --git a/src/components/CreateGroup/CreateGroup.test.js b/src/components/CreateGroup/CreateGroup.test.js index 82360ff30..e2e8c1bcd 100644 --- a/src/components/CreateGroup/CreateGroup.test.js +++ b/src/components/CreateGroup/CreateGroup.test.js @@ -2,6 +2,14 @@ import CreateGroup from './CreateGroup' import { shallow } from 'enzyme' import React from 'react' +jest.mock('react-i18next', () => ({ + // this mock makes sure any components using the translate HoC receive the t function as a prop + withTranslation: () => Component => { + Component.defaultProps = { ...Component.defaultProps, t: (str) => str } + return Component + } +})) + describe('CreateGroup', () => { it('matches snapshot', () => { const wrapper = shallow() diff --git a/src/components/CreateModal/CreateModal.js b/src/components/CreateModal/CreateModal.js index 048a0bba7..0cac67f45 100644 --- a/src/components/CreateModal/CreateModal.js +++ b/src/components/CreateModal/CreateModal.js @@ -1,6 +1,7 @@ import React, { useState } from 'react' import { useSelector } from 'react-redux' import { Route, Switch, useHistory, useLocation } from 'react-router-dom' +import { useTranslation } from 'react-i18next' import { CSSTransition } from 'react-transition-group' import getPreviousLocation from 'store/selectors/getPreviousLocation' import CreateModalChooser from './CreateModalChooser' @@ -9,12 +10,13 @@ import Icon from 'components/Icon' import PostEditor from 'components/PostEditor' import './CreateModal.scss' -export default function CreateModal (props) { +const CreateModal = (props) => { const location = useLocation() const history = useHistory() const previousLocation = useSelector(getPreviousLocation) const [returnToLocation] = useState(previousLocation) const [isDirty, setIsDirty] = useState() + const { t } = useTranslation() const querystringParams = new URLSearchParams(location.search) const mapLocation = (querystringParams.has('lat') && querystringParams.has('lng')) @@ -29,7 +31,7 @@ export default function CreateModal (props) { } const confirmClose = () => { - const confirmed = !isDirty || window.confirm('Changes won\'t be saved. Are you sure you want to cancel?') + const confirmed = !isDirty || window.confirm(t('Changes won\'t be saved. Are you sure you want to cancel?')) if (confirmed) { closeModal() @@ -71,3 +73,5 @@ export default function CreateModal (props) { ) } + +export default CreateModal diff --git a/src/components/CreateModal/CreateModal.test.js b/src/components/CreateModal/CreateModal.test.js index 009663508..9f9a98e60 100644 --- a/src/components/CreateModal/CreateModal.test.js +++ b/src/components/CreateModal/CreateModal.test.js @@ -3,6 +3,18 @@ import orm from 'store/models' import { AllTheProviders, render, screen } from 'util/testing/reactTestingLibraryExtended' import CreateModal from './CreateModal' +jest.mock('react-i18next', () => ({ + ...jest.requireActual('react-i18next'), + useTranslation: (domain) => { + return { + t: (str) => str, + i18n: { + changeLanguage: () => new Promise(() => {}) + } + } + } +})) + function testProviders () { const ormSession = orm.mutableSession(orm.getEmptyState()) ormSession.Me.create({ id: '1' }) diff --git a/src/components/CreateModal/CreateModalChooser.js b/src/components/CreateModal/CreateModalChooser.js index 4aa72892d..e2ede87f4 100644 --- a/src/components/CreateModal/CreateModalChooser.js +++ b/src/components/CreateModal/CreateModalChooser.js @@ -4,16 +4,33 @@ import isWebView from 'util/webView' import { POST_TYPES } from 'store/models/Post' import Icon from 'components/Icon' import './CreateModal.scss' +import { useTranslation } from 'react-i18next' const postTypes = Object.keys(POST_TYPES).filter(t => t !== 'chat') export default function CreateModalChooser ({ location }) { const querystringParams = new URLSearchParams(location.search) const hasLocation = querystringParams.has('lat') && querystringParams.has('lng') + const { t } = useTranslation() + // These need to be invoked here so that they get picked up by the translation extractor + t('What help can you offer?') + t('What are you looking for help with?') + t('What resource is available?') + t('What would you like to call your project?') + t('What is your event called?') + t('Whats on your mind?') + t('Add a description') + t('request') + t('discusion') + t('offer') + t('resource') + t('project') + t('event') + t('Talk about whats important with others') return (
-

{hasLocation && 'New Post at this location: '}What would you like to create?

+

{hasLocation && t('New Post at this location:') + ' '}{t('What would you like to create?')}

{postTypes.map(postType => { querystringParams.set('newPostType', postType) @@ -26,8 +43,8 @@ export default function CreateModalChooser ({ location }) {
- {postType} - {POST_TYPES[postType].description} + {t(postType)} + {t(POST_TYPES[postType].description)}
@@ -40,8 +57,8 @@ export default function CreateModalChooser ({ location }) {
- Group - Create a new movement, network, community or group! + {t('Group')} + {t('Create a new movement, network, community or group!')}
diff --git a/src/components/CreateTopic/CreateTopic.js b/src/components/CreateTopic/CreateTopic.js index 6f695fc03..376140c9c 100644 --- a/src/components/CreateTopic/CreateTopic.js +++ b/src/components/CreateTopic/CreateTopic.js @@ -1,4 +1,5 @@ import React, { Component } from 'react' +import { withTranslation } from 'react-i18next' import { Redirect } from 'react-router' import { any, arrayOf, func, object, string, bool } from 'prop-types' import { debounce, has, get, isEmpty, trim } from 'lodash/fp' @@ -11,7 +12,7 @@ import TextInput from 'components/TextInput' import './CreateTopic.scss' -export default class CreateTopic extends Component { +class CreateTopic extends Component { static propTypes = { buttonText: string, groupId: any, @@ -25,13 +26,13 @@ export default class CreateTopic extends Component { defaultState = { closeOnSubmit: false, - modalTitle: 'Create a Topic', + modalTitle: this.props.t('Create a Topic'), modalVisible: false, nameError: null, loading: false, showCancelButton: true, showSubmitButton: true, - submitButtonText: 'Add Topic', + submitButtonText: this.props.t('Add Topic'), topicName: '', useNotificationFormat: false } @@ -74,9 +75,9 @@ export default class CreateTopic extends Component { this.setState({ closeOnSubmit: true, loading: false, - modalTitle: 'Topic Created', + modalTitle: this.props.t('Topic Created'), showCancelButton: false, - submitButtonText: 'Ok', + submitButtonText: this.props.t('Ok'), useNotificationFormat: true }) } @@ -94,7 +95,7 @@ export default class CreateTopic extends Component { const name = this.safeTopicName() if (isEmpty(name)) { - return this.setState({ nameError: 'Topic name is required.' }) + return this.setState({ nameError: this.props.t('Topic name is required.') }) } // First, check if we already have the topic, and it has posts. If so, we @@ -157,7 +158,7 @@ export default class CreateTopic extends Component { topicName, useNotificationFormat } = this.state - const { subscribeAfterCreate } = this.props + const { subscribeAfterCreate, t } = this.props if (redirectTopic && subscribeAfterCreate) { const url = topicUrl(encodeURI(redirectTopic), { groupSlug: this.props.groupSlug }) @@ -179,7 +180,7 @@ export default class CreateTopic extends Component { submitButtonText={submitButtonText} useNotificationFormat={useNotificationFormat}> {useNotificationFormat - ? (subscribeAfterCreate ?
you're subscribed to #{this.ignoreHash(topicName)}
:
Created topic #{this.ignoreHash(topicName)}
) + ? (subscribeAfterCreate ?
{t('you\'re subscribed to #{{topicName}}', { topicName: this.ignoreHash(topicName) })}
:
{t('Created topic #{{topicName}}', { topicName: this.ignoreHash(topicName) })}
) :
{nameError &&
{nameError}
}
} @@ -196,3 +197,4 @@ export default class CreateTopic extends Component { } } +export default withTranslation()(CreateTopic) diff --git a/src/components/CreateTopic/CreateTopic.test.js b/src/components/CreateTopic/CreateTopic.test.js index f6f3a8b26..68bdbf6b1 100644 --- a/src/components/CreateTopic/CreateTopic.test.js +++ b/src/components/CreateTopic/CreateTopic.test.js @@ -2,6 +2,14 @@ import React from 'react' import { shallow, mount } from 'enzyme' import CreateTopic from './CreateTopic' +jest.mock('react-i18next', () => ({ + ...jest.requireActual('react-i18next'), + withTranslation: () => Component => { + Component.defaultProps = { ...Component.defaultProps, t: (str) => str } + return Component + } +})) + describe('CreateTopic', () => { let instance, props, wrapper diff --git a/src/components/CreateTopic/__snapshots__/CreateTopic.test.js.snap b/src/components/CreateTopic/__snapshots__/CreateTopic.test.js.snap index 0140f1ad5..0d3d60af9 100644 --- a/src/components/CreateTopic/__snapshots__/CreateTopic.test.js.snap +++ b/src/components/CreateTopic/__snapshots__/CreateTopic.test.js.snap @@ -80,7 +80,7 @@ exports[`CreateTopic snapshots (various states) matches the last snapshot for no
- you're subscribed to # + you're subscribed to #{{topicName}}
diff --git a/src/components/ErrorBoundary/ErrorBoundary.js b/src/components/ErrorBoundary/ErrorBoundary.js index 2e93c1948..83f2ebc6b 100644 --- a/src/components/ErrorBoundary/ErrorBoundary.js +++ b/src/components/ErrorBoundary/ErrorBoundary.js @@ -1,8 +1,9 @@ import React from 'react' +import { withTranslation } from 'react-i18next' import './ErrorBoundary.scss' import rollbar from 'client/rollbar' -export default class ErrorBoundary extends React.Component { +class ErrorBoundary extends React.Component { constructor (props) { super(props) this.state = { hasError: false } @@ -17,7 +18,7 @@ export default class ErrorBoundary extends React.Component { } render () { - const message = this.props.message || 'Oops! Something went wrong. Try reloading the page.' + const message = this.props.message || this.props.t('Oops! Something went wrong. Try reloading the page.') if (this.state.hasError) { // You can render any custom fallback UI return
@@ -31,3 +32,4 @@ export default class ErrorBoundary extends React.Component { return this.props.children } } +export default withTranslation()(ErrorBoundary) diff --git a/src/components/ErrorBoundary/ErrorBoundary.test.js b/src/components/ErrorBoundary/ErrorBoundary.test.js index 7c286f07c..e612a0417 100644 --- a/src/components/ErrorBoundary/ErrorBoundary.test.js +++ b/src/components/ErrorBoundary/ErrorBoundary.test.js @@ -2,6 +2,14 @@ import ErrorBoundary from './ErrorBoundary' import { mount } from 'enzyme' import React from 'react' +jest.mock('react-i18next', () => ({ + ...jest.requireActual('react-i18next'), + withTranslation: () => Component => { + Component.defaultProps = { ...Component.defaultProps, t: (str) => str } + return Component + } +})) + const Something = () => null it('renders children correctly', () => { diff --git a/src/components/ErrorBoundary/__snapshots__/ErrorBoundary.test.js.snap b/src/components/ErrorBoundary/__snapshots__/ErrorBoundary.test.js.snap index e3f61f952..fa52c226c 100644 --- a/src/components/ErrorBoundary/__snapshots__/ErrorBoundary.test.js.snap +++ b/src/components/ErrorBoundary/__snapshots__/ErrorBoundary.test.js.snap @@ -3,6 +3,7 @@ exports[`renders an error when an error is thrown 1`] = `
diff --git a/src/components/EventInviteDialog/EventInviteDialog.js b/src/components/EventInviteDialog/EventInviteDialog.js index 230641895..159d082e8 100644 --- a/src/components/EventInviteDialog/EventInviteDialog.js +++ b/src/components/EventInviteDialog/EventInviteDialog.js @@ -1,4 +1,5 @@ import React, { useState, useEffect } from 'react' +import { useTranslation } from 'react-i18next' import cx from 'classnames' import { bgImageStyle } from 'util/index' import ModalDialog from 'components/ModalDialog' @@ -11,7 +12,7 @@ import useInView from 'react-cool-inview' import Loading from 'components/Loading' const pageSize = 30 -export default function EventInviteDialog ({ +const EventInviteDialog = ({ fetchPeople, forGroups, eventInvitations, @@ -20,7 +21,7 @@ export default function EventInviteDialog ({ onClose, invitePeopleToEvent, pending -}) { +}) => { const [invitedIds, setInvitedIds] = useState([]) const [searchTerm, setSearchTerm] = useState('') const [pageFetched, setPageFetched] = useState(0) @@ -30,6 +31,7 @@ export default function EventInviteDialog ({ : setInvitedIds(invitedIds.concat([id])) const onSearchChange = ({ target: { value } }) => setSearchTerm(value) + const { t } = useTranslation() useEffect(() => { const fetch = () => { @@ -66,10 +68,10 @@ export default function EventInviteDialog ({ const filteredInviteSuggestions = getFilteredInviteSuggestions() const inviteButtonLabel = invitedIds.length === 0 - ? 'Select people to invite' + ? t('Select people to invite') : invitedIds.length === 1 - ? 'Invite 1 person' - : `Invite ${invitedIds.length} people` + ? t('Invite 1 person') + : t('Invite {{invitedIds.length}} people', { invitedIds }) return
-
Already Invited
+
{t('Already Invited')}
{eventInvitations.map(eventInvitation => { }) export function Search ({ onChange }) { + const { t } = useTranslation() return
x && x.focus()} - placeholder='Search members' + placeholder={t('Search members')} onChange={onChange} />
} + +export default EventInviteDialog diff --git a/src/components/EventInviteDialog/EventInviteDialog.test.js b/src/components/EventInviteDialog/EventInviteDialog.test.js index 91b54058e..574d9897c 100644 --- a/src/components/EventInviteDialog/EventInviteDialog.test.js +++ b/src/components/EventInviteDialog/EventInviteDialog.test.js @@ -2,6 +2,18 @@ import EventInviteDialog, { InviteeRow, Search } from './EventInviteDialog' import { shallow } from 'enzyme' import React from 'react' +jest.mock('react-i18next', () => ({ + ...jest.requireActual('react-i18next'), + useTranslation: (domain) => { + return { + t: (str) => str, + i18n: { + changeLanguage: () => new Promise(() => {}) + } + } + } +})) + describe('EventInviteDialog', () => { it('renders correctly', () => { const props = { diff --git a/src/components/FacebookButton/FacebookButton.js b/src/components/FacebookButton/FacebookButton.js index 794701a45..134cfd3c0 100644 --- a/src/components/FacebookButton/FacebookButton.js +++ b/src/components/FacebookButton/FacebookButton.js @@ -1,4 +1,5 @@ import React from 'react' +import { useTranslation } from 'react-i18next' import cx from 'classnames' import Icon from 'components/Icon' import './FacebookButton.scss' @@ -8,7 +9,8 @@ export default function FacebookButton ({ signUp, className = '' }) { - const label = 'Continue with Facebook' + const { t } = useTranslation() + const label = t('Continue with Facebook') return
-

Explanation for Flagging

+

{t('Explanation for Flagging')}

@@ -100,9 +104,9 @@ export default class FlagContent extends PureComponent { -
Project Management
+
{t('Project Management')}
) } + +export default withTranslation()(PostEditor) diff --git a/src/components/PostEditor/PostEditor.test.js b/src/components/PostEditor/PostEditor.test.js index 0e26ec963..e96599ee2 100644 --- a/src/components/PostEditor/PostEditor.test.js +++ b/src/components/PostEditor/PostEditor.test.js @@ -8,6 +8,14 @@ jest.mock('lodash/debounce', () => fn => { return fn }) +jest.mock('react-i18next', () => ({ + ...jest.requireActual('react-i18next'), + withTranslation: () => Component => { + Component.defaultProps = { ...Component.defaultProps, t: (str) => str } + return Component + } +})) + describe('PostEditor', () => { const baseProps = {} @@ -29,22 +37,11 @@ describe('PostEditor', () => { const props = { ...baseProps, initialPrompt: 'a test prompt', - titlePlaceholderForPostType: { - default: 'a test title placeholder' - }, - detailsPlaceholder: 'details placeholder' } const wrapper = shallow() expect(wrapper).toMatchSnapshot() }) - const titlePlaceholderForPostType = { - discussion: 'discussion placeholder', - request: 'request placeholder', - offer: 'offer placeholder', - resource: 'resource placeholder', - default: 'default placeholder' - } const renderForType = (type) => { const props = { ...baseProps, @@ -53,8 +50,7 @@ describe('PostEditor', () => { startTime: new Date(1551908483315), endTime: new Date(1551908483315), groups: [] - }, - titlePlaceholderForPostType + } } return shallow() } diff --git a/src/components/PostEditor/__snapshots__/PostEditor.test.js.snap b/src/components/PostEditor/__snapshots__/PostEditor.test.js.snap index 8cfd5a855..1adcccb0e 100644 --- a/src/components/PostEditor/__snapshots__/PostEditor.test.js.snap +++ b/src/components/PostEditor/__snapshots__/PostEditor.test.js.snap @@ -233,7 +233,6 @@ exports[`PostEditor editing a post form in editing mode 1`] = `
@@ -472,7 +473,6 @@ exports[`PostEditor editing a post post is loaded into fields 1`] = `
@@ -662,7 +664,7 @@ exports[`PostEditor for a new post correct title placeholder and type button sel disabled={false} maxLength={50} onChange={[Function]} - placeholder="discussion placeholder" + placeholder="What’s on your mind?" type="text" value="" /> @@ -725,14 +727,15 @@ exports[`PostEditor for a new post correct title placeholder and type button sel
@@ -819,7 +822,7 @@ exports[`PostEditor for a new post correct title placeholder and type button sel disabled={false} maxLength={50} onChange={[Function]} - placeholder="offer placeholder" + placeholder="What help can you offer?" type="text" value="" /> @@ -882,14 +885,15 @@ exports[`PostEditor for a new post correct title placeholder and type button sel
@@ -1004,7 +1008,7 @@ exports[`PostEditor for a new post correct title placeholder and type button sel disabled={false} maxLength={50} onChange={[Function]} - placeholder="request placeholder" + placeholder="What are you looking for help with?" type="text" value="" /> @@ -1067,14 +1071,15 @@ exports[`PostEditor for a new post correct title placeholder and type button sel
@@ -1189,7 +1194,7 @@ exports[`PostEditor for a new post correct title placeholder and type button sel disabled={false} maxLength={50} onChange={[Function]} - placeholder="resource placeholder" + placeholder="What resource is available?" type="text" value="" /> @@ -1252,14 +1257,15 @@ exports[`PostEditor for a new post correct title placeholder and type button sel
@@ -1374,7 +1380,7 @@ exports[`PostEditor for a new post initial prompt and placeholders 1`] = ` disabled={false} maxLength={50} onChange={[Function]} - placeholder="a test title placeholder" + placeholder="What’s on your mind?" type="text" value="" /> @@ -1438,14 +1444,15 @@ exports[`PostEditor for a new post initial prompt and placeholders 1`] = `
@@ -1637,14 +1644,15 @@ exports[`PostEditor post is defaulted while loading editor 1`] = `
@@ -1795,14 +1803,15 @@ exports[`PostEditor post is defaulted while loading editor for NEW post 1`] = `
@@ -1953,14 +1962,15 @@ exports[`PostEditor renders announcement option with admin in props 1`] = `
@@ -2130,14 +2140,15 @@ exports[`PostEditor renders contribution button 1`] = `
@@ -2172,7 +2183,9 @@ exports[`PostEditor renders contribution button 1`] = `
- If you turn 'Accept Contributions' on, people will be able to send money to your Stripe connected account to support this project. + If you turn 'Accept Contributions' on, people will be able + to send money to your Stripe connected account to support + this project.
@@ -2352,14 +2365,15 @@ exports[`PostEditor renders with min props 1`] = `
diff --git a/src/components/PostGridItem/PostGridItem.js b/src/components/PostGridItem/PostGridItem.js index da3f8dba2..2f4f3ebfc 100644 --- a/src/components/PostGridItem/PostGridItem.js +++ b/src/components/PostGridItem/PostGridItem.js @@ -1,8 +1,7 @@ import React from 'react' import cx from 'classnames' import Tooltip from 'components/Tooltip' - -// import Moment from 'moment-timezone' +import { useTranslation } from 'react-i18next' import { personUrl } from 'util/navigation' import { TextHelpers } from 'hylo-shared' import Avatar from 'components/Avatar' @@ -33,7 +32,7 @@ export default function PostGridItem (props) { if (!creator) { // PostCard guards against this, so it must be important? ;P return null } - + const { t } = useTranslation() const creatorUrl = personUrl(creator.id, routeParams.slug) const unread = false // will reintegrate once I have attachment vars @@ -45,10 +44,9 @@ export default function PostGridItem (props) { {childPost &&
- {/* TODO: i18n on tooltip */} { const numOtherCommentors = commentersTotal - 1 const unread = false const startTimeMoment = Moment(post.startTime) + const { t } = useTranslation() return (
@@ -61,7 +63,7 @@ const PostListRow = (props) => { {creator.name} { numOtherCommentors > 1 - ? ( and {numOtherCommentors} others) + ? ( {t('and')} {numOtherCommentors} {t('others')}) : null }
} @@ -69,10 +71,9 @@ const PostListRow = (props) => { {childPost &&
- {/* TODO: i18n on tooltip */} -
- {/* - Note: Can make memberDetails optional by adding a `withDetails` flag - sending in `goToMember` and switchin the onClick on a `MemberRow` to - go there instead of showing detail and making adding a conditional - style to make width of members-list be 100% in that case. - */} -
- {this.props.members.length > 7 && } -
- {members.map(member => )} -
+ return ( + +
+ {/* + Note: Can make memberDetails optional by adding a `withDetails` flag + sending in `goToMember` and switchin the onClick on a `MemberRow` to + go there instead of showing detail and making adding a conditional + style to make width of members-list be 100% in that case. + */} +
+ {this.props.members.length > 7 && } +
+ {members.map(member => )} +
+
+ {selectedMember && }
- {selectedMember && } -
- + + ) } } function MemberRow ({ member, selected, onClick }) { const { name, avatarUrl, response } = member - return
-
-
-
-
- {name} + return ( +
+
+
+
+
+ {name} +
+ {response &&
+ {humanResponse(response)} +
}
- {response &&
- {humanResponse(response)} -
} -
+ ) } function MemberDetail ({ member, currentGroup }) { - return
- -
+ return ( +
+ +
+ ) } + +export default withTranslation()(PostPeopleDialog) diff --git a/src/components/PostSelector/PostSelector.js b/src/components/PostSelector/PostSelector.js index d76d2758e..3ab38aef8 100644 --- a/src/components/PostSelector/PostSelector.js +++ b/src/components/PostSelector/PostSelector.js @@ -1,6 +1,7 @@ import update from 'immutability-helper' import { isEmpty } from 'lodash' import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' import useInView from 'react-cool-inview' import { useDrag, useDrop } from 'react-dnd' import { useDispatch, useSelector } from 'react-redux' @@ -24,6 +25,7 @@ export default function PostSelector ({ collection, draggable, group, onRemovePo const [selectedPosts, setSelectedPosts] = useState(posts || []) const [suggestionsOpen, setSuggestionsOpen] = useState(false) const searchBoxRef = useRef() + const { t } = useTranslation() const debouncedAutcomplete = useDebounce(autocomplete, 300) @@ -82,7 +84,7 @@ export default function PostSelector ({ collection, draggable, group, onRemovePo } const handleDelete = (post, index) => () => { - if (window.confirm('Remove post?')) { + if (window.confirm(t('Remove post?'))) { setSelectedPosts((prevPosts) => { onRemovePost(post) return update(prevPosts, { @@ -142,7 +144,7 @@ export default function PostSelector ({ collection, draggable, group, onRemovePo handleInputChange(event.target.value)} onFocus={() => setSuggestionsOpen(true)} @@ -171,6 +173,7 @@ export default function PostSelector ({ collection, draggable, group, onRemovePo export function SelectedPost ({ draggable, post, index, movePost, handleDelete }) { const ref = useRef(null) + const { t } = useTranslation() const [{ handlerId }, drop] = useDrop({ // eslint-disable-line no-unused-vars accept: 'post', @@ -235,7 +238,7 @@ export function SelectedPost ({ draggable, post, index, movePost, handleDelete } return
  • {post.title} - + {draggable && }
  • } diff --git a/src/components/PublicToggle/PublicToggle.js b/src/components/PublicToggle/PublicToggle.js index 2cd49081c..c2f752f01 100644 --- a/src/components/PublicToggle/PublicToggle.js +++ b/src/components/PublicToggle/PublicToggle.js @@ -1,12 +1,13 @@ import PropTypes from 'prop-types' import React, { Component } from 'react' +import { withTranslation } from 'react-i18next' import cx from 'classnames' import SwitchStyled from 'components/SwitchStyled' import Icon from 'components/Icon' import './PublicToggle.scss' const { func, bool } = PropTypes -export default class PublicToggle extends Component { +class PublicToggle extends Component { static propTypes = { isPublic: bool, togglePublic: func @@ -17,16 +18,17 @@ export default class PublicToggle extends Component { } render () { - const { isPublic, togglePublic } = this.props + const { isPublic, togglePublic, t } = this.props return (
    - Make Public: + {t('Make Public:')} - {isPublic ? 'Anyone on Hylo can see this post' : 'Currently, only groups you specify above will see this post'} + {isPublic ? t('Anyone on Hylo can see this post') : t('Currently, only groups you specify above will see this post')}
    ) } } +export default withTranslation()(PublicToggle) diff --git a/src/components/RemovableListItem/RemovableListItem.js b/src/components/RemovableListItem/RemovableListItem.js index 7ad09b891..828687c30 100644 --- a/src/components/RemovableListItem/RemovableListItem.js +++ b/src/components/RemovableListItem/RemovableListItem.js @@ -1,14 +1,16 @@ import React from 'react' +import { useTranslation } from 'react-i18next' import { Link } from 'react-router-dom' import { DEFAULT_AVATAR } from 'store/models/Group' import RoundImage from 'components/RoundImage' import './RemovableListItem.scss' export default function RemovableListItem ({ item, removeItem, skipConfirm = false, square, size, confirmMessage, url }) { + const { t } = useTranslation() const remove = () => { if (skipConfirm) return removeItem(item.id) - confirmMessage = confirmMessage || `Are you sure you want to remove ${item.name}?` + confirmMessage = confirmMessage || t('Are you sure you want to remove {{item.name}}?', { item }) if (window.confirm(confirmMessage)) { removeItem(item.id) } @@ -26,6 +28,6 @@ export default function RemovableListItem ({ item, removeItem, skipConfirm = fal {url && {title}} {!url && {title}} - {removeItem && Remove} + {removeItem && {t('Remove')}}
    } diff --git a/src/components/RemovableListItem/RemovableListItem.test.js b/src/components/RemovableListItem/RemovableListItem.test.js index bfed5a884..cf9fe45e0 100644 --- a/src/components/RemovableListItem/RemovableListItem.test.js +++ b/src/components/RemovableListItem/RemovableListItem.test.js @@ -2,6 +2,18 @@ import RemovableListItem from './RemovableListItem' import { shallow } from 'enzyme' import React from 'react' +jest.mock('react-i18next', () => ({ + ...jest.requireActual('react-i18next'), + useTranslation: (domain) => { + return { + t: (str) => str, + i18n: { + changeLanguage: () => new Promise(() => {}) + } + } + } +})) + describe('RemovableListItem', () => { it('renders correctly', () => { const item = { diff --git a/src/components/RemovableListItem/__snapshots__/RemovableListItem.test.js.snap b/src/components/RemovableListItem/__snapshots__/RemovableListItem.test.js.snap index d57679b72..fb80cf56b 100644 --- a/src/components/RemovableListItem/__snapshots__/RemovableListItem.test.js.snap +++ b/src/components/RemovableListItem/__snapshots__/RemovableListItem.test.js.snap @@ -13,6 +13,7 @@ exports[`RemovableListItem does not render as links when no URL specified 1`] = Zeus @@ -63,6 +64,7 @@ exports[`RemovableListItem renders correctly 1`] = ` Zeus diff --git a/src/components/SendAnnouncementModal/SendAnnouncementModal.js b/src/components/SendAnnouncementModal/SendAnnouncementModal.js index bd98c95e1..50e31c195 100644 --- a/src/components/SendAnnouncementModal/SendAnnouncementModal.js +++ b/src/components/SendAnnouncementModal/SendAnnouncementModal.js @@ -1,4 +1,5 @@ import React from 'react' +import { useTranslation } from 'react-i18next' import './SendAnnouncementModal.scss' import Button from '../Button' @@ -12,26 +13,27 @@ export default function SendAnnouncementModal ({ const groupIds = groups.map(c => c.id) const groupModIds = myModeratedGroups.map(c => c.id) const canModerateAllGroups = groupIds.every(val => groupModIds.indexOf(val) >= 0) + const { t } = useTranslation() return
    -

    MAKE AN ANNOUNCEMENT

    +

    {t('MAKE AN ANNOUNCEMENT')}

    {groupCount === 1 && canModerateAllGroups && -

    This means that all members of this group will receive an instant email and push notification about this Post. (This feature is available to moderators only.)

    +

    {t('This means that all members of this group will receive an instant email and push notification about this Post. (This feature is available to moderators only.')}

    } {groupCount > 1 && canModerateAllGroups && -

    This means that all members of the {groupCount} groups selected will receive instant email and push notifications about this Post. (This feature is available to moderators only.)

    +

    {t('This means that all members of the {{groupCount}} groups selected will receive instant email and push notifications about this Post. (This feature is available to moderators only.', { groupCount })}

    } {!canModerateAllGroups && -

    This means that all members of the {groupCount} groups selected will receive an instant email and push notification about this Post.

    -

    This will only be sent as an Announcement to the groups where you are a Moderator. For other groups it will be shared as a regular Post.

    +

    {t('This means that all members of the {{groupCount}} groups selected will receive an instant email and push notification about this Post.', { groupCount })}

    +

    {t('This will only be sent as an Announcement to the groups where you are a Moderator. For other groups it will be shared as a regular Post.')}

    }
    - - + +
    diff --git a/src/components/SendAnnouncementModal/SendAnnouncementModal.test.js b/src/components/SendAnnouncementModal/SendAnnouncementModal.test.js index 3be7003f9..baae4826d 100644 --- a/src/components/SendAnnouncementModal/SendAnnouncementModal.test.js +++ b/src/components/SendAnnouncementModal/SendAnnouncementModal.test.js @@ -2,6 +2,18 @@ import SendAnnouncementModal from './SendAnnouncementModal' import { shallow } from 'enzyme' import React from 'react' +jest.mock('react-i18next', () => ({ + ...jest.requireActual('react-i18next'), + useTranslation: (domain) => { + return { + t: (str) => str, + i18n: { + changeLanguage: () => new Promise(() => {}) + } + } + } +})) + it('renders correctly with one group', () => { const props = { closeModal: jest.fn(), diff --git a/src/components/SendAnnouncementModal/__snapshots__/SendAnnouncementModal.test.js.snap b/src/components/SendAnnouncementModal/__snapshots__/SendAnnouncementModal.test.js.snap index c32f3f0cc..9158bea84 100644 --- a/src/components/SendAnnouncementModal/__snapshots__/SendAnnouncementModal.test.js.snap +++ b/src/components/SendAnnouncementModal/__snapshots__/SendAnnouncementModal.test.js.snap @@ -48,8 +48,7 @@ exports[`renders correctly with multiple groups, where the current user is not a

    - This means that all members of the - groups selected will receive an instant email and push notification about this Post. + This means that all members of the {{groupCount}} groups selected will receive an instant email and push notification about this Post.

    } } +export default withTranslation()(SkillsSection) diff --git a/src/components/SkillsSection/SkillsSection.test.js b/src/components/SkillsSection/SkillsSection.test.js index 07ea328d7..56ee84a57 100644 --- a/src/components/SkillsSection/SkillsSection.test.js +++ b/src/components/SkillsSection/SkillsSection.test.js @@ -2,6 +2,14 @@ import SkillsSection from './SkillsSection' import { shallow } from 'enzyme' import React from 'react' +jest.mock('react-i18next', () => ({ + ...jest.requireActual('react-i18next'), + withTranslation: () => Component => { + Component.defaultProps = { ...Component.defaultProps, t: (str) => str } + return Component + } +})) + it('shows basic pills', () => { const skills = [{ id: 1, name: 'test' }, { id: 2, name: 'unclickable' }] diff --git a/src/components/SkillsSection/__snapshots__/SkillsSection.test.js.snap b/src/components/SkillsSection/__snapshots__/SkillsSection.test.js.snap index a48e6b409..32704eb23 100644 --- a/src/components/SkillsSection/__snapshots__/SkillsSection.test.js.snap +++ b/src/components/SkillsSection/__snapshots__/SkillsSection.test.js.snap @@ -25,6 +25,7 @@ exports[`shows basic pills 1`] = ` ] } placeholder="What skills and interests do you have?" + t={[Function]} />

    `; @@ -55,6 +56,7 @@ exports[`shows editable fields when isMe = true 1`] = ` ] } placeholder="What skills and interests do you have?" + t={[Function]} />
    `; diff --git a/src/components/SkillsToLearnSection/SkillsToLearnSection.js b/src/components/SkillsToLearnSection/SkillsToLearnSection.js index 357b5d44a..9e73e689f 100644 --- a/src/components/SkillsToLearnSection/SkillsToLearnSection.js +++ b/src/components/SkillsToLearnSection/SkillsToLearnSection.js @@ -1,4 +1,10 @@ import React from 'react' +import { useTranslation } from 'react-i18next' import SkillsSection from '../SkillsSection/SkillsSection' -export default (props) => +const SkillsToLearnSection = (props) => { + const { t } = useTranslation() + return +} + +export default SkillsToLearnSection diff --git a/src/components/SkillsToLearnSection/SkillsToLearnSection.test.js b/src/components/SkillsToLearnSection/SkillsToLearnSection.test.js index f08d904cd..177499335 100644 --- a/src/components/SkillsToLearnSection/SkillsToLearnSection.test.js +++ b/src/components/SkillsToLearnSection/SkillsToLearnSection.test.js @@ -2,6 +2,18 @@ import SkillsToLearnSection from './SkillsToLearnSection' import { shallow } from 'enzyme' import React from 'react' +jest.mock('react-i18next', () => ({ + ...jest.requireActual('react-i18next'), + useTranslation: (domain) => { + return { + t: (str) => str, + i18n: { + changeLanguage: () => new Promise(() => {}) + } + } + } +})) + it('shows basic pills', () => { const skills = [{ id: 1, name: 'test' }, { id: 2, name: 'unclickable' }] diff --git a/src/components/SkillsToLearnSection/__snapshots__/SkillsToLearnSection.test.js.snap b/src/components/SkillsToLearnSection/__snapshots__/SkillsToLearnSection.test.js.snap index 02f16045c..0c70867db 100644 --- a/src/components/SkillsToLearnSection/__snapshots__/SkillsToLearnSection.test.js.snap +++ b/src/components/SkillsToLearnSection/__snapshots__/SkillsToLearnSection.test.js.snap @@ -1,8 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`shows basic pills 1`] = ` -} {customViewType === 'stream' - ?
    {numCustomFilters} Filters
    + ?
    {numCustomFilters} {t('Filters')}
    : customViewType === 'collection' - ?
    Collection
    + ?
    {t('Collection')}
    : ''} {subtitle &&
    @@ -86,18 +94,50 @@ export default function StreamBanner ({
    + {currentUserHasMemberships && context !== CONTEXT_MY && ()} + + {/* The ReactTooltip with getContent breaks our snapshots because it uses dynamic classname, so removing in our tests */} + {!isTesting && ( + + Displaying   + {customActivePostsOnly ? 'active' : ''} + + + {customPostTypes.length === 0 ? 'None' : customPostTypes.map((p, i) => {p}s +)} + {customViewTopics.length > 0 &&
    filtered by topics:
    } + {customViewTopics.length > 0 && customViewTopics.map(t => #{t.name})} +
    + : '' + ) + }} + />)} - {currentUserHasMemberships && context !== CONTEXT_MY && ()} + />} {/* The ReactTooltip with getContent breaks our snapshots because it uses dynamic classname, so removing in our tests */} - {!isTesting && ( - Displaying   + {t('Displaying') + ' '}; {customActivePostsOnly ? 'active' : ''} - {customPostTypes.length === 0 ? 'None' : customPostTypes.map((p, i) => {p}s +)} - {customViewTopics.length > 0 &&
    filtered by topics:
    } + {customPostTypes.length === 0 ? t('None') : customPostTypes.map((p, i) => {p}s +)} + {customViewTopics.length > 0 &&
    {t('filtered by topics:')}
    } {customViewTopics.length > 0 && customViewTopics.map(t => #{t.name})} : '' ) }} - />)} + />} ) } -export function postPromptString (type = '', { firstName }) { - const postPrompts = { - event: `Hi ${firstName}, want to create an event?`, - offer: `Hi ${firstName}, what would you like to share?`, - request: `Hi ${firstName}, what are you looking for?`, - project: `Hi ${firstName}, what would you like to create?`, - resource: `Hi ${firstName}, what resource is available?`, - default: `Hi ${firstName}, what's on your mind?` - } - - return postPrompts[type] || postPrompts['default'] -} - -export class PostPrompt extends React.Component { - static defaultProps = { - firstName: '', - promptStringFunc: postPromptString, - querystringParams: {}, - routeParams: {}, - type: '' +export const PostPrompt = (props) => { + const { avatarUrl, className = 'post-prompt', firstName = '', type = '', querystringParams = {}, routeParams = {} } = props + const [hover, setHover] = useState(false) + const { t } = useTranslation() + + const postPromptString = (type, firstName) => { + const postPrompts = { + offer: t('Hi {{firstName}}, what would you like to share?', { firstName }), + request: t('Hi {{firstName}}, what are you looking for?', { firstName }), + project: t('Hi {{firstName}}, what would you like to create?', { firstName }), + event: t('Hi {{firstName}}, want to create an event?', { firstName }), + default: t('Hi {{firstName}}, what\'s on your mind?', { firstName }) + } + return postPrompts[type] || postPrompts.default } - constructor (props) { - super(props) - this.state = { hover: false } - } - - onMouseEnterHandler = () => this.setState({ hover: true }) - - onMouseLeaveHandler = () => this.setState({ hover: false }) - - render () { - const { avatarUrl, className, firstName, type, promptStringFunc, querystringParams, routeParams } = this.props - const { hover } = this.state - - return ( -
    - -
    - - {promptStringFunc(type, { firstName })} -
    - -
    -
    - ) - } + return ( +
    setHover(true)} onMouseLeave={() => setHover(false)}> + +
    + + {postPromptString(type, firstName)} +
    + +
    +
    + ) } diff --git a/src/components/StreamBanner/StreamBanner.test.js b/src/components/StreamBanner/StreamBanner.test.js index 6b50444a5..514ceefe1 100644 --- a/src/components/StreamBanner/StreamBanner.test.js +++ b/src/components/StreamBanner/StreamBanner.test.js @@ -1,7 +1,20 @@ import React from 'react' -import StreamBanner, { postPromptString } from './StreamBanner' +import StreamBanner, { PostPrompt } from './StreamBanner' +import { BrowserRouter } from 'react-router-dom' import { mount } from 'enzyme' +jest.mock('react-i18next', () => ({ + ...jest.requireActual('react-i18next'), + useTranslation: (domain) => { + return { + t: (str) => str, + i18n: { + changeLanguage: () => new Promise(() => {}) + } + } + } +})) + const currentUser = { avatarUrl: 'me.png', firstName: () => 'Bob', @@ -46,11 +59,11 @@ it('matches the snapshot for an orphan user', () => { expect(node).toMatchSnapshot() }) -describe('postPromptString', () => { - it('renders a post prompt string', () => { - const firstName = 'anybody' +describe('PostPrompt', () => { + it('renders a post prompt', () => { + const firstName = 'Arturo' + const wrapper = mount() - expect(postPromptString('project', { firstName })).toMatchSnapshot() - expect(postPromptString('', { firstName })).toMatchSnapshot() + expect(wrapper).toMatchSnapshot() }) }) diff --git a/src/components/StreamBanner/__snapshots__/StreamBanner.test.js.snap b/src/components/StreamBanner/__snapshots__/StreamBanner.test.js.snap index ada32d883..8304fa206 100644 --- a/src/components/StreamBanner/__snapshots__/StreamBanner.test.js.snap +++ b/src/components/StreamBanner/__snapshots__/StreamBanner.test.js.snap @@ -1,5 +1,75 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`PostPrompt renders a post prompt 1`] = ` + + + +
    + + + +
    + +
    + + Hi {{firstName}}, what would you like to create? +
    +
    + + +
    +
    + + + +`; + exports[`matches the snapshot for an orphan user 1`] = ` `; -exports[`postPromptString renders a post prompt string 1`] = `"Hi anybody, what would you like to create?"`; - -exports[`postPromptString renders a post prompt string 2`] = `"Hi anybody, what's on your mind?"`; - exports[`renders for all groups 1`] = ` ( - - - {options.find(o => o.id === selected).label} - - } - items={options.map(({ id, label }) => ({ - label, - onClick: () => onChange(id) - }))} - /> -) +const makeDropdown = (selected, options, onChange) => { + const { t } = useTranslation() + return ( + + + {t(options.find(o => o.id === selected).label)} + + } + items={options.map(({ id, label }) => ({ + label: t(label), + onClick: () => onChange(id) + }))} + /> + ) +} const StreamViewControls = (props) => { + const { t } = useTranslation() + const POST_TYPE_OPTIONS = [ + { id: undefined, label: 'All Posts' }, + { id: 'discussion', label: 'Discussions' }, + { id: 'event', label: 'Events' }, + { id: 'offer', label: 'Offers' }, + { id: 'project', label: 'Projects' }, + { id: 'request', label: 'Requests' }, + { id: 'resource', label: 'Resources' } + ] + const { customViewType, sortBy, postTypeFilter, viewMode, changeSearch, changeSort, changeTab, changeView, context, searchValue, view, customPostTypes, changeChildPostInclusion, childPostInclusion } = props const [searchActive, setSearchActive] = useState(!!searchValue) const [searchState, setSearchState] = useState('') @@ -54,12 +58,11 @@ const StreamViewControls = (props) => {
    - {/* TODO: i18n on tooltip */} {![CONTEXT_MY, 'all', 'public'].includes(context) &&
    @@ -74,7 +77,7 @@ const StreamViewControls = (props) => {
    changeView('cards')} - data-tip='Card view' data-for='stream-viewmode-tip' + data-tip={t('Card view')} data-for='stream-viewmode-tip' >
    @@ -82,7 +85,7 @@ const StreamViewControls = (props) => {
    changeView('list')} - data-tip='List view' data-for='stream-viewmode-tip' + data-tip={t('List view')} data-for='stream-viewmode-tip' >
    @@ -90,7 +93,7 @@ const StreamViewControls = (props) => {
    changeView('bigGrid')} - data-tip='Large Grid' data-for='stream-viewmode-tip' + data-tip={t('Large Grid')} data-for='stream-viewmode-tip' >
    @@ -98,7 +101,7 @@ const StreamViewControls = (props) => {
    changeView('grid')} - data-tip='Small Grid' data-for='stream-viewmode-tip' + data-tip={t('Small Grid')} data-for='stream-viewmode-tip' >
    @@ -122,7 +125,7 @@ const StreamViewControls = (props) => { e.target.blur() } }} - placeholder='Search posts' + placeholder={t('Search posts')} value={searchState} />
    } diff --git a/src/components/TagInput/TagInput.js b/src/components/TagInput/TagInput.js index 24e5b29c9..15ae90642 100644 --- a/src/components/TagInput/TagInput.js +++ b/src/components/TagInput/TagInput.js @@ -1,11 +1,12 @@ import PropTypes from 'prop-types' import React, { Component } from 'react' +import { withTranslation } from 'react-i18next' import { debounce, includes, isEmpty } from 'lodash' import { uniqBy } from 'lodash/fp' import cx from 'classnames' import { getKeyCode, keyMap } from 'util/textInput' import Icon from 'components/Icon' -import { KeyControlledItemList } from 'components/KeyControlledList' +import KeyControlledItemList from 'components/KeyControlledList/KeyControlledItemList' import RoundImage from 'components/RoundImage' import { accessibilityIcon, visibilityIcon, accessibilityString, accessibilityDescription, visibilityString, visibilityDescription } from 'store/models/Group' import styles from './TagInput.scss' @@ -15,7 +16,7 @@ const { object, array, bool, string, func } = PropTypes // keys that can be pressed to create a new tag const creationKeyCodes = [keyMap.ENTER, keyMap.SPACE, keyMap.COMMA] -export default class TagInput extends Component { +class TagInput extends Component { static propTypes = { tags: array, type: string, @@ -113,12 +114,7 @@ export default class TagInput extends Component { }, 200) render () { - let { tags, placeholder } = this.props - - const { suggestions, className, theme, readOnly, maxTags, addLeadingHashtag, renderSuggestion, tagType } = this.props - if (!tags) tags = [] - if (!placeholder) placeholder = 'Type...' - + const { tags = [], placeholder = this.props.t('Type...'), suggestions, className, theme, readOnly, maxTags, addLeadingHashtag, renderSuggestion, tagType } = this.props const optionalHashtag = addLeadingHashtag ? '#' : '' const selectedItems = uniqBy('id', tags).map(t => @@ -130,13 +126,13 @@ export default class TagInput extends Component {
    -
    {accessibilityString(t.accessibility)} - {accessibilityDescription(t.accessibility)}
    +
    {this.props.t(accessibilityString(t.accessibility))} - {this.props.t(accessibilityDescription(t.accessibility))}
    -
    {visibilityString(t.visibility)} - {visibilityDescription(t.visibility)}
    +
    {this.props.t(visibilityString(t.visibility))} - {this.props.t(visibilityDescription(t.visibility))}
    } @@ -151,46 +147,49 @@ export default class TagInput extends Component { const suggestionsOrError = maxReached ? isEmpty(this.input.current.value) ? [] - : [{ name: `no more than ${maxTags} allowed`, isError: true }] + : [{ name: this.props.t('no more than {{maxTags}} allowed', { maxTags }), isError: true }] : suggestions - - return
    -
      - {selectedItems} -
    -
    -
    - { this.handleChange('') }} - onBlur={() => { - this.input.current.value = '' - this.handleChange(null) - }} - onChange={event => this.handleChange(event.target.value)} - onKeyDown={this.handleKeys} - disabled={readOnly} /> -
    - {!isEmpty(suggestionsOrError) && -
    - +
      + {selectedItems} +
    +
    +
    + { this.handleChange('') }} + onBlur={() => { + this.input.current.value = '' + this.handleChange(null) }} - ref={this.list} /> + onChange={event => this.handleChange(event.target.value)} + onKeyDown={this.handleKeys} + disabled={readOnly} />
    - } + {!isEmpty(suggestionsOrError) && +
    + +
    + } +
    -
    + ) } } + +export default withTranslation()(TagInput) diff --git a/src/components/TagInput/TagInput.test.js b/src/components/TagInput/TagInput.test.js index 1a66760e3..c0b0e81fa 100644 --- a/src/components/TagInput/TagInput.test.js +++ b/src/components/TagInput/TagInput.test.js @@ -2,6 +2,14 @@ import React from 'react' import { shallow } from 'enzyme' import TagInput from './TagInput' +jest.mock('react-i18next', () => ({ + ...jest.requireActual('react-i18next'), + withTranslation: () => Component => { + Component.defaultProps = { ...Component.defaultProps, t: (str) => str } + return Component + } +})) + const defaultMinProps = { handleInputChange: () => {} } diff --git a/src/components/TopicSelector/SingleTopicSelector.js b/src/components/TopicSelector/SingleTopicSelector.js index 0debcc857..da311fc10 100644 --- a/src/components/TopicSelector/SingleTopicSelector.js +++ b/src/components/TopicSelector/SingleTopicSelector.js @@ -1,4 +1,5 @@ import React, { Component } from 'react' +import { withTranslation } from 'react-i18next' import AsyncCreatableSelect from 'react-select/async-creatable' import styles from './TopicSelector.scss' import { isEmpty, sortBy } from 'lodash/fp' @@ -30,8 +31,7 @@ const inputStyles = { class SingleTopicSelector extends Component { static defaultProps = { currentGroup: null, - defaultTopics: [], - placeholder: 'Find/add a topic' + defaultTopics: [] } static defaultState = { @@ -70,10 +70,10 @@ class SingleTopicSelector extends Component { } render () { - const { currentGroup, defaultTopics, placeholder } = this.props + const { currentGroup, defaultTopics, placeholder = this.props.t('Find/add a topic'), t } = this.props const { value } = this.state - const defaultsToShow = defaultTopics ? [ { label: currentGroup.name + ' topics', options: defaultTopics } ] : [] + const defaultsToShow = defaultTopics ? [{ label: t('{{currentGroup.name}} topics', { currentGroup }), options: defaultTopics }] : [] return ( ({ name: inputValue, label: inputValue, value: inputValue, __isNew__: true })} noOptionsMessage={() => { - return 'Start typing to find/create a topic to add' + return t('Start typing to find/create a topic to add') }} isOptionDisabled={option => option.__isNew__ && option.value.length < 3} formatOptionLabel={(item, { context, inputValue, selectValue }) => { @@ -96,7 +96,7 @@ class SingleTopicSelector extends Component { return
    #{item.label}
    } if (item.__isNew__) { - return
    {item.value.length < 3 ? 'Topics must be longer than 2 characters' : `Create topic "${item.value}"`}
    + return
    {item.value.length < 3 ? t('Topics must be longer than 2 characters') : t('Create topic "{{item.value}}"', { item })}
    } const { name, postsTotal, followersTotal } = item const formatCount = count => isNaN(count) @@ -108,8 +108,8 @@ class SingleTopicSelector extends Component { return
    #{name}
    - {formatCount(followersTotal)} subscribers - {formatCount(postsTotal)} posts + {formatCount(followersTotal)} {t('subscribers')} + {formatCount(postsTotal)} {t('posts')}
    }} @@ -118,4 +118,4 @@ class SingleTopicSelector extends Component { } } -export default connector(SingleTopicSelector) +export default withTranslation()(connector(SingleTopicSelector)) diff --git a/src/components/TopicSelector/TopicSelector.js b/src/components/TopicSelector/TopicSelector.js index ffb8dec9f..8fe6fddac 100644 --- a/src/components/TopicSelector/TopicSelector.js +++ b/src/components/TopicSelector/TopicSelector.js @@ -1,4 +1,5 @@ import React, { Component } from 'react' +import { withTranslation } from 'react-i18next' import AsyncCreatableSelect from 'react-select/async-creatable' import styles from './TopicSelector.scss' import { isEmpty, isEqual, uniqBy, orderBy, get, includes } from 'lodash/fp' @@ -49,11 +50,10 @@ const inputStyles = { indicatorSeparator: styles => ({ display: 'none' }) } -export default class TopicSelector extends Component { +class TopicSelector extends Component { static defaultProps = { forGroups: [], defaultTopics: [], - placeholder: 'Enter up to three topics...', selectedTopics: [] } @@ -113,7 +113,7 @@ export default class TopicSelector extends Component { [{ label: forGroups && forGroups.length > 0 ? forGroups[0].name - : 'Default Topics', + : this.props.t('Default Topics'), options: groupTopics }] ) @@ -142,7 +142,7 @@ export default class TopicSelector extends Component { return [ ...this.formatGroupTopicSuggestions(filteredDefaultTopics) || [], { - label: 'All Topics', + label: this.props.t('All Topics'), options: sortedTopicResults } ] @@ -162,7 +162,7 @@ export default class TopicSelector extends Component { } render () { - const { placeholder, defaultTopics: providedDefaultTopics } = this.props + const { placeholder = this.props.t('Enter up to three topics...'), defaultTopics: providedDefaultTopics, t } = this.props const { selected } = this.state const defaultTopics = this.formatGroupTopicSuggestions(providedDefaultTopics) || [] @@ -191,8 +191,8 @@ export default class TopicSelector extends Component { }} noOptionsMessage={() => { return selected.length >= MAX_TOPICS - ? `You can only select up to ${MAX_TOPICS} topics` - : 'Start typing to add a topic' + ? t(`You can only select up to {{MAX_TOPICS}} topics`, { MAX_TOPICS }) + : t('Start typing to add a topic') }} formatOptionLabel={(item, { context }) => { if (context === 'value') { @@ -200,7 +200,7 @@ export default class TopicSelector extends Component { } if (item.__isNew__) { - return
    Create topic "#{item.value}"
    + return
    {t('Create topic "#{{item.value}}"', { item })}
    } const { name, postsTotal, followersTotal } = item @@ -214,8 +214,8 @@ export default class TopicSelector extends Component {
    #{name}
    - {formatCount(followersTotal)} subscribers - {formatCount(postsTotal)} posts + {formatCount(followersTotal)} {t('subscribers')} + {formatCount(postsTotal)} {t('posts')}
    ) @@ -224,3 +224,5 @@ export default class TopicSelector extends Component { ) } } + +export default withTranslation()(TopicSelector) diff --git a/src/components/TopicSelector/TopicSelector.test.js b/src/components/TopicSelector/TopicSelector.test.js index 4ecff49be..2f54cda26 100644 --- a/src/components/TopicSelector/TopicSelector.test.js +++ b/src/components/TopicSelector/TopicSelector.test.js @@ -2,6 +2,14 @@ import React from 'react' import { shallow } from 'enzyme' import TopicSelector from './TopicSelector' +jest.mock('react-i18next', () => ({ + ...jest.requireActual('react-i18next'), + withTranslation: () => Component => { + Component.defaultProps = { ...Component.defaultProps, t: (str) => str } + return Component + } +})) + describe('TopicSelector', () => { const defaultMinProps = { fetchDefaultTopics: () => {} diff --git a/src/components/TopicSupportComingSoon/TopicSupportComingSoon.test.js b/src/components/TopicSupportComingSoon/TopicSupportComingSoon.test.js index abdf051a3..b2ad748c4 100644 --- a/src/components/TopicSupportComingSoon/TopicSupportComingSoon.test.js +++ b/src/components/TopicSupportComingSoon/TopicSupportComingSoon.test.js @@ -1,6 +1,18 @@ -import TopicSupportComingSoon from './index' -import { shallow } from 'enzyme' import React from 'react' +import { shallow } from 'enzyme' +import TopicSupportComingSoon from './index' + +jest.mock('react-i18next', () => ({ + ...jest.requireActual('react-i18next'), + useTranslation: (domain) => { + return { + t: (str) => str, + i18n: { + changeLanguage: () => new Promise(() => {}) + } + } + } +})) it('renders', () => { const wrapper = shallow() diff --git a/src/components/TopicSupportComingSoon/__snapshots__/TopicSupportComingSoon.test.js.snap b/src/components/TopicSupportComingSoon/__snapshots__/TopicSupportComingSoon.test.js.snap index 2ef9c0204..4bbf78c57 100644 --- a/src/components/TopicSupportComingSoon/__snapshots__/TopicSupportComingSoon.test.js.snap +++ b/src/components/TopicSupportComingSoon/__snapshots__/TopicSupportComingSoon.test.js.snap @@ -6,15 +6,13 @@ exports[`renders 1`] = ` >

    We're working on expanding -
    - #topics to more places +#topics to more places

    In the meantime, click a topic from an individual -
    - group to see posts from that group. +group to see posts from that group.

    -

    We're working on expanding
    #topics to more places

    -

    In the meantime, click a topic from an individual
    group to see posts from that group.

    +

    {t(`We're working on expanding\n#topics to more places`)}

    +

    {t('In the meantime, click a topic from an individual\ngroup to see posts from that group.')}

    - +
    diff --git a/src/components/Widget/AtAGlanceWidget/AtAGlanceWidget.js b/src/components/Widget/AtAGlanceWidget/AtAGlanceWidget.js index afa127ece..6335b4128 100644 --- a/src/components/Widget/AtAGlanceWidget/AtAGlanceWidget.js +++ b/src/components/Widget/AtAGlanceWidget/AtAGlanceWidget.js @@ -1,4 +1,5 @@ import React from 'react' +import { useTranslation } from 'react-i18next' import { capitalize } from 'lodash' import FancyLink from 'components/FancyLink' import { getSocialMedia, getWebsite, getAtAGlance } from 'store/selectors/farmExtensionSelectors' @@ -15,12 +16,13 @@ export default function AtAGlanceWidget ({ group }) { const socialMedia = sanitizeURL(getSocialMedia(group)) const website = sanitizeURL(getWebsite(group)) const atAGlance = getAtAGlance(group) + const { t } = useTranslation() return (
    {atAGlance.length > 0 && capitalize(item))} />} - {website && } - {socialMedia && } + {website && } + {socialMedia && }
    ) } diff --git a/src/components/Widget/EventsWidget/EventsWidget.js b/src/components/Widget/EventsWidget/EventsWidget.js index 54b82d733..e5c861371 100644 --- a/src/components/Widget/EventsWidget/EventsWidget.js +++ b/src/components/Widget/EventsWidget/EventsWidget.js @@ -1,6 +1,7 @@ import cx from 'classnames' import moment from 'moment-timezone' import React, { useCallback, useState } from 'react' +import { useTranslation } from 'react-i18next' import Icon from 'components/Icon' import { Link } from 'react-router-dom' import Slider from 'react-slick' @@ -36,6 +37,7 @@ export default ({ items, group, routeParams, isMember }) => { }, [swiped] ) + const { t } = useTranslation() return (
    @@ -55,9 +57,9 @@ export default ({ items, group, routeParams, isMember }) => {
    -

    Bring your group together

    -

    What you will do at your next event?

    -
    + Create an event
    +

    {t('Bring your group together')}

    +

    {t('What you will do at your next event?')}

    +
    {t('+ Create an event')}
    } diff --git a/src/components/Widget/FarmOpenToPublic/FarmOpenToPublic.js b/src/components/Widget/FarmOpenToPublic/FarmOpenToPublic.js index 1f0c829f0..5c54fe33c 100644 --- a/src/components/Widget/FarmOpenToPublic/FarmOpenToPublic.js +++ b/src/components/Widget/FarmOpenToPublic/FarmOpenToPublic.js @@ -1,4 +1,5 @@ import React from 'react' +import { useTranslation } from 'react-i18next' import { keyBy } from 'lodash' import { getOpeningHours, getPublicOfferings, getOpenToPublic, getFarmAddressLine1, getFarmLocality, getFarmAdministrativeArea, getFarmPostalCode, getFarmCountryCode } from 'store/selectors/farmExtensionSelectors' import { PUBLIC_OFFERINGS } from 'util/constants' @@ -16,18 +17,19 @@ export default function FarmOpenToPublic ({ group }) { const openingHours = getOpeningHours(group) const publicOfferings = getPublicOfferings(group) const openToPublic = getOpenToPublic(group) + const { t } = useTranslation() return ( openToPublic ?
    -
    Open {openingHours}
    +
    {t('Open', { openingHours })}
    {getFarmAddressLine1(group) &&
    {getFarmAddressLine1(group)}
    {`${getFarmLocality(group)}, ${getFarmAdministrativeArea(group)}`}
    {`${getFarmPostalCode(group)}, ${getFarmCountryCode(group)}`}
    } - {publicOfferings.length > 0 && publicOfferingsLookup[offering].label)} title='Public Offerings' />} + {publicOfferings.length > 0 && publicOfferingsLookup[offering].label)} title={t('Public Offerings')} />}
    : null ) diff --git a/src/components/Widget/GroupsWidget/GroupsWidget.js b/src/components/Widget/GroupsWidget/GroupsWidget.js index 09ec24539..7870b1af4 100644 --- a/src/components/Widget/GroupsWidget/GroupsWidget.js +++ b/src/components/Widget/GroupsWidget/GroupsWidget.js @@ -1,4 +1,5 @@ import React, { Component } from 'react' +import { withTranslation, useTranslation } from 'react-i18next' import PropTypes from 'prop-types' import { Link } from 'react-router-dom' import Slider from 'react-slick' @@ -32,7 +33,7 @@ const sliderSettings = { ] } -export default class GroupsWidget extends Component { +class GroupsWidget extends Component { static propTypes = { group: object, items: array, @@ -48,7 +49,7 @@ export default class GroupsWidget extends Component { {items && items.map(group => )}
    - + Create Group + {this.props.t('+ Create Group')} {/* might have to make this conditional, to suit a wider range of uses */}
    @@ -60,13 +61,14 @@ export default class GroupsWidget extends Component { } export function GroupCard ({ group, routeParams, className }) { + const { t } = useTranslation() return (
    {group.name}
    -
    {group.memberCount} member{group.memberCount !== 1 ? 's' : ''}
    +
    {group.memberCount} {t('member', { count: group.memberCount })}
    @@ -74,13 +76,15 @@ export function GroupCard ({ group, routeParams, className }) { {group.description && group.description.length > 140 &&
    }
    {group.memberStatus === 'member' - ?
    MemberVisit
    + ?
    {t('Member')}{t('Visit')}
    : group.memberStatus === 'requested' - ?
    PendingView
    - :
    ViewView
    } + ?
    {t('Pending')}{t('View')}
    + :
    {t('View')}{t('View')}
    }
    ) } + +export default withTranslation()(GroupsWidget) diff --git a/src/components/Widget/GroupsWidget/GroupsWidget.test.js b/src/components/Widget/GroupsWidget/GroupsWidget.test.js index e87b4cedc..24925a241 100644 --- a/src/components/Widget/GroupsWidget/GroupsWidget.test.js +++ b/src/components/Widget/GroupsWidget/GroupsWidget.test.js @@ -3,6 +3,22 @@ import { MemoryRouter } from 'react-router' import { mount } from 'enzyme' import GroupsWidget from './GroupsWidget' +jest.mock('react-i18next', () => ({ + ...jest.requireActual('react-i18next'), + useTranslation: (domain) => { + return { + t: (str) => str, + i18n: { + changeLanguage: () => new Promise(() => {}) + } + } + }, + withTranslation: () => Component => { + Component.defaultProps = { ...Component.defaultProps, t: (str) => str } + return Component + } +})) + const defaultMinProps = { routeParams: { context: 'groups', slug: 'group one' }, items: [] diff --git a/src/components/Widget/GroupsWidget/__snapshots__/GroupsWidget.test.js.snap b/src/components/Widget/GroupsWidget/__snapshots__/GroupsWidget.test.js.snap index 37cee4f65..4536a725b 100644 --- a/src/components/Widget/GroupsWidget/__snapshots__/GroupsWidget.test.js.snap +++ b/src/components/Widget/GroupsWidget/__snapshots__/GroupsWidget.test.js.snap @@ -49,6 +49,7 @@ exports[`GroupsWidget renders correctly (with min props) 1`] = ` "slug": "group one", } } + t={[Function]} >
    1 - member + + member
    10 - member - s + + member
    dispatch(removeSkill(skillId)) const handleJoinGroup = (groupId) => dispatch(joinGroup(groupId)) const handleRequestToJoinGroup = (groupId, questionAnswers) => dispatch(createJoinRequest(groupId, questionAnswers)) + const { t } = useTranslation() return
    {!currentUser - ?
    Signup or Login to connect with {group.name}
    + ?
    {t('Signup or Login to connect with')} {group.name}
    :
    -
    Recent Posts
    +
    {t('Recent Posts')}
    - Only members of this group can see posts + {t('Only members of this group can see posts')}
    -
    {group.memberCount} {group.memberCount > 1 ? `Members` : `Member`}
    +
    {group.memberCount} {group.memberCount > 1 ? t(`Members`) : t(`Member`)}
    {get(group, 'settings.publicMemberDirectory') ?
    {group.members.map(member => { return
    {member.name}
    })}
    :
    - Join to see + {t('Join to see')}
    }
    diff --git a/src/components/Widget/MapWidget/MapWidget.js b/src/components/Widget/MapWidget/MapWidget.js index 5750dee87..c370ad10e 100644 --- a/src/components/Widget/MapWidget/MapWidget.js +++ b/src/components/Widget/MapWidget/MapWidget.js @@ -1,11 +1,12 @@ import PropTypes from 'prop-types' import React, { Component } from 'react' +import { withTranslation } from 'react-i18next' import './MapWidget.scss' const { array } = PropTypes -export default class MapWidget extends Component { +class MapWidget extends Component { static propTypes = { map: array } @@ -13,8 +14,10 @@ export default class MapWidget extends Component { render () { return (
    - Community map + {this.props.t('Community map')}
    ) } } + +export default withTranslation()(MapWidget) diff --git a/src/components/Widget/MembersWidget/MembersWidget.js b/src/components/Widget/MembersWidget/MembersWidget.js index eb5e57411..ce93fc1a8 100644 --- a/src/components/Widget/MembersWidget/MembersWidget.js +++ b/src/components/Widget/MembersWidget/MembersWidget.js @@ -1,4 +1,5 @@ import React, { useCallback, useState } from 'react' +import { useTranslation } from 'react-i18next' import { Link } from 'react-router-dom' import Slider from 'react-slick' import Icon from 'components/Icon' @@ -33,6 +34,7 @@ export default ({ items, group }) => { }, [swiped] ) + const { t } = useTranslation() return (
    @@ -50,7 +52,7 @@ export default ({ items, group }) => {
    )}
    - All + {t('All')}
    diff --git a/src/components/Widget/ModeratorsWidget/ModeratorsWidget.js b/src/components/Widget/ModeratorsWidget/ModeratorsWidget.js index f8fd31da0..998b82878 100644 --- a/src/components/Widget/ModeratorsWidget/ModeratorsWidget.js +++ b/src/components/Widget/ModeratorsWidget/ModeratorsWidget.js @@ -1,11 +1,14 @@ import React from 'react' +import { useTranslation } from 'react-i18next' import './Moderators.scss' export default function ModeratorsWidget (props) { + const { t } = useTranslation() + return (
    - The moderators go here + {t('The moderators go here')}
    ) } diff --git a/src/components/Widget/OffersAndRequestsWidget/OffersAndRequestsWidget.js b/src/components/Widget/OffersAndRequestsWidget/OffersAndRequestsWidget.js index b3dfeea3c..bd9e380ac 100644 --- a/src/components/Widget/OffersAndRequestsWidget/OffersAndRequestsWidget.js +++ b/src/components/Widget/OffersAndRequestsWidget/OffersAndRequestsWidget.js @@ -1,6 +1,7 @@ import cx from 'classnames' import PropTypes from 'prop-types' import React, { Component } from 'react' +import { withTranslation } from 'react-i18next' import { Link } from 'react-router-dom' import { postUrl, createPostUrl } from 'util/navigation' import RoundImage from '../../RoundImage' @@ -9,7 +10,7 @@ import './OffersAndRequestsWidget.scss' const { array, bool, object } = PropTypes -export default class OffersAndRequestsWidget extends Component { +class OffersAndRequestsWidget extends Component { static propTypes = { isMember: bool, items: array, @@ -17,14 +18,14 @@ export default class OffersAndRequestsWidget extends Component { } render () { - const { isMember, items, routeParams } = this.props + const { isMember, items, routeParams, t } = this.props return (
    {items.map(p =>
    - {p.type} from {p.creator.name} + {t(p.type)}{' '}{t('from')}{' '}{p.creator.name} {p.commentsTotal}
    {p.title}
    @@ -33,12 +34,12 @@ export default class OffersAndRequestsWidget extends Component { )} {items.length < 3 && isMember ?
    - Create a request or offer! + {t('Create a request or offer!')}
    -
    What do you need? What are you offering?
    +
    {t('What do you need? What are you offering?')}
    - + New Offer - + New Request + {t('+ New')} {t('Offer')} + {t('+ New')} {t('Request')}
    : ' '} @@ -46,3 +47,5 @@ export default class OffersAndRequestsWidget extends Component { ) } } + +export default withTranslation()(OffersAndRequestsWidget) diff --git a/src/components/Widget/OpportunitiesToCollaborateWidget/OpportunitiesToCollaborateWidget.js b/src/components/Widget/OpportunitiesToCollaborateWidget/OpportunitiesToCollaborateWidget.js index cacf29dbe..40b590f44 100644 --- a/src/components/Widget/OpportunitiesToCollaborateWidget/OpportunitiesToCollaborateWidget.js +++ b/src/components/Widget/OpportunitiesToCollaborateWidget/OpportunitiesToCollaborateWidget.js @@ -1,4 +1,5 @@ import React, { useCallback } from 'react' +import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' import { newMessageUrl } from 'util/navigation' import useRouter from 'hooks/useRouter' @@ -23,9 +24,64 @@ export default function OpportunitiesToCollaborateWidget () { } export function OpportunityToCollaborate ({ group, opportunity }) { + const { t } = useTranslation() + const promptLookup = { // TODO: Can we figure out a way to reduce duplication here? + research: t('research projects'), + events: t('event collaboration'), + volunteering: t('volunteer opportunities'), + mentorship: t('mentorship & advice'), + cooperative: t('cooperatives'), + buy: t('purchasing from you'), + markets: t('new markets'), + ecosystem_service_markets: t('ecosystem services'), + loans: t('low-cost loans'), + support: t('farm support'), + equipment_sharing: t('equipment sharing'), + increase_sales: t('increasing sales'), + communication: t('helping with your communication & marketing'), + new_products_methods: t('introducing new methods & practices'), + carbon: t('carbon markets'), + environmental_impact: t('environmental impact'), + benchmarking: t('benchmarking your farm'), + insetting: t('insetting'), + reduce_risk: t('hazard risk mitigation'), + farm_valuation: t('farm valuation'), + certifications: t('certifications'), + food_nutrition: t('nutrient density'), + biodiversity: t('biodiversity'), + product_quality: t('product quality'), + animal_welfare: t('animal welfare') + } + const collabTitle = { + research: t('Research projects'), + events: t('Event collaboration'), + volunteering: t('Volunteer opportunities'), + mentorship: t('Mentorship & advice'), + cooperative: t('Cooperatives'), + buy: t('Buy from us'), + markets: t('New markets'), + ecosystem_service_markets: t('Ecosystem services'), + loans: t('Low-cost loans'), + support: t('Farm support'), + equipment_sharing: t('Equipment sharing'), + increase_sales: t('Increasing sales'), + communication: t('Communication & marketing'), + new_products_methods: t('New methods & practices'), + carbon: t('Carbon markets'), + environmental_impact: t('Environmental impact'), + benchmarking: t('Benchmarking'), + insetting: t('Insetting'), + reduce_risk: t('Hazard risk mitigation'), + farm_valuation: t('Farm valuation'), + certifications: t('Certifications'), + food_nutrition: t('Nutrient density'), + biodiversity: t('Biodiversity'), + product_quality: t('Product quality'), + animal_welfare: t('Animal welfare') + } const currentUser = useSelector(state => getMe(state)) const { push } = useRouter() - const prompt = `Hi there ${group.name}, I'd like to talk about ${promptLookup[opportunity]}.` + const prompt = t(`Hi there {{groupName}}, I'd like to talk about {{prompt}}.`, { groupName: group.name, prompt: promptLookup[opportunity] }) const goToGroupModeratorsMessage = useCallback(() => { push( `${newMessageUrl()}?participants=${group.moderators.map(m => m.id).join(',')}&prompt=${encodeURIComponent(prompt)}` @@ -51,93 +107,37 @@ export function OpportunityToCollaborate ({ group, opportunity }) { ) } -const collabTitle = { - research: 'Research projects', - events: 'Event collaboration', - volunteering: 'Volunteer opportunities', - mentorship: 'Mentorship & advice', - cooperative: 'Cooperatives', - buy: 'Buy from us', - markets: 'New markets', - ecosystem_service_markets: 'Ecosystem services', - loans: 'Low-cost loans', - support: 'Farm support', - equipment_sharing: 'Equipment sharing', - increase_sales: 'Increasing sales', - communication: 'Communication & marketing', - new_products_methods: 'New methods & practices', - carbon: 'Carbon markets', - environmental_impact: 'Environmental impact', - benchmarking: 'Benchmarking', - insetting: 'Insetting', - reduce_risk: 'Hazard risk mitigation', - farm_valuation: 'Farm valuation', - certifications: 'Certifications', - food_nutrition: 'Nutrient density', - biodiversity: 'Biodiversity', - product_quality: 'Product quality', - animal_welfare: 'Animal welfare' -} - const collabText = (group) => { + const { t } = useTranslation() return { - research: `${group.name} is available to participate in research`, - events: `${group.name} is open to co-hosting events`, - volunteering: `${group.name} has some volunteering opportunities`, - mentorship: `Contact ${group.name} to learn about their practices`, - cooperative: `${group.name} is interested in forming a cooperative`, - buy: `Buy from ${group.name}`, - markets: `${group.name} is seeking new markets`, - ecosystem_service_markets: `${group.name} is seeking ecosystem services`, - loans: `${group.name} is interested in low-cost loans`, - support: `${group.name} is seeking farm support`, - equipment_sharing: `${group.name} is interested in equipment sharing`, - increase_sales: `${group.name} is interested in increasing their sales`, - communication: `${group.name} seeks better communication / marketing to their buyers`, - new_products_methods: `${group.name} wants to learn about new methods or practices`, - carbon: `${group.name} is interested in carbon markets`, - environmental_impact: `${group.name} is curious about better understanding their farm through data (soil, environmental impacts, etc.)`, - benchmarking: `${group.name} wants to comparing my farm to others`, - insetting: `${group.name} wants to easily provide data to supply chain partners`, - reduce_risk: `${group.name} wants to reduce hazard risk (fire, flood, drought, etc.)`, - farm_valuation: `${group.name} wants to increase their farm valuation`, - certifications: `${group.name} is curious about further certification (organic, ISO, regenerative, etc.)`, - food_nutrition: `${group.name} is curious about understanding the nutrient density of their product`, - biodiversity: `${group.name} wants help focusing on biodiversity (protecting species, improving ecology, markets)`, - product_quality: `${group.name} wants to improve product quality`, - animal_welfare: `${group.name} wants to ensure animal welfare` + research: t(`{{group.name}} is available to participate in research`, { group }), + events: t(`{{group.name}} is open to co-hosting events`, { group }), + volunteering: t(`{{group.name}} has some volunteering opportunities`, { group }), + mentorship: t(`Contact {{group.name}} to learn about their practices`, { group }), + cooperative: t(`{{group.name}} is interested in forming a cooperative`, { group }), + buy: t(`Buy from {{group.name}}`, { group }), + markets: t(`{{group.name}} is seeking new markets`, { group }), + ecosystem_service_markets: t(`{{group.name}} is seeking ecosystem services`, { group }), + loans: t(`{{group.name}} is interested in low-cost loans`, { group }), + support: t(`{{group.name}} is seeking farm support`, { group }), + equipment_sharing: t(`{{group.name}} is interested in equipment sharing`, { group }), + increase_sales: t(`{{group.name}} is interested in increasing their sales`, { group }), + communication: t(`{{group.name}} seeks better communication / marketing to their buyers`, { group }), + new_products_methods: t(`{{group.name}} wants to learn about new methods or practices`, { group }), + carbon: t(`{{group.name}} is interested in carbon markets`, { group }), + environmental_impact: t(`{{group.name}} is curious about better understanding their farm through data (soil, environmental impacts, etc.)`, { group }), + benchmarking: t(`{{group.name}} wants to comparing my farm to others`, { group }), + insetting: t(`{{group.name}} wants to easily provide data to supply chain partners`, { group }), + reduce_risk: t(`{{group.name}} wants to reduce hazard risk (fire, flood, drought, etc.)`, { group }), + farm_valuation: t(`{{group.name}} wants to increase their farm valuation`, { group }), + certifications: t(`{{group.name}} is curious about further certification (organic, ISO, regenerative, etc.)`, { group }), + food_nutrition: t(`{{group.name}} is curious about understanding the nutrient density of their product`, { group }), + biodiversity: t(`{{group.name}} wants help focusing on biodiversity (protecting species, improving ecology, markets)`, { group }), + product_quality: t(`{{group.name}} wants to improve product quality`, { group }), + animal_welfare: t(`{{group.name}} wants to ensure animal welfare`, { group }) } } -// XXX: flag for internationalization/translation -const promptLookup = { - research: 'research projects', - events: 'event collaboration', - volunteering: 'volunteer opportunities', - mentorship: 'mentorship & advice', - cooperative: 'cooperatives', - buy: 'purchasing from you', - markets: 'new markets', - ecosystem_service_markets: 'ecosystem services', - loans: 'low-cost loans', - support: 'farm support', - equipment_sharing: 'equipment sharing', - increase_sales: 'increasing sales', - communication: 'helping with your communication & marketing', - new_products_methods: 'introducing new methods & practices', - carbon: 'carbon markets', - environmental_impact: 'environmental impact', - benchmarking: 'benchmarking your farm', - insetting: 'insetting', - reduce_risk: 'hazard risk mitigation', - farm_valuation: 'farm valuation', - certifications: 'certifications', - food_nutrition: 'nutrient density', - biodiversity: 'biodiversity', - product_quality: 'product quality', - animal_welfare: 'animal welfare' -} - const determineIcon = (opportunity) => { switch (opportunity) { case 'research': return 'Research' diff --git a/src/components/Widget/PrivacyWidget/PrivacyWidget.js b/src/components/Widget/PrivacyWidget/PrivacyWidget.js index 1109c3c00..c582e7516 100644 --- a/src/components/Widget/PrivacyWidget/PrivacyWidget.js +++ b/src/components/Widget/PrivacyWidget/PrivacyWidget.js @@ -1,11 +1,14 @@ import React from 'react' +import { useTranslation } from 'react-i18next' import './Privacy.scss' export default function PrivacyWidget (props) { + const { t } = useTranslation() + return (
    - group privacy settings + {t('group privacy settings')}
    ) } diff --git a/src/components/Widget/ProjectsWidget/ProjectsWidget.js b/src/components/Widget/ProjectsWidget/ProjectsWidget.js index 7c462e435..5fc5b1886 100644 --- a/src/components/Widget/ProjectsWidget/ProjectsWidget.js +++ b/src/components/Widget/ProjectsWidget/ProjectsWidget.js @@ -1,6 +1,7 @@ import moment from 'moment-timezone' import PropTypes from 'prop-types' import React, { Component } from 'react' +import { withTranslation } from 'react-i18next' import { Link } from 'react-router-dom' import { postUrl, createPostUrl } from 'util/navigation' import RoundImage from '../../RoundImage' @@ -9,7 +10,7 @@ import './ProjectsWidget.scss' const { array, bool, object } = PropTypes -export default class ProjectsWidget extends Component { +class ProjectsWidget extends Component { static propTypes = { isMember: bool, items: array, @@ -17,7 +18,7 @@ export default class ProjectsWidget extends Component { } render () { - const { isMember, items, routeParams } = this.props + const { isMember, items, routeParams, t } = this.props return (
    @@ -36,10 +37,10 @@ export default class ProjectsWidget extends Component {
    -
    What are you doing together?
    -
    Projects help you and your group accomplish shared goals.
    +
    {t('What are you doing together?')}
    +
    {t('Projects help you and your group accomplish shared goals.')}
    -
    + New project
    +
    {t('+ New project')}
    : '' } @@ -47,3 +48,4 @@ export default class ProjectsWidget extends Component { ) } } +export default withTranslation()(ProjectsWidget) diff --git a/src/components/Widget/RecentPostsWidget/RecentPostsWidget.js b/src/components/Widget/RecentPostsWidget/RecentPostsWidget.js index 47327ba11..091133a9c 100644 --- a/src/components/Widget/RecentPostsWidget/RecentPostsWidget.js +++ b/src/components/Widget/RecentPostsWidget/RecentPostsWidget.js @@ -1,4 +1,5 @@ import React, { useCallback, useState } from 'react' +import { useTranslation } from 'react-i18next' import Slider from 'react-slick' import { Link } from 'react-router-dom' import { filter, isEmpty } from 'lodash/fp' @@ -34,8 +35,9 @@ const settings = { ] } -export default ({ group, items, routeParams }) => { +const RecentPostsWidget = ({ group, items, routeParams }) => { const [swiped, setSwiped] = useState(false) + const { t } = useTranslation() const handleSwiped = useCallback(() => { setSwiped(true) @@ -59,13 +61,13 @@ export default ({ group, items, routeParams }) => {
    -
    NO MORE RECENT ACTIVITY
    -
    + New post
    +
    {t('NO MORE RECENT ACTIVITY')}
    +
    {t('+ New post')}
    - View all + {t('View all')}
    @@ -97,3 +99,4 @@ const RecentPostCard = ({ group, onClickCapture, post, routeParams }) => {
    ) } +export default RecentPostsWidget diff --git a/src/components/Widget/RichTextWidget/RichTextWidget.js b/src/components/Widget/RichTextWidget/RichTextWidget.js index f7a734455..9019de11c 100644 --- a/src/components/Widget/RichTextWidget/RichTextWidget.js +++ b/src/components/Widget/RichTextWidget/RichTextWidget.js @@ -1,14 +1,17 @@ import React from 'react' +import { useTranslation } from 'react-i18next' import GroupAboutVideoEmbed from 'components/GroupAboutVideoEmbed' import ClickCatcher from 'components/ClickCatcher' import HyloHTML from 'components/HyloHTML' import './RichText.scss' export default function RichTextWidget ({ group, settings }) { + const { t } = useTranslation() + return (
    -

    {settings.title || `Welcome to ${group.name}!`}

    +

    {settings.title || t('Welcome to {{group.name}}!', { group })}

    {settings.richText && ( @@ -19,7 +22,7 @@ export default function RichTextWidget ({ group, settings }) { )} {!settings.text && !settings.richText && (

    - We're happy you're here! Please take a moment to explore this page to see what's alive in our group. Introduce yourself by clicking Create to post a Discussion sharing who you are and what brings you to our group. And don't forget to fill out your profile - so likeminded new friends can connect with you! + {t("We're happy you're here! Please take a moment to explore this page to see what's alive in our group. Introduce yourself by clicking Create to post a Discussion sharing who you are and what brings you to our group. And don't forget to fill out your profile - so likeminded new friends can connect with you!")}

    )}
    diff --git a/src/components/Widget/TopicsWidget/TopicsWidget.js b/src/components/Widget/TopicsWidget/TopicsWidget.js index c38168ff8..43ebfe023 100644 --- a/src/components/Widget/TopicsWidget/TopicsWidget.js +++ b/src/components/Widget/TopicsWidget/TopicsWidget.js @@ -1,11 +1,14 @@ import React from 'react' +import { useTranslation } from 'react-i18next' import './Topics.scss' export default function TopicsWidget ({ group = {} }) { + const { t } = useTranslation() + return ( group.topics ?
    -
    Topics
    +
    {t('Topics')}
    {group.topics.slice(0, 10).map(topic => { return ( -

    {settings.title || `Welcome to ${group.name}!`}

    +

    {settings.title || this.props.t('Welcome to {{group.name}}!', { group })}

    @@ -25,3 +26,4 @@ export default class WelcomeWidget extends Component { ) } } +export default withTranslation()(WelcomeWidget) diff --git a/src/components/Widget/Widget.js b/src/components/Widget/Widget.js index 092819212..4b84bcc5d 100644 --- a/src/components/Widget/Widget.js +++ b/src/components/Widget/Widget.js @@ -1,4 +1,5 @@ import React, { useState } from 'react' +import { useTranslation } from 'react-i18next' import Icon from 'components/Icon' import AtAGlanceWidget from 'components/Widget/AtAGlanceWidget' import AnnouncementWidget from 'components/Widget/AnnouncementWidget' @@ -28,103 +29,113 @@ import getMe from 'store/selectors/getMe' import { updateWidget } from './Widget.store' import useRouter from 'hooks/useRouter' -const WIDGETS = { - text_block: { - title: '', - moderatorTitle: 'Welcome Message', - component: WelcomeWidget - }, - announcements: { - title: 'Announcements', - component: AnnouncementWidget - }, - active_members: { - title: 'Recently Active Members', - component: MembersWidget - }, - requests_offers: { - title: 'Open Requests & Offers', - component: OffersAndRequestsWidget - }, - posts: { - title: 'Recent Posts', - component: RecentPostsWidget - }, - community_topics: { - title: 'Community Topics', - component: GroupTopicsWidget - }, - events: { - title: 'Upcoming Events', - component: EventsWidget - }, - project_activity: { - title: 'Recently Active Projects', - component: ProjectsWidget - }, - group_affiliations: { - title: 'Child Groups', - component: GroupsWidget - }, - relevant_project_activity: { - title: 'Recently Active Projects', - component: ProjectsWidget - }, - relevant_groups: { - title: 'Nearby Relevant Groups', // TODO: ensure there is a way to customize/overwrite this - component: GroupsWidget - }, - relevant_events: { - title: 'Nearby Relevant Events', // TODO: ensure there is a way to customize/overwrite this - component: EventsWidget - }, - relevant_requests_offers: { - title: 'Nearby Relevant Offers and Requests', // TODO: ensure there is a way to customize/overwrite this - component: OffersAndRequestsWidget - }, - farm_at_a_glance: { - title: 'At A Glance', - component: AtAGlanceWidget - }, - farm_details: { - title: 'Farm Details', - component: FarmDetailsWidget - }, - farm_open_to_public: { - title: 'Location & Hours', - component: FarmOpenToPublic - }, - farm_map: { - title: 'Farm Surrounds & Posts', - component: FarmMapWidget - }, - moderators: { - title: 'Moderators', // TODO: ensure there is a way to customize/overwrite this - component: ModeratorsWidget - }, - opportunities_to_collaborate: { - title: 'Opportunities to Collaborate', - component: OpportunitiesToCollaborateWidget - }, - privacy_settings: { - title: 'Privacy', - component: PrivacyWidget - }, - mission: { - title: null, - component: RichTextWidget - }, - join: { - title: null, - component: JoinWidget - }, - topics: { - title: null, - component: TopicsWidget +export default function Widget (props) { + const { t } = useTranslation() + const WIDGETS = { + text_block: { + title: '', + moderatorTitle: t('Welcome Message'), + component: WelcomeWidget + }, + announcements: { + title: t('Announcements'), + component: AnnouncementWidget + }, + active_members: { + title: t('Recently Active Members'), + component: MembersWidget + }, + requests_offers: { + title: t('Open Requests & Offers'), + component: OffersAndRequestsWidget + }, + posts: { + title: t('Recent Posts'), + component: RecentPostsWidget + }, + community_topics: { + title: t('Community Topics'), + component: GroupTopicsWidget + }, + events: { + title: t('Upcoming Events'), + component: EventsWidget + }, + project_activity: { + title: t('Recently Active Projects'), + component: ProjectsWidget + }, + group_affiliations: { + title: t('Subgroups'), + component: GroupsWidget + }, + relevant_project_activity: { + title: t('Recently Active Projects'), + component: ProjectsWidget + }, + relevant_groups: { + title: t('Nearby Relevant Groups'), // TODO: ensure there is a way to customize/overwrite this + component: GroupsWidget + }, + relevant_events: { + title: t('Nearby Relevant Events'), // TODO: ensure there is a way to customize/overwrite this + component: EventsWidget + }, + relevant_requests_offers: { + title: t('Nearby Relevant Offers and Requests'), // TODO: ensure there is a way to customize/overwrite this + component: OffersAndRequestsWidget + }, + farm_at_a_glance: { + title: t('At A Glance'), + component: AtAGlanceWidget + }, + farm_details: { + title: t('Farm Details'), + component: FarmDetailsWidget + }, + farm_open_to_public: { + title: t('Location & Hours'), + component: FarmOpenToPublic + }, + farm_map: { + title: t('Farm Surrounds & Posts'), + component: FarmMapWidget + }, + moderators: { + title: t('Moderators'), // TODO: ensure there is a way to customize/overwrite this + component: ModeratorsWidget + }, + opportunities_to_collaborate: { + title: t('Opportunities to Collaborate'), + component: OpportunitiesToCollaborateWidget + }, + privacy_settings: { + title: t('Privacy'), + component: PrivacyWidget + }, + mission: { + title: null, + component: RichTextWidget + }, + join: { + title: null, + component: JoinWidget + }, + topics: { + title: null, + component: TopicsWidget + } + } + const HiddenWidget = ({ name }) => { + const { t } = useTranslation() + return ( +
    +

    {t('Hidden')}

    +

    {t('The {{title}} section is not visible to members of this group. Click the three dots', { title: WIDGETS[name].moderatorTitle || WIDGETS[name].title })} () {t('above this box to change the visibility settings. Only moderators can see this message')}.

    +
    + ) } -} -export default function Widget (props) { const dispatch = useDispatch() const { childGroups, id, isModerator, isVisible, name, posts, isMember, settings = {} } = props const router = useRouter() @@ -154,14 +165,14 @@ export default function Widget (props) {
    {!isEditingSettings &&
      - {name === 'text_block' &&
    setIsEditingSettings(!isEditingSettings)}> Edit welcome message
    } + {name === 'text_block' &&
    setIsEditingSettings(!isEditingSettings)}> {t('Edit welcome message')}
    }
    handleUpdateWidget(id, { isVisible: !isVisible })} styleName='widget-visibility' - backgroundColor={isVisible ? 'gray' : 'black'} /> Visibility: {isVisible ? 'Visible' : 'Hidden'} + backgroundColor={isVisible ? 'gray' : 'black'} /> {t('Visibility')}: {isVisible ? t('Visible') : t('Hidden')}
    }
    @@ -185,16 +196,8 @@ export default function Widget (props) { ) } -const HiddenWidget = ({ isVisible, name }) => { - return ( -
    -

    Hidden

    -

    The {WIDGETS[name].moderatorTitle || WIDGETS[name].title} section is not visible to members of this group. Click the three dots () above this box to change the visibility settings. Only moderators can see this message.

    -
    - ) -} - const EditForm = ({ id, setIsEditingSettings, setIsMenuOpen, newSettings, updateSettings, save }) => { + const { t } = useTranslation() return (
    @@ -202,7 +205,7 @@ const EditForm = ({ id, setIsEditingSettings, setIsMenuOpen, newSettings, update type='text' autoFocus onChange={e => updateSettings({ ...newSettings, title: e.target.value.substring(0, 50) })} - placeholder='Enter a title' + placeholder={t('Enter a title')} value={newSettings.title} />
    {(newSettings.title && newSettings.title.length) || 0}/{50}
    @@ -212,7 +215,7 @@ const EditForm = ({ id, setIsEditingSettings, setIsMenuOpen, newSettings, update