diff --git a/.github/labeler.yml b/.github/labeler.yml index fa78979ab7..1e33a32913 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,7 +1,7 @@ "Component: Frontend": - frontend/**/* "Component: Backend": - - server/**/* + - backend/**/* - tests/**/* - migrations/**/* - ./manage.py @@ -12,3 +12,4 @@ - scripts/aws/**/* - scripts/docker/**/* - scripts/install/**/* + - docker-*.yml diff --git a/backend/models/dtos/organisation_dto.py b/backend/models/dtos/organisation_dto.py index 22d6a1781e..46f4658f84 100644 --- a/backend/models/dtos/organisation_dto.py +++ b/backend/models/dtos/organisation_dto.py @@ -43,6 +43,10 @@ class OrganisationDTO(Model): class ListOrganisationsDTO(Model): + def __init__(self): + super().__init__() + self.organisations = [] + organisations = ListType(ModelType(OrganisationDTO)) @@ -58,12 +62,10 @@ class NewOrganisationDTO(Model): class UpdateOrganisationDTO(OrganisationDTO): + + organisation_id = IntType(serialized_name="organisationId", required=False) managers = ListType(StringType()) name = StringType() - - -class OrganisationProjectsDTO(Model): - """ Describes a JSON model to create a project team """ - - project_name = StringType(serialize_when_none=False) - project_id = IntType(serialize_when_none=False) + logo = StringType() + description = StringType() + url = StringType() diff --git a/backend/models/dtos/team_dto.py b/backend/models/dtos/team_dto.py index c2dab1b5b7..8756268a75 100644 --- a/backend/models/dtos/team_dto.py +++ b/backend/models/dtos/team_dto.py @@ -9,7 +9,6 @@ ModelType, ) from backend.models.postgis.statuses import TeamMemberFunctions, TeamVisibility -from backend.models.dtos.organisation_dto import OrganisationProjectsDTO def validate_team_visibility(value): @@ -67,7 +66,6 @@ def __init__(self): super().__init__() self.members = [] self.team_projects = [] - self.organisation_projects = [] """ Describes JSON model for a team """ team_id = IntType(serialized_name="teamId") @@ -86,7 +84,6 @@ def __init__(self): is_general_admin = BooleanType(default=False) members = ListType(ModelType(TeamMembersDTO)) team_projects = ListType(ModelType(ProjectTeamDTO)) - organisation_projects = ListType(ModelType(OrganisationProjectsDTO)) class TeamDTO(Model): diff --git a/backend/models/postgis/project_chat.py b/backend/models/postgis/project_chat.py index 8e661219bb..fb0f303589 100644 --- a/backend/models/postgis/project_chat.py +++ b/backend/models/postgis/project_chat.py @@ -1,4 +1,5 @@ import bleach +from markdown import markdown from flask import current_app from backend import db from backend.models.postgis.user import User @@ -30,7 +31,31 @@ def create_from_dto(cls, dto: ChatMessageDTO): new_message.user_id = dto.user_id # Use bleach to remove any potential mischief - clean_message = bleach.clean(dto.message) + allowed_tags = [ + "a", + "b", + "blockquote", + "br", + "code", + "em", + "h1", + "h2", + "h3", + "img", + "i", + "li", + "ol", + "p", + "pre", + "strong", + "ul", + ] + allowed_atrributes = {"a": ["href", "rel"], "img": ["src", "alt"]} + clean_message = bleach.clean( + markdown(dto.message, output_format="html"), + tags=allowed_tags, + attributes=allowed_atrributes, + ) clean_message = bleach.linkify(clean_message) new_message.message = clean_message diff --git a/backend/models/postgis/team.py b/backend/models/postgis/team.py index 0e1e441d67..98c0966784 100644 --- a/backend/models/postgis/team.py +++ b/backend/models/postgis/team.py @@ -1,8 +1,17 @@ from backend import db -from backend.models.dtos.team_dto import TeamDTO, NewTeamDTO +from backend.models.dtos.team_dto import ( + TeamDTO, + NewTeamDTO, + TeamMembersDTO, + TeamProjectDTO, +) from backend.models.dtos.organisation_dto import OrganisationTeamsDTO from backend.models.postgis.organisation import Organisation -from backend.models.postgis.statuses import TeamVisibility, TeamMemberFunctions +from backend.models.postgis.statuses import ( + TeamVisibility, + TeamMemberFunctions, + TeamRoles, +) from backend.models.postgis.user import User from backend.models.postgis.utils import NotFound @@ -148,7 +157,7 @@ def get(team_id: int): def get_team_by_name(team_name: str): """ Gets specified team by name - :param team_id: team name in scope + :param team_name: team name in scope :return: Team if found otherwise None """ return Team.query.filter_by(name=team_name).one_or_none() @@ -178,6 +187,25 @@ def as_dto_inside_org(self): team_dto.visibility = TeamVisibility(self.visibility).name return team_dto + def as_dto_team_member(self, member) -> TeamMembersDTO: + """ Returns a dto for the team member""" + member_dto = TeamMembersDTO() + user = User.get_by_id(member.user_id) + member_function = TeamMemberFunctions(member.function).name + member_dto.username = user.username + member_dto.function = member_function + member_dto.picture_url = user.picture_url + member_dto.active = member.active + return member_dto + + def as_dto_team_project(self, project) -> TeamProjectDTO: + """ Returns a dto for the team project """ + project_team_dto = TeamProjectDTO() + project_team_dto.project_name = project.name + project_team_dto.project_id = project.project_id + project_team_dto.role = TeamRoles(project.role).name + return project_team_dto + def _get_team_members(self): """ Helper to get JSON serialized members """ members = [] diff --git a/backend/services/messaging/message_service.py b/backend/services/messaging/message_service.py index 024a29efdd..02cd8eebc6 100644 --- a/backend/services/messaging/message_service.py +++ b/backend/services/messaging/message_service.py @@ -412,7 +412,8 @@ def send_message_after_chat(chat_from: int, chat: str, project_id: int): message.message = chat messages.append(dict(message=message, user=user)) - MessageService._push_messages(messages) + # it's important to keep that line inside the if to avoid duplicated emails + MessageService._push_messages(messages) @staticmethod def send_favorite_project_activities(user_id: int): diff --git a/backend/services/messaging/smtp_service.py b/backend/services/messaging/smtp_service.py index 1fd6fa5648..0feff39ae0 100644 --- a/backend/services/messaging/smtp_service.py +++ b/backend/services/messaging/smtp_service.py @@ -7,6 +7,7 @@ from backend.services.messaging.template_service import ( get_template, template_var_replacing, + format_username_link, ) @@ -76,9 +77,12 @@ def send_email_alert( ["[ORG_CODE]", org_code], ["[PROFILE_LINK]", inbox_url], ["[SETTINGS_LINK]", settings_url], - ["[CONTENT]", content], ] - html_template = template_var_replacing(html_template, replace_list) + html_replace_list = replace_list + [ + ["[CONTENT]", format_username_link(content)] + ] + html_template = template_var_replacing(html_template, html_replace_list) + replace_list += [["[CONTENT]", content]] text_template = template_var_replacing(text_template, replace_list) SMTPService._send_message(to_address, subject, html_template, text_template) @@ -96,7 +100,9 @@ def _send_message( msg = MIMEMultipart("alternative") msg["Subject"] = subject - msg["From"] = from_address + msg["From"] = "{} Tasking Manager <{}>".format( + current_app.config["ORG_CODE"], from_address + ) msg["To"] = to_address # Record the MIME types of both parts - text/plain and text/html. diff --git a/backend/services/messaging/template_service.py b/backend/services/messaging/template_service.py index 9a214eb629..635a370287 100644 --- a/backend/services/messaging/template_service.py +++ b/backend/services/messaging/template_service.py @@ -1,5 +1,6 @@ import os import re + from flask import current_app @@ -31,3 +32,15 @@ def clean_html(raw_html): cleanr = re.compile("<.*?>") clean_text = re.sub(cleanr, "", raw_html) return clean_text + + +def format_username_link(content): + expression = re.compile("@\\[.*?\\]") + names = expression.findall(content) + for name in names: + username = name[2:-1] + content = content.replace( + name, + f'@{username}', + ) + return content diff --git a/backend/services/organisation_service.py b/backend/services/organisation_service.py index b3ff6af439..826ddeb053 100644 --- a/backend/services/organisation_service.py +++ b/backend/services/organisation_service.py @@ -10,7 +10,6 @@ ) from backend.models.postgis.organisation import Organisation from backend.models.postgis.project import Project, ProjectInfo -from backend.models.postgis.team import Team from backend.models.postgis.utils import NotFound from backend.services.users.user_service import UserService @@ -43,7 +42,6 @@ def get_organisation_by_id_as_dto( raise NotFound() organisation_dto = org.as_dto(abbreviated) - organisation_dto.teams = [] if user_id != 0: organisation_dto.is_manager = OrganisationService.can_user_manage_organisation( @@ -55,9 +53,7 @@ def get_organisation_by_id_as_dto( if abbreviated: return organisation_dto - teams = OrganisationService.get_teams_by_organisation_id(organisation_id) - for team in teams: - organisation_dto.teams.append(team.as_dto_inside_org()) + organisation_dto.teams = [team.as_dto_inside_org() for team in org.teams] return organisation_dto @@ -130,7 +126,6 @@ def get_organisations_as_dto( ): orgs = OrganisationService.get_organisations(manager_user_id) orgs_dto = ListOrganisationsDTO() - orgs_dto.organisations = [] for org in orgs: org_dto = org.as_dto(omit_managers) if not authenticated_user_id: @@ -151,10 +146,7 @@ def get_organisations_managed_by_user(user_id: int): def get_organisations_managed_by_user_as_dto(user_id: int): orgs = OrganisationService.get_organisations_managed_by_user(user_id) orgs_dto = ListOrganisationsDTO() - orgs_dto.organisations = [] - for org in orgs: - orgs_dto.organisations.append(org.as_dto()) - + orgs_dto.organisations = [org.as_dto() for org in orgs] return orgs_dto @staticmethod @@ -171,15 +163,6 @@ def get_projects_by_organisation_id(organisation_id: int) -> Organisation: return projects - @staticmethod - def get_teams_by_organisation_id(organisation_id: int) -> Organisation: - teams = Team.query.filter(Team.organisation_id == organisation_id).all() - - if teams is None: - raise NotFound() - - return teams - @staticmethod def assert_validate_name(org: Organisation, name: str): """ Validates that the organisation name doesn't exist """ diff --git a/backend/services/project_search_service.py b/backend/services/project_search_service.py index eb23870de9..9d74f1d4a9 100644 --- a/backend/services/project_search_service.py +++ b/backend/services/project_search_service.py @@ -209,14 +209,18 @@ def search_projects(search_dto: ProjectSearchDTO, user) -> ProjectSearchResultsD @staticmethod def _filter_projects(search_dto: ProjectSearchDTO, user): """ Filters all projects based on criteria provided by user""" + query = ProjectSearchService.create_search_query(user) + query = query.join(ProjectInfo).filter( ProjectInfo.locale.in_([search_dto.preferred_locale, "en"]) ) project_status_array = [] if search_dto.project_statuses: - for project_status in search_dto.project_statuses: - project_status_array.append(ProjectStatus[project_status].value) + project_status_array = [ + ProjectStatus[project_status].value + for project_status in search_dto.project_statuses + ] query = query.filter(Project.status.in_(project_status_array)) else: if not search_dto.created_by: @@ -232,7 +236,6 @@ def _filter_projects(search_dto: ProjectSearchDTO, user): projects_mapped = UserService.get_projects_mapped(search_dto.mapped_by) query = query.filter(Project.id.in_(projects_mapped)) if search_dto.favorited_by: - user = UserService.get_user_by_id(search_dto.favorited_by) projects_favorited = user.favorites query = query.filter( Project.id.in_([project.id for project in projects_favorited]) @@ -254,16 +257,16 @@ def _filter_projects(search_dto: ProjectSearchDTO, user): ).filter(ProjectTeams.team_id == search_dto.team_id) if search_dto.campaign: - query = query.join(Campaign, Project.campaign).group_by( - Project.id, Campaign.name - ) + query = query.join(Campaign, Project.campaign).group_by(Campaign.name) query = query.filter(Campaign.name == search_dto.campaign) if search_dto.mapping_types: # Construct array of mapping types for query mapping_type_array = [] - for mapping_type in search_dto.mapping_types: - mapping_type_array.append(MappingTypes[mapping_type].value) + mapping_type_array = [ + MappingTypes[mapping_type].value + for mapping_type in search_dto.mapping_types + ] query = query.filter(Project.mapping_types.contains(mapping_type_array)) @@ -276,7 +279,7 @@ def _filter_projects(search_dto: ProjectSearchDTO, user): ProjectInfo.text_searchable.match( or_search, postgresql_regconfig="english" ), - ProjectInfo.name.like(f"%{or_search}%"), + ProjectInfo.name.ilike(f"%{or_search}%"), ] try: opts.append(Project.id == int(search_dto.text_search)) @@ -325,7 +328,13 @@ def _filter_projects(search_dto: ProjectSearchDTO, user): all_results = [] if not search_dto.omit_map_results: - all_results = query.all() + query_result = query + query_result.column_descriptions.clear() + query_result.add_column(Project.id) + query_result.add_column(Project.centroid.ST_AsGeoJSON().label("centroid")) + query_result.add_column(Project.priority) + all_results = query_result.all() + paginated_results = query.paginate(search_dto.page, 14, True) return all_results, paginated_results diff --git a/backend/services/project_service.py b/backend/services/project_service.py index 4d3b8d7a78..cd7f8ea37f 100644 --- a/backend/services/project_service.py +++ b/backend/services/project_service.py @@ -108,7 +108,7 @@ def get_contribs_by_day(project_id: int) -> ProjectContribsDTO: ) .group_by("action_text", "day", "task_id") .order_by("day") - ) + ).all() contribs_dto = ProjectContribsDTO() # Filter and store unique dates diff --git a/backend/services/team_service.py b/backend/services/team_service.py index 1fe9f31bd6..37cd50901a 100644 --- a/backend/services/team_service.py +++ b/backend/services/team_service.py @@ -6,12 +6,9 @@ TeamDTO, NewTeamDTO, TeamsListDTO, - TeamMembersDTO, ProjectTeamDTO, - TeamProjectDTO, TeamDetailsDTO, ) -from backend.models.dtos.organisation_dto import OrganisationProjectsDTO from backend.models.postgis.team import Team, TeamMembers from backend.models.postgis.project import ProjectTeams from backend.models.postgis.project_info import ProjectInfo @@ -188,7 +185,7 @@ def get_all_teams( omit_members: bool = False, ) -> TeamsListDTO: - query = db.session.query(Team).outerjoin(TeamMembers).outerjoin(ProjectTeams) + query = db.session.query(Team) orgs_query = None is_admin = UserService.is_user_an_admin(user_id) @@ -218,21 +215,35 @@ def get_all_teams( if team_role_filter: try: role = TeamRoles[team_role_filter.upper()].value - query = query.filter(ProjectTeams.role == role) + project_teams = ( + db.session.query(ProjectTeams) + .filter(ProjectTeams.role == role) + .subquery() + ) + query = query.outerjoin(project_teams) except KeyError: pass if member_filter: - query = query.filter( - TeamMembers.user_id == member_filter, TeamMembers.active == True # noqa + team_member = ( + db.session.query(TeamMembers) + .filter( + TeamMembers.user_id == member_filter, TeamMembers.active.is_(True) + ) + .subquery() ) + query = query.outerjoin(team_member) if member_request_filter: - query = query.filter( - TeamMembers.user_id == member_request_filter, - TeamMembers.active == False, # noqa + team_member = ( + db.session.query(TeamMembers) + .filter( + TeamMembers.user_id == member_request_filter, + TeamMembers.active.is_(False), + ) + .subquery() ) - + query = query.outerjoin(team_member) if orgs_query: query = query.union(orgs_query) @@ -252,16 +263,11 @@ def get_all_teams( is_team_member = TeamService.is_user_an_active_team_member(team.id, user_id) # Skip if members are not included if not omit_members: - team_members = TeamService._get_team_members(team.id) - for member in team_members: - user = UserService.get_user_by_id(member.user_id) - member_function = TeamMemberFunctions(member.function).name - member_dto = TeamMembersDTO() - member_dto.username = user.username - member_dto.function = member_function - member_dto.picture_url = user.picture_url - member_dto.active = member.active - team_dto.members.append(member_dto) + team_members = team.members + + team_dto.members = [ + team.as_dto_team_member(member) for member in team_members + ] if team_dto.visibility == "PRIVATE" and not is_admin: if is_team_member: @@ -271,7 +277,9 @@ def get_all_teams( return teams_list_dto @staticmethod - def get_team_as_dto(team_id: int, user_id: int, abbreviated: bool) -> TeamDTO: + def get_team_as_dto( + team_id: int, user_id: int, abbreviated: bool + ) -> TeamDetailsDTO: team = TeamService.get_team_by_id(team_id) if team is None: @@ -302,40 +310,18 @@ def get_team_as_dto(team_id: int, user_id: int, abbreviated: bool) -> TeamDTO: if abbreviated: return team_dto - team_members = TeamService._get_team_members(team_id) - for member in team_members: - user = UserService.get_user_by_id(member.user_id) - member_dto = TeamMembersDTO() - member_dto.username = user.username - member_dto.pictureUrl = user.picture_url - member_dto.function = TeamMemberFunctions(member.function).name - member_dto.picture_url = user.picture_url - member_dto.active = member.active - - team_dto.members.append(member_dto) + team_dto.members = [team.as_dto_team_member(member) for member in team.members] team_projects = TeamService.get_projects_by_team_id(team.id) - for team_project in team_projects: - project_team_dto = TeamProjectDTO() - project_team_dto.project_name = team_project.name - project_team_dto.project_id = team_project.project_id - project_team_dto.role = TeamRoles(team_project.role).name - - team_dto.team_projects.append(project_team_dto) - org_projects = OrganisationService.get_projects_by_organisation_id( - team.organisation.id - ) - for org_project in org_projects: - org_project_dto = OrganisationProjectsDTO() - org_project_dto.project_id = org_project.id - org_project_dto.project_name = org_project.name - team_dto.organisation_projects.append(org_project_dto) + team_dto.team_projects = [ + team.as_dto_team_project(project) for project in team_projects + ] return team_dto @staticmethod - def get_projects_by_team_id(team_id: int) -> ProjectInfo: + def get_projects_by_team_id(team_id: int): projects = ( db.session.query( ProjectInfo.name, ProjectTeams.project_id, ProjectTeams.role @@ -405,7 +391,7 @@ def get_team_by_name(team_name: str) -> Team: def create_team(new_team_dto: NewTeamDTO) -> int: """ Creates a new team using a team dto - :param team_dto: Team DTO + :param new_team_dto: Team DTO :returns: ID of new Team """ TeamService.assert_validate_organisation(new_team_dto.organisation_id) diff --git a/backend/services/users/user_service.py b/backend/services/users/user_service.py index 9b366caae7..b75089d642 100644 --- a/backend/services/users/user_service.py +++ b/backend/services/users/user_service.py @@ -2,7 +2,7 @@ from flask import current_app import datetime from sqlalchemy.sql.expression import literal -from sqlalchemy import text, func, or_, desc, and_, distinct, cast, Time +from sqlalchemy import func, or_, desc, and_, distinct, cast, Time from backend import db from backend.models.dtos.project_dto import ProjectFavoritesDTO, ProjectSearchResultsDTO from backend.models.dtos.user_dto import ( @@ -204,7 +204,7 @@ def get_interests_stats(user_id): "count_projects" ), ) - .outerjoin( + .join( project_interests, and_( Interest.id == project_interests.c.interest_id, @@ -395,23 +395,31 @@ def get_detailed_stats(username: str): ) total_validation_time = db.session.query( func.sum(cast(func.to_timestamp(query.c.tm, "HH24:MI:SS"), Time)) - ).all() - - for time in total_validation_time: - total_validation_time = time[0] - if total_validation_time: - stats_dto.time_spent_validating = total_validation_time.total_seconds() - stats_dto.total_time_spent += stats_dto.time_spent_validating - - sql = """SELECT SUM(TO_TIMESTAMP(action_text, 'HH24:MI:SS')::TIME) FROM task_history - WHERE (action='LOCKED_FOR_MAPPING' or action='AUTO_UNLOCKED_FOR_MAPPING') - and user_id = :user_id;""" - total_mapping_time = db.engine.execute(text(sql), user_id=user.id) - for time in total_mapping_time: - total_mapping_time = time[0] - if total_mapping_time: - stats_dto.time_spent_mapping = total_mapping_time.total_seconds() - stats_dto.total_time_spent += stats_dto.time_spent_mapping + ).scalar() + + if total_validation_time: + stats_dto.time_spent_validating = total_validation_time.total_seconds() + stats_dto.total_time_spent += stats_dto.time_spent_validating + + total_mapping_time = ( + db.session.query( + func.sum( + cast(func.to_timestamp(TaskHistory.action_text, "HH24:MI:SS"), Time) + ) + ) + .filter( + or_( + TaskHistory.action == TaskAction.LOCKED_FOR_MAPPING.name, + TaskHistory.action == TaskAction.AUTO_UNLOCKED_FOR_MAPPING.name, + ) + ) + .filter(TaskHistory.user_id == user.id) + .scalar() + ) + + if total_mapping_time: + stats_dto.time_spent_mapping = total_mapping_time.total_seconds() + stats_dto.total_time_spent += stats_dto.time_spent_mapping stats_dto.contributions_interest = UserService.get_interests_stats(user.id) @@ -731,7 +739,6 @@ def check_and_update_mapper_level(user_id: int): return user.save() - return user @staticmethod def notify_level_upgrade(user_id: int, username: str, level: str): @@ -784,7 +791,6 @@ def register_user_with_email(user_dto: UserRegisterEmailDTO): @staticmethod def get_interests(user: User) -> InterestsListDTO: dto = InterestsListDTO() - dto.interests = [] for interest in Interest.query.all(): int_dto = interest.as_dto() if interest in user.interests: diff --git a/example.env b/example.env index 7902961e70..915ebc8f60 100644 --- a/example.env +++ b/example.env @@ -45,7 +45,8 @@ TM_ORG_GITHUB=https://github.com/example/ # It is more complex to use for TM if your frontend and backend are on same server. #TM_ENABLE_SERVICEWORKER=0 -# Define an API URL and KEY of a image upload service. It will be used to store the Organisation logos. +# Define an API URL and KEY of an image upload service. +# It will be used to store the Organisation logos and the images uploaded on comments input fields. # HOT uses this service: https://github.com/hotosm/cdn-upload-api/ to setup an image upload API #TM_IMAGE_UPLOAD_API_URL= #TM_IMAGE_UPLOAD_API_KEY= diff --git a/frontend/package.json b/frontend/package.json index 2c4d39634e..f527f9dba0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -44,14 +44,15 @@ "react-chartjs-2": "^2.10.0", "react-click-outside": "^3.0.1", "react-dom": "^16.13.1", + "react-dropzone": "^11.0.2", "react-final-form": "^6.5.1", - "react-intl": "^5.4.5", + "react-intl": "^5.4.7", "react-meta-elements": "^1.0.0", "react-placeholder": "^4.0.3", "react-redux": "^7.2.1", "react-scripts": "^3.4.1", "react-select": "^3.1.0", - "react-tooltip": "^4.2.7", + "react-tooltip": "^4.2.8", "reactjs-popup": "^1.5.0", "redux": "^4.0.5", "redux-thunk": "^2.3.0", @@ -90,11 +91,11 @@ }, "devDependencies": { "@testing-library/jest-dom": "^5.11.2", - "@testing-library/react": "^10.4.7", + "@testing-library/react": "^10.4.8", "@testing-library/react-hooks": "^3.4.1", "combine-react-intl-messages": "^4.0.0", "jest-canvas-mock": "^2.2.0", - "msw": "^0.20.1", + "msw": "^0.20.4", "prettier": "^2.0.5", "react-test-renderer": "^16.13.1" }, diff --git a/frontend/src/components/comments/commentInput.js b/frontend/src/components/comments/commentInput.js new file mode 100644 index 0000000000..9459268f7c --- /dev/null +++ b/frontend/src/components/comments/commentInput.js @@ -0,0 +1,106 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import ReactTextareaAutocomplete from '@webscopeio/react-textarea-autocomplete'; +import { useDropzone } from 'react-dropzone'; +import { FormattedMessage } from 'react-intl'; + +import messages from './messages'; +import { useOnDrop } from '../../hooks/UseUploadImage'; +import { fetchLocalJSONAPI } from '../../network/genericJSONRequest'; +import { HashtagPaste } from './hashtagPaste'; + +export const CommentInputField = ({ comment, setComment }: Object) => { + const token = useSelector((state) => state.auth.get('token')); + const appendImgToComment = (url) => setComment(`${comment}\n![](${url})\n`); + const [uploadError, uploading, onDrop] = useOnDrop(appendImgToComment); + const { fileRejections, getRootProps, getInputProps } = useDropzone({ + onDrop, + accept: 'image/*', + multiple: false, + maxSize: 256000, + }); + const fileRejectionItems = fileRejections.map(({ file, errors }) => ( +
  • + {file.path} ( + {errors.map((e) => ( + + {e.message}, + + ))} + ) +
  • + )); + + return ( +
    + setComment(e.target.value)} + token={token} + /> + {comment && ( + + + , + + + )} + {uploadError && ( + + + + )} + {uploading && ( + + + + )} + +
    + ); +}; + +export const UserFetchTextarea = ({ value, setValueFn, token }) => { + const fetchUsers = async (user) => { + const url = `users/queries/filter/${user}/`; + let userItems; + + try { + const res = await fetchLocalJSONAPI(url, token); + userItems = res.usernames.map((u) => { + return { name: u }; + }); + } catch (e) { + userItems = []; + } + + return userItems; + }; + + return ( + } + rows={3} + trigger={{ + '@': { + dataProvider: fetchUsers, + component: Item, + output: (item, trigger) => '@[' + item.name + ']', + }, + }} + /> + ); +}; + +const Item = ({ entity: { name } }) => ( +
    + {`${name}`} +
    +); diff --git a/frontend/src/components/projectDetail/hashtagPaste.js b/frontend/src/components/comments/hashtagPaste.js similarity index 100% rename from frontend/src/components/projectDetail/hashtagPaste.js rename to frontend/src/components/comments/hashtagPaste.js diff --git a/frontend/src/components/comments/messages.js b/frontend/src/components/comments/messages.js new file mode 100644 index 0000000000..9def559c86 --- /dev/null +++ b/frontend/src/components/comments/messages.js @@ -0,0 +1,23 @@ +import { defineMessages } from 'react-intl'; + +/** + * Internationalized messages for use on comment input field. + */ +export default defineMessages({ + imageUploadFailed: { + id: 'comment.input.imageUpload.error', + defaultMessage: 'The image upload failed.', + }, + imageUploadOnProgress: { + id: 'comment.input.imageUpload.progress', + defaultMessage: 'Uploading file...', + }, + managersHashtagTip: { + id: 'comment.hashtags.help.managers', + defaultMessage: 'Add "{hashtag}" to notify the project managers about your comment.', + }, + authorHashtagTip: { + id: 'comment.hashtags.help.author', + defaultMessage: 'Add "{hashtag}" to notify the project author about your comment.', + }, +}); diff --git a/frontend/src/components/projectDetail/tests/hashtagPaste.test.js b/frontend/src/components/comments/tests/hashtagPaste.test.js similarity index 100% rename from frontend/src/components/projectDetail/tests/hashtagPaste.test.js rename to frontend/src/components/comments/tests/hashtagPaste.test.js diff --git a/frontend/src/components/notifications/notificationBodyCard.js b/frontend/src/components/notifications/notificationBodyCard.js index f1d6772875..6e0ed36541 100644 --- a/frontend/src/components/notifications/notificationBodyCard.js +++ b/frontend/src/components/notifications/notificationBodyCard.js @@ -1,4 +1,5 @@ import React from 'react'; +import { useSelector } from 'react-redux'; import { Link, useLocation, navigate } from '@reach/router'; import ReactPlaceholder from 'react-placeholder'; import 'react-placeholder/lib/reactPlaceholder.css'; @@ -6,14 +7,10 @@ import { selectUnit } from '@formatjs/intl-utils'; import { FormattedRelativeTime, FormattedMessage } from 'react-intl'; import messages from './messages'; -import { - MessageAvatar, - typesThatUseSystemAvatar, - rawHtmlNotification, - stripHtmlToText, -} from './notificationCard'; -import { CloseIcon } from '../../components/svgIcons'; -import { DeleteModal } from '../deleteModal'; +import { MessageAvatar, typesThatUseSystemAvatar, rawHtmlNotification } from './notificationCard'; +import { CloseIcon } from '../svgIcons'; +import { fetchLocalJSONAPI } from '../../network/genericJSONRequest'; +import { DeleteButton } from '../teamsAndOrgs/management'; export const NotificationBodyModal = (props) => { const location = useLocation(); @@ -78,6 +75,8 @@ export function NotificationBodyCard({ loading, card: { messageId, name, messageType, fromUsername, subject, message, sentDate }, }: Object) { + const token = useSelector((state) => state.auth.get('token')); + const location = useLocation(); const { value, unit } = selectUnit(new Date((sentDate && new Date(sentDate)) || new Date())); const showASendingUser = fromUsername || (typesThatUseSystemAvatar.indexOf(messageType) !== -1 && 'System'); @@ -92,6 +91,13 @@ export function NotificationBodyCard({ if (message !== undefined) { replacedMessage = message.replace('task=', 'search='); } + const deleteNotification = (id) => { + fetchLocalJSONAPI(`notifications/${id}/`, token, 'DELETE') + .then((success) => navigate(`../../${location.search}`)) + .catch((e) => { + console.log(e.message); + }); + }; return ( @@ -116,11 +122,9 @@ export function NotificationBodyCard({ dangerouslySetInnerHTML={rawHtmlNotification(replacedMessage)} /> - deleteNotification(messageId)} /> diff --git a/frontend/src/components/notifications/notificationCard.js b/frontend/src/components/notifications/notificationCard.js index a4bf83283e..472212b933 100644 --- a/frontend/src/components/notifications/notificationCard.js +++ b/frontend/src/components/notifications/notificationCard.js @@ -9,7 +9,7 @@ import messages from './messages'; import systemAvatar from '../../assets/img/hot-system-avatar-square-opaque.png'; import { EyeIcon } from '../svgIcons'; import { UserAvatar } from '../user/avatar'; -import { DeleteModal } from '../deleteModal'; +import { DeleteButton } from '../teamsAndOrgs/management'; import { RelativeTimeWithUnit } from '../../utils/formattedRelativeTime'; import { fetchLocalJSONAPI } from '../../network/genericJSONRequest'; import { navigate, useLocation } from '@reach/router'; @@ -69,6 +69,13 @@ export function NotificationCard({ const setMessageAsRead = (messageId) => { fetchLocalJSONAPI(`notifications/${messageId}/`, token).then(() => retryFn()); }; + const deleteNotification = (id) => { + fetchLocalJSONAPI(`notifications/${id}/`, token, 'DELETE') + .then((success) => retryFn()) + .catch((e) => { + console.log(e.message); + }); + }; const replacedSubject = subject.replace('task=', 'search='); const Navigate = () => navigate(`/inbox/message/${messageId}/${location.search}`); @@ -121,11 +128,10 @@ export function NotificationCard({ )} - deleteNotification(messageId)} /> {messageType !== null ? ( diff --git a/frontend/src/components/projectCreate/index.js b/frontend/src/components/projectCreate/index.js index eb817ee77b..e6c2097b70 100644 --- a/frontend/src/components/projectCreate/index.js +++ b/frontend/src/components/projectCreate/index.js @@ -195,7 +195,7 @@ const ProjectCreate = (props) => { setMapObj={setMapObj} step={step} /> -
    +

    MAP_MAX_AREA || metadata.area === 0 ? 'bg-red' : 'bg-green' diff --git a/frontend/src/components/projectCreate/projectCreationMap.js b/frontend/src/components/projectCreate/projectCreationMap.js index 0a24b5f161..8ece0bad0c 100644 --- a/frontend/src/components/projectCreate/projectCreationMap.js +++ b/frontend/src/components/projectCreate/projectCreationMap.js @@ -17,12 +17,12 @@ const BasemapMenu = ({ map }) => { // Remove elements that require mapbox token; let styles = BASEMAP_OPTIONS; if (!MAPBOX_TOKEN) { - styles = BASEMAP_OPTIONS.filter(s => typeof s.value === 'object'); + styles = BASEMAP_OPTIONS.filter((s) => typeof s.value === 'object'); } const [basemap, setBasemap] = useState(styles[0].label); - const handleClick = style => { + const handleClick = (style) => { let styleValue = style.value; if (typeof style.value === 'string') { @@ -34,7 +34,7 @@ const BasemapMenu = ({ map }) => { return (

    - {styles.map(style => { + {styles.map((style) => { return (
    handleClick(style)} @@ -52,7 +52,7 @@ const BasemapMenu = ({ map }) => { const ProjectCreationMap = ({ mapObj, setMapObj, metadata, updateMetadata, step }) => { const mapRef = React.createRef(); - const locale = useSelector(state => state.preferences['locale']); + const locale = useSelector((state) => state.preferences['locale']); useLayoutEffect(() => { setMapObj({ @@ -65,7 +65,8 @@ const ProjectCreationMap = ({ mapObj, setMapObj, metadata, updateMetadata, step attributionControl: false, }) .addControl(new mapboxgl.AttributionControl({ compact: false })) - .addControl(new MapboxLanguage({ defaultLanguage: locale.substr(0, 2) || 'en' })), + .addControl(new MapboxLanguage({ defaultLanguage: locale.substr(0, 2) || 'en' })) + .addControl(new mapboxgl.ScaleControl({ unit: 'metric' })), }); return () => { @@ -82,11 +83,11 @@ const ProjectCreationMap = ({ mapObj, setMapObj, metadata, updateMetadata, step }); // Remove area and geometry when aoi is deleted. - mapObj.map.on('draw.delete', event => { + mapObj.map.on('draw.delete', (event) => { updateMetadata({ ...metadata, geom: null, area: 0 }); }); - mapObj.map.on('style.load', event => { + mapObj.map.on('style.load', (event) => { if (!MAPBOX_TOKEN) { return; } diff --git a/frontend/src/components/projectDetail/messages.js b/frontend/src/components/projectDetail/messages.js index 299334e890..4609b622d0 100644 --- a/frontend/src/components/projectDetail/messages.js +++ b/frontend/src/components/projectDetail/messages.js @@ -195,14 +195,6 @@ export default defineMessages({ id: 'project.detail.sections.questionsAndComments', defaultMessage: 'Questions and comments', }, - managersHashtagTip: { - id: 'project.detail.sections.questionsAndComments.help.managers', - defaultMessage: 'Add "{hashtag}" to notify the project managers about your comment.', - }, - authorHashtagTip: { - id: 'project.detail.sections.questionsAndComments.help.author', - defaultMessage: 'Add "{hashtag}" to notify the project author about your comment.', - }, contributions: { id: 'project.detail.sections.contributions', defaultMessage: 'Contributions', diff --git a/frontend/src/components/projectDetail/questionsAndComments.js b/frontend/src/components/projectDetail/questionsAndComments.js index 43f3069560..6dbc47e50b 100644 --- a/frontend/src/components/projectDetail/questionsAndComments.js +++ b/frontend/src/components/projectDetail/questionsAndComments.js @@ -1,85 +1,17 @@ import React, { useLayoutEffect, useState } from 'react'; import { useSelector } from 'react-redux'; -import ReactTextareaAutocomplete from '@webscopeio/react-textarea-autocomplete'; import { FormattedMessage } from 'react-intl'; import messages from './messages'; import { RelativeTimeWithUnit } from '../../utils/formattedRelativeTime'; import { PaginatorLine } from '../paginator'; import { Button } from '../button'; -import { HashtagPaste } from './hashtagPaste'; +import { CommentInputField } from '../comments/commentInput'; import { CurrentUserAvatar, UserAvatar } from '../user/avatar'; -import { htmlFromMarkdown } from '../../utils/htmlFromMarkdown'; +import { htmlFromMarkdown, formatUserNamesToLink } from '../../utils/htmlFromMarkdown'; import { pushToLocalJSONAPI, fetchLocalJSONAPI } from '../../network/genericJSONRequest'; import '@webscopeio/react-textarea-autocomplete/style.css'; -const formatUserNamesToLink = (text) => { - const regex = /@\[([^\]]+)\]/gi; - // Find usernames with a regular expression. They all start with '[@' and end with ']' - const usernames = text && text.match(regex); - if (usernames) { - for (let i = 0; i < usernames.length; i++) { - // Strip off the first two characters: '@[' - let username = usernames[i].substring(2, usernames[i].length); - // Strip off the last character - username = username.substring(0, username.length - 1); - text = text.replace( - usernames[i], - '' + - username + - '', - ); - } - } - return text; -}; - -const Item = ({ entity: { name } }) => ( -
    - {`${name}`} -
    -); - -export const UserFetchTextarea = ({ value, setValueFn, token }) => { - const fetchUsers = async (user) => { - const url = `users/queries/filter/${user}/`; - let userItems; - - try { - const res = await fetchLocalJSONAPI(url, token); - userItems = res.usernames.map((u) => { - return { name: u }; - }); - } catch (e) { - userItems = []; - } - - return userItems; - }; - - return ( - } - rows={3} - trigger={{ - '@': { - dataProvider: fetchUsers, - component: Item, - output: (item, trigger) => '@[' + item.name + ']', - }, - }} - /> - ); -}; - const PostProjectComment = ({ token, projectId, setStat }) => { const [comment, setComment] = useState(''); @@ -102,18 +34,7 @@ const PostProjectComment = ({ token, projectId, setStat }) => {
    - setComment(e.target.value)} - token={token} - /> - {comment && ( - - - , - - - )} +
    @@ -305,11 +301,7 @@ export function CompletionTabForValidation({

    - setTaskComment(e.target.value)} - token={token} - /> +

    diff --git a/frontend/src/components/taskSelection/footer.js b/frontend/src/components/taskSelection/footer.js index 9c8531242a..8662d82ee2 100644 --- a/frontend/src/components/taskSelection/footer.js +++ b/frontend/src/components/taskSelection/footer.js @@ -214,7 +214,10 @@ const TaskSelectionFooter = (props) => { ) ? ( ) : ( - + )}
    diff --git a/frontend/src/components/taskSelection/messages.js b/frontend/src/components/taskSelection/messages.js index 6819ff672a..4a0bc7b7fe 100644 --- a/frontend/src/components/taskSelection/messages.js +++ b/frontend/src/components/taskSelection/messages.js @@ -196,7 +196,8 @@ export default defineMessages({ }, validateSelectedTask: { id: 'project.selectTask.footer.button.validateSelectedTask', - defaultMessage: 'Validate selected task', + defaultMessage: + '{number, plural, one {Validate selected task} other {Validate # selected tasks}}', }, validateAnotherTask: { id: 'project.selectTask.footer.button.validateAnotherTask', diff --git a/frontend/src/components/taskSelection/taskActivity.js b/frontend/src/components/taskSelection/taskActivity.js index 7f87c1fdcd..680afedceb 100644 --- a/frontend/src/components/taskSelection/taskActivity.js +++ b/frontend/src/components/taskSelection/taskActivity.js @@ -1,5 +1,6 @@ import React, { useState, useEffect, useCallback } from 'react'; import { useSelector } from 'react-redux'; + import { viewport } from '@mapbox/geo-viewport'; import { FormattedMessage } from 'react-intl'; @@ -8,6 +9,7 @@ import { RelativeTimeWithUnit } from '../../utils/formattedRelativeTime'; import { CloseIcon } from '../svgIcons'; import { useInterval } from '../../hooks/UseInterval'; import { formatOSMChaLink } from '../../utils/osmchaLink'; +import { htmlFromMarkdown, formatUserNamesToLink } from '../../utils/htmlFromMarkdown'; import { getIdUrl, sendJosmCommands } from '../../utils/openEditor'; import { formatOverpassLink } from '../../utils/overpassLink'; import { compareHistoryLastUpdate } from '../../utils/sorting'; @@ -15,8 +17,7 @@ import { CurrentUserAvatar, UserAvatar } from '../user/avatar'; import { pushToLocalJSONAPI, fetchLocalJSONAPI } from '../../network/genericJSONRequest'; import { Button, CustomButton } from '../button'; import { Dropdown } from '../dropdown'; -import { UserFetchTextarea } from '../projectDetail/questionsAndComments'; -import { HashtagPaste } from '../projectDetail/hashtagPaste'; +import { CommentInputField } from '../comments/commentInput'; const PostComment = ({ projectId, taskId, setCommentPayload }) => { const token = useSelector((state) => state.auth.get('token')); @@ -46,18 +47,7 @@ const PostComment = ({ projectId, taskId, setCommentPayload }) => {
    - setComment(e.target.value)} - token={token} - /> - {comment && ( - - - , - - - )} +
    @@ -164,7 +154,12 @@ export const TaskHistory = ({ projectId, taskId, commentPayload }) => { {getTaskActionMessage(t.action, t.actionText)}{' '}

    - {t.action === 'COMMENT' ?

    {t.actionText}

    : null} + {t.action === 'COMMENT' ? ( +

    + ) : null}
    )); diff --git a/frontend/src/components/teamsAndOrgs/management.js b/frontend/src/components/teamsAndOrgs/management.js index 4b1786d59c..bd5c84f742 100644 --- a/frontend/src/components/teamsAndOrgs/management.js +++ b/frontend/src/components/teamsAndOrgs/management.js @@ -23,15 +23,17 @@ export const AddButton = () => ( ); -export const DeleteButton = ({ className, onClick }: Object) => ( +export const DeleteButton = ({ className, onClick, showText = true }: Object) => ( - - - + {showText && ( + + + + )} ); diff --git a/frontend/src/components/teamsAndOrgs/organisations.js b/frontend/src/components/teamsAndOrgs/organisations.js index ac35ccd962..fd405ce732 100644 --- a/frontend/src/components/teamsAndOrgs/organisations.js +++ b/frontend/src/components/teamsAndOrgs/organisations.js @@ -7,7 +7,7 @@ import { FormattedMessage } from 'react-intl'; import messages from './messages'; import { IMAGE_UPLOAD_SERVICE } from '../../config'; -import { pushToLocalJSONAPI } from '../../network/genericJSONRequest'; +import { useUploadImage } from '../../hooks/UseUploadImage'; import { EditModeControl } from './editMode'; import { Management } from './management'; import { Button } from '../button'; @@ -134,49 +134,10 @@ export function OrganisationForm(props) { export function OrgInformation(props) { const token = useSelector((state) => state.auth.get('token')); - const [uploading, setUploading] = useState(false); - const [uploadError, setUploadError] = useState(null); + const [uploadError, uploading, uploadImg] = useUploadImage(); const labelClasses = 'db pt3 pb2'; const fieldClasses = 'blue-grey w-100 pv3 ph2 input-reset ba b--grey-light bg-transparent'; - const uploadImage = (file, onChange) => { - const fileInfo = file.files[0]; - const promise = new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.readAsDataURL(fileInfo); - reader.onload = () => { - if (!!reader.result) { - resolve(reader.result); - } else { - reject(Error('Failed converting to base64')); - } - }; - }); - promise.then( - (result) => { - const payload = JSON.stringify({ - mime: fileInfo.type, - data: result.split('base64,')[1], - filename: fileInfo.name, - }); - setUploading(true); - pushToLocalJSONAPI('system/image-upload/', payload, token) - .then((res) => { - onChange(res.url); - setUploading(false); - setUploadError(null); - }) - .catch((e) => { - setUploadError(e); - setUploading(false); - }); - }, - (err) => { - setUploadError(err); - }, - ); - }; - return ( <>
    @@ -209,7 +170,7 @@ export function OrgInformation(props) { type="file" multiple={false} accept="image/png, image/jpeg, image/webp" - onChange={(e) => uploadImage(e.target, fieldProps.input.onChange)} + onChange={(e) => uploadImg(e.target.files[0], fieldProps.input.onChange, token)} /> { it('exports MATOMO_ID', () => { expect(typeof config.MATOMO_ID).toBe('string'); }); -it('exports IMAGE_UPLOAD_SERVICE', () => { +it('exports HOMEPAGE_VIDEO_URL', () => { expect(typeof config.HOMEPAGE_VIDEO_URL).toBe('string'); }); it('exports IMAGE_UPLOAD_SERVICE', () => { - expect(typeof config.IMAGE_UPLOAD_SERVICE).toBe('boolean'); + expect(typeof config.IMAGE_UPLOAD_SERVICE).toBe('string'); }); -it('exports DEFAULT locale to be english', () => { +it('DEFAULT_LOCALE to be english', () => { expect(config.DEFAULT_LOCALE).toBe('en'); }); it('exports PROJECTCARD_CONTRIBUTION_SHOWN_THRESHOLD', () => { diff --git a/frontend/src/hooks/UseUploadImage.js b/frontend/src/hooks/UseUploadImage.js new file mode 100644 index 0000000000..f531ecf4b3 --- /dev/null +++ b/frontend/src/hooks/UseUploadImage.js @@ -0,0 +1,62 @@ +import { useState, useCallback } from 'react'; +import { useSelector } from 'react-redux'; + +import { pushToLocalJSONAPI } from '../network/genericJSONRequest'; + +export const useUploadImage = () => { + const [uploading, setUploading] = useState(false); + const [uploadError, setUploadError] = useState(null); + + const uploadImg = useCallback((file, updateFn, token) => { + if (file && updateFn && token) { + const promise = new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => { + if (!!reader.result) { + resolve(reader.result); + } else { + reject(Error('Failed converting to base64')); + } + }; + }); + promise.then( + (result) => { + const payload = JSON.stringify({ + mime: file.type, + data: result.split('base64,')[1], + filename: file.name, + }); + setUploading(true); + pushToLocalJSONAPI('system/image-upload/', payload, token) + .then((res) => { + updateFn(res.url); + setUploading(false); + setUploadError(null); + }) + .catch((e) => { + setUploadError(e); + setUploading(false); + }); + }, + (err) => { + setUploadError(err); + }, + ); + } + }, []); + return [uploadError, uploading, uploadImg]; +}; + +export const useOnDrop = (appendImgToComment) => { + const token = useSelector((state) => state.auth.get('token')); + const [uploadError, uploading, uploadImg] = useUploadImage(); + + const onDrop = useCallback( + (acceptedFiles) => { + acceptedFiles.forEach(async (file) => await uploadImg(file, appendImgToComment, token)); + }, + [token, uploadImg, appendImgToComment], + ); + return [uploadError, uploading, onDrop]; +}; diff --git a/frontend/src/hooks/tests/UseUploadImage.test.js b/frontend/src/hooks/tests/UseUploadImage.test.js new file mode 100644 index 0000000000..be573524d3 --- /dev/null +++ b/frontend/src/hooks/tests/UseUploadImage.test.js @@ -0,0 +1,14 @@ +import { renderHook } from '@testing-library/react-hooks'; + +import { useUploadImage } from '../UseUploadImage'; + +describe('useUploadImage', () => { + it('updateFn returns error, loading and uploadImg function', () => { + // this test is incomplete because it's difficult to test the FileReader + const { result } = renderHook(() => useUploadImage()); + const [error, uploading, uploadImg] = result.current; + expect(error).toBeFalsy(); + expect(uploading).toBeFalsy(); + expect(typeof uploadImg).toBe('function'); + }); +}); diff --git a/frontend/src/locales/ar.json b/frontend/src/locales/ar.json index 9c84a50c1b..da25ff9380 100644 --- a/frontend/src/locales/ar.json +++ b/frontend/src/locales/ar.json @@ -4,6 +4,10 @@ "banner.button.agree": "", "banner.privacyPolicy": "", "banner.text": "", + "comment.input.imageUpload.error": "", + "comment.input.imageUpload.progress": "", + "comment.hashtags.help.managers": "", + "comment.hashtags.help.author": "", "mytasks.mainSection.title": "", "mytasks.contribution": "", "mytasks.filter.all": "", @@ -276,8 +280,6 @@ "project.detail.sections.overview": "", "project.detail.sections.description": "", "project.detail.sections.questionsAndComments": "", - "project.detail.sections.questionsAndComments.help.managers": "", - "project.detail.sections.questionsAndComments.help.author": "", "project.detail.sections.contributions": "", "project.detail.sections.contributionsTimeline": "", "project.detail.sections.contributions.osmcha": "", @@ -532,7 +534,7 @@ "project.selectTask.footer.button.mapSelectedTask": "", "project.selectTask.footer.button.mapAnotherTask": "", "project.selectTask.footer.button.validateRandomTask": "", - "project.selectTask.footer.button.validateSelectedTask": "", + "project.selectTask.footer.button.validateSelectedTask": "{number, plural, zero {} one {} two {} few {} many {} other {}}", "project.selectTask.footer.button.validateAnotherTask": "", "project.selectTask.footer.button.selectAnotherProject": "", "project.selectTask.footer.button.resumeMapping": "", @@ -711,8 +713,10 @@ "user.notifications.mentions": "", "user.notifications.mentions.description": "", "user.notifications.projects": "", + "user.notifications.tasks": "", "user.settings.required": "", "user.notifications.projects.description": "", + "user.notifications.task.description": "", "user.notifications.comments": "", "user.notifications.comments.description": "", "user.settings.become_validator.button": "", diff --git a/frontend/src/locales/cs.json b/frontend/src/locales/cs.json index ba265c2370..f64396c45e 100644 --- a/frontend/src/locales/cs.json +++ b/frontend/src/locales/cs.json @@ -4,6 +4,10 @@ "banner.button.agree": "Souhlasím", "banner.privacyPolicy": "Zásady ochrany osobních údajů", "banner.text": "Používáme cookies a podobné technologie k rozpoznání a analýze vašich návštěv a měření míry využití a aktivity provozu. Informace o tom, jak používáme údaje o vaší návštěvě, nebo informace, které nám poskytnete, se dozvíte na našem {link}. Kliknutím na „Souhlasím“ souhlasíte s používáním cookies.", + "comment.input.imageUpload.error": "Nahrávání obrázku se nezdařilo.", + "comment.input.imageUpload.progress": "", + "comment.hashtags.help.managers": "Přidejte \"{hashtag}\" a informujte vedoucí projektu o vašem komentáři.", + "comment.hashtags.help.author": "Přidejte „{hashtag}“ a informujte autora projektu o vašem komentáři.", "mytasks.mainSection.title": "Mé úlohy", "mytasks.contribution": "Zapojit se", "mytasks.filter.all": "Vše", @@ -132,7 +136,7 @@ "project.typesOfMapping.landUse": "Využití krajiny", "project.typesOfMapping.waterways": "Vodní cesty", "project.typesOfMapping.other": "Ostatní", - "project.typesOfMapping.pointsOfInterest": "", + "project.typesOfMapping.pointsOfInterest": "Body zájmu", "notifications.mainSection.title": "Oznámení", "notifications.filter.all": "Vše", "notifications.filter.messages": "Zprávy", @@ -144,8 +148,8 @@ "notifications.navFilters.error": "Chyba při načítání {xWord} pro {yWord}", "notifications.navFilters.error.simple": "Chyba při načítání {xWord}", "notifications.pagination.count": "Zobrazeno {number} z {total}", - "notifications.nav.new.one": "", - "notifications.nav.new.plural": "", + "notifications.nav.new.one": "1 nová zpráva", + "notifications.nav.new.plural": "{n} nové zprávy", "notifications.nav.viewAll": "Zobrazit vše", "notifications.nav.goToNotifications": "Přejít na oznámení", "notifications.nav.noUnread": "Žádná nepřečtená zpráva", @@ -246,7 +250,7 @@ "project.detail.editProject": "Upravit projekt", "project.detail.editor": "Editor", "project.detail.editor.select": "Vybrat editor", - "project.detail.tasks": "Úkoly", + "project.detail.tasks": "Úlohy", "project.detail.instructions": "Instrukce", "project.detail.imagery": "Mapový podklad", "project.detail.imagery.tms": "Vlastní vrstva TMS", @@ -276,12 +280,10 @@ "project.detail.sections.overview": "Přehled", "project.detail.sections.description": "Popis", "project.detail.sections.questionsAndComments": "Otázky a komentáře", - "project.detail.sections.questionsAndComments.help.managers": "", - "project.detail.sections.questionsAndComments.help.author": "", "project.detail.sections.contributions": "Příspěvky", "project.detail.sections.contributionsTimeline": "Časová osa příspěvků", "project.detail.sections.contributions.osmcha": "Změny v OSMCha", - "project.detail.sections.contributions.changesets": "", + "project.detail.sections.contributions.changesets": "Sady změn", "project.detail.sections.contributors": "Přispěvatelé", "project.detail.sections.relatedProjects": "Související projekty", "project.detail.sections.contributions.timelineError": "Časová osa bude k dispozici po mapování prvního úkolu.", @@ -338,7 +340,7 @@ "projects.actions.validate_all_tasks.confirmation": "Jste si jistí, že chcete validovat všechny úlohy? Tato akce nelze vzít zpět.", "projects.actions.validate_all_tasks.description": "Tímto se u všech úloh (kromě těch 'nedostupných') nastaví stav 'dokončeno'. Prosím, využijte tuto možnost pouze pokud jste si jistí tím, co děláte. ", "projects.actions.validate_all_tasks.success": "Úlohy byly úspěšně validovány.", - "projects.actions.validate_all_tasks.error": "", + "projects.actions.validate_all_tasks.error": "Validování všech úloh selhalo z neznámého důvodu.", "projects.actions.reset_bad_imagery_tasks.title": "Obnovit úlohy označené jako nedostupné", "projects.actions.reset_bad_imagery_tasks.confirmation": "Opravdu chcete obnovit všechny nedostupné úlohy v tomto projektu? Tento krok je nevratný.", "projects.actions.reset_bad_imagery_tasks.description": "Tímto se u vybraných úloh nastaví stav 'připraveno k mapování'. Prosím, využijte tuto možnost pouze pokud jste si jistí tím, co děláte. ", @@ -532,7 +534,7 @@ "project.selectTask.footer.button.mapSelectedTask": "Mapovat vybranou úlohu", "project.selectTask.footer.button.mapAnotherTask": "Mapovat další úlohu", "project.selectTask.footer.button.validateRandomTask": "Validovat úlohu", - "project.selectTask.footer.button.validateSelectedTask": "Validovat vybranou úlohu", + "project.selectTask.footer.button.validateSelectedTask": "{number, plural, one {Validovat vybranou úlohu} few {Validovat vybrané úlohy} many {Validovat vybrané úlohy} other {Validovat # vybraných úloh}}", "project.selectTask.footer.button.validateAnotherTask": "Validovat jinou úlohu", "project.selectTask.footer.button.selectAnotherProject": "Vybrat jiný projekt", "project.selectTask.footer.button.resumeMapping": "Zpět k mapování", @@ -617,7 +619,7 @@ "project.level.intermediate": "Středně pokročilý", "project.level.beginner": "Začátečník", "project.level.new_users": "Noví uživatelé", - "project.contributions.stats": "", + "project.contributions.stats": "Statistiky", "project.permissions.error.title": "Nejste připraveni pracovat na tomto projektu ...", "project.permissions.error.userLevelToValidate": "Tento projekt mohou ověřit pouze uživatelé se střední nebo pokročilou zkušeností.", "project.permissions.error.userLevelToMap": "Tento projekt mohou mapovat pouze uživatelé se středně pokročilými nebo pokročilými zkušenostmi.", @@ -708,13 +710,15 @@ "user.settings.language.description": "Definujte preferovaný jazyk. Ovlivní to také jazyk map, který vidíte v Tasking Manageru", "user.settings.become_validator": "Staňte se validátorem", "user.settings.become_validator.description": "Validátoři kontrolují kvalitu úprav dokončených map a poskytují zpětnou vazbu maperům k jejich zlepšení. Pokud máte větší zkušenosti s mapováním, požádejte si o roli validátora.", - "user.notifications.mentions": "", - "user.notifications.mentions.description": "", + "user.notifications.mentions": "E-maily se zmínkami", + "user.notifications.mentions.description": "Obdržíte e-mail pokaždé, když je vaše uživatelské jméno uvedeno v komentáři.", "user.notifications.projects": "Aktualizace projektu", + "user.notifications.tasks": "", "user.settings.required": "Povinná pole", "user.notifications.projects.description": "Přijde vám oznámení, pokud v projektu, do kterého jste se zapojili, dojde k nějaké změně.", + "user.notifications.task.description": "", "user.notifications.comments": "Komentáře", - "user.notifications.comments.description": "", + "user.notifications.comments.description": "Obdrží oznámení pokaždé, když někdo zveřejní komentář k projektům nebo úkolům, ke kterým jste přispěli.", "user.settings.become_validator.button": "Zjistit jak", "user.welcome.title": "Vítejte v Tasking Manageru!", "user.interests.update.success": "Zájmy úspěšně aktualizovány.", @@ -850,7 +854,7 @@ "pages.learn.tutorials.osm_step_by_step.title": "Naučte se OpenStreetMap krok za krokem", "pages.learn.tutorials.osm_step_by_step.description": "Průvodce pro začátečníky k mapování na OpenStreetMap", "pages.learn.tutorials.learnosm.title": "Průvodce administrací", - "pages.learn.tutorials.learnosm.description": "Manuál o tom, jak vytvářet a spravovat projekty ve Správci úloh", + "pages.learn.tutorials.learnosm.description": "Manuál o tom, jak vytvářet a spravovat projekty v Tasking Manageru", "pages.learn.map.steps.project.title": "Vyberte projekt", "pages.learn.map.steps.project.description": "Vyhledejte v našem seznamu projektů ten, který odpovídá vašim zájmům a odpovídá vaší úrovni dovedností.", "pages.learn.map.steps.task.title": "Vyberte úlohu", @@ -864,7 +868,7 @@ "pages.learn.videos.map_buildings.title": "Vyberte úlohu a mapujte budovy", "pages.learn.videos.map_buildings.description": "Naučte se, jak mapovat budovy na OpenStreetMap.", "pages.learn.validate.video.title": "Naučit se validovat", - "pages.learn.validate.video.description": "", + "pages.learn.validate.video.description": "Naučte se, jak začít validovat projekty v Tasking Manageru.", "pages.learn.validate_title": "Naučte se validovat", "pages.learn.validate.intro": "Validace je důležitou součástí procesu. Vyžaduje důvěru ve vaše mapovací schopnosti a ochotu pomoci koučovat a radit novějším mapovačům.", "pages.learn.validate.description": "Získání druhého, třetího nebo čtvrtého páru očí na mapované funkce je důležitým krokem k zajištění kvality dat přidávaných do OpenStreetMap a jejich použití po celém světě.", diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json index 598e13b926..8b108b569e 100644 --- a/frontend/src/locales/de.json +++ b/frontend/src/locales/de.json @@ -4,6 +4,10 @@ "banner.button.agree": "Ich stimme zu", "banner.privacyPolicy": "Datenschutzrichtlinien", "banner.text": "Wir verwenden Cookies und ähnliche Technologien um Ihre Besuche auf unserer Seite zu erkennen und zu analysieren sowie um den Datentransfer zu verfolgen. Wenn Sie mehr darüber erfahren wollen, wie wir die Daten verwenden die wir von Ihnen erhalten, sehen Sie bitte hier nach: {link}. Wenn Sie auf \"Ich stimme zu\" klicken, erteilen SIe die Genehmigung, Cookies in diesem Sinn zu verwenden.", + "comment.input.imageUpload.error": "Der Bild-Upload ist fehlgeschlagen.", + "comment.input.imageUpload.progress": "", + "comment.hashtags.help.managers": "", + "comment.hashtags.help.author": "", "mytasks.mainSection.title": "Meine Aufgaben", "mytasks.contribution": "Beiträge", "mytasks.filter.all": "Alle", @@ -276,8 +280,6 @@ "project.detail.sections.overview": "Überblick", "project.detail.sections.description": "Beschreibung", "project.detail.sections.questionsAndComments": "Fragen und Kommentare", - "project.detail.sections.questionsAndComments.help.managers": "", - "project.detail.sections.questionsAndComments.help.author": "", "project.detail.sections.contributions": "Beiträge", "project.detail.sections.contributionsTimeline": "Zeitachse der Beiträge", "project.detail.sections.contributions.osmcha": "Changsets in OSMCha", @@ -532,7 +534,7 @@ "project.selectTask.footer.button.mapSelectedTask": "Ausgewählte Aufgabe kartieren", "project.selectTask.footer.button.mapAnotherTask": "Eine andere Aufgabe kartieren", "project.selectTask.footer.button.validateRandomTask": "Validieren einer Aufgabe", - "project.selectTask.footer.button.validateSelectedTask": "Ausgewählte Aufgabe validieren", + "project.selectTask.footer.button.validateSelectedTask": "{number, plural, one {} other {}}", "project.selectTask.footer.button.validateAnotherTask": "Validieren Sie eine andere Aufgabe", "project.selectTask.footer.button.selectAnotherProject": "Wählen Sie ein anderes Projekt aus", "project.selectTask.footer.button.resumeMapping": "Kartierung fortsetzen", @@ -711,8 +713,10 @@ "user.notifications.mentions": "", "user.notifications.mentions.description": "", "user.notifications.projects": "Projektaktualisierungen", + "user.notifications.tasks": "", "user.settings.required": "Erforderliche Felder", "user.notifications.projects.description": "Sie erhalten eine Benachrichtigung, wenn ein Projekt, zu dem Sie beigetragen haben, Fortschritte macht.", + "user.notifications.task.description": "", "user.notifications.comments": "Kommentare", "user.notifications.comments.description": "", "user.settings.become_validator.button": "So geht's", diff --git a/frontend/src/locales/el.json b/frontend/src/locales/el.json index 6a55307b54..49efaec4e1 100644 --- a/frontend/src/locales/el.json +++ b/frontend/src/locales/el.json @@ -4,6 +4,10 @@ "banner.button.agree": "Συμφωνώ", "banner.privacyPolicy": "πολιτική απορρήτου", "banner.text": "Χρησιμοποιούμε cookies και παρόμοιες τεχνολογίες για να αναγνωρίζουμε και να αναλύουμε τις επισκέψεις σας καθώς και για να μετράμε την κίνηση χρήσης και τη δραστηριότητα. Μπορείτε να μάθετε για το πώς χρησιμοποιούμε τα δεδομένα σχετικά με την επίσκεψή σας ή τις πληροφορίες που παρέχετε, διαβάζοντας στο {link}. Κάνοντας κλικ στο \"Συμφωνώ\", αποδέχεστε την χρήση των cookies.", + "comment.input.imageUpload.error": "Το ανέβασμα της εικόνας απέτυχε.", + "comment.input.imageUpload.progress": "", + "comment.hashtags.help.managers": "", + "comment.hashtags.help.author": "", "mytasks.mainSection.title": "Οι Εργασίες μου", "mytasks.contribution": "Συνεισφορά", "mytasks.filter.all": "Όλα", @@ -276,8 +280,6 @@ "project.detail.sections.overview": "Επισκόπηση", "project.detail.sections.description": "Περιγραφή", "project.detail.sections.questionsAndComments": "Ερωτήσεις και σχόλια", - "project.detail.sections.questionsAndComments.help.managers": "", - "project.detail.sections.questionsAndComments.help.author": "", "project.detail.sections.contributions": "Συνεισφορές", "project.detail.sections.contributionsTimeline": "Χρονοσειρά συνεισφορών", "project.detail.sections.contributions.osmcha": "Σετ Αλλαγών στην OSMCha", @@ -532,7 +534,7 @@ "project.selectTask.footer.button.mapSelectedTask": "Χαρτογραφήστε την επιλεγμένη εργασία", "project.selectTask.footer.button.mapAnotherTask": "Χαρτογραφήστε άλλη εργασία", "project.selectTask.footer.button.validateRandomTask": "Επικυρώστε εργασία", - "project.selectTask.footer.button.validateSelectedTask": "Επικυρώστε την επιλεγμένη εργασία", + "project.selectTask.footer.button.validateSelectedTask": "{number, plural, one {} other {}}", "project.selectTask.footer.button.validateAnotherTask": "Επικυρώστε άλλη εργασία", "project.selectTask.footer.button.selectAnotherProject": "Επιλέξτε ένα άλλο έργο", "project.selectTask.footer.button.resumeMapping": "Συνεχίστε την χαρτογράφηση", @@ -711,8 +713,10 @@ "user.notifications.mentions": "", "user.notifications.mentions.description": "", "user.notifications.projects": "Ενημερώσεις έργου", + "user.notifications.tasks": "", "user.settings.required": "Απαιτούμενα πεδία", "user.notifications.projects.description": "Λαμβάνετε μια ειδοποίηση όταν υπάρχει πρόοδος σε ένα έργο στο οποίο έχετε συνεισφέρει.", + "user.notifications.task.description": "", "user.notifications.comments": "Σχόλια", "user.notifications.comments.description": "", "user.settings.become_validator.button": "Μάθετε πώς", diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 2aae1d8ecc..1e59fefec8 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -4,6 +4,10 @@ "banner.button.agree": "I agree", "banner.privacyPolicy": "privacy policy", "banner.text": "We use cookies and similar technologies to recognize and analyze your visits, and measure traffic usage and activity. You can learn about how we use the data about your visit or information you provide reading our {link}. By clicking \"I Agree\", you consent to the use of cookies.", + "comment.input.imageUpload.error": "The image upload failed.", + "comment.input.imageUpload.progress": "Uploading file...", + "comment.hashtags.help.managers": "Add \"{hashtag}\" to notify the project managers about your comment.", + "comment.hashtags.help.author": "Add \"{hashtag}\" to notify the project author about your comment.", "mytasks.mainSection.title": "My Tasks", "mytasks.contribution": "Contribution", "mytasks.filter.all": "All", @@ -276,8 +280,6 @@ "project.detail.sections.overview": "Overview", "project.detail.sections.description": "Description", "project.detail.sections.questionsAndComments": "Questions and comments", - "project.detail.sections.questionsAndComments.help.managers": "Add \"{hashtag}\" to notify the project managers about your comment.", - "project.detail.sections.questionsAndComments.help.author": "Add \"{hashtag}\" to notify the project author about your comment.", "project.detail.sections.contributions": "Contributions", "project.detail.sections.contributionsTimeline": "Contributions timeline", "project.detail.sections.contributions.osmcha": "Changesets in OSMCha", @@ -532,7 +534,7 @@ "project.selectTask.footer.button.mapSelectedTask": "Map selected task", "project.selectTask.footer.button.mapAnotherTask": "Map another task", "project.selectTask.footer.button.validateRandomTask": "Validate a task", - "project.selectTask.footer.button.validateSelectedTask": "Validate selected task", + "project.selectTask.footer.button.validateSelectedTask": "{number, plural, one {Validate selected task} other {Validate # selected tasks}}", "project.selectTask.footer.button.validateAnotherTask": "Validate another task", "project.selectTask.footer.button.selectAnotherProject": "Select another project", "project.selectTask.footer.button.resumeMapping": "Resume mapping", @@ -711,8 +713,10 @@ "user.notifications.mentions": "Mentions emails", "user.notifications.mentions.description": "Receive an email every time your username is mentioned on a comment.", "user.notifications.projects": "Project updates", + "user.notifications.tasks": "Tasks validation emails", "user.settings.required": "Required fields", "user.notifications.projects.description": "You get a notification when a project you have contributed to makes progress.", + "user.notifications.task.description": "Receive an email when a task you have contributed to is validated.", "user.notifications.comments": "Comments", "user.notifications.comments.description": "Receive a notification every time someone posts a comment on projects or tasks you have contributed to.", "user.settings.become_validator.button": "Learn how", diff --git a/frontend/src/locales/es.json b/frontend/src/locales/es.json index 08455836bd..a4659e8df4 100644 --- a/frontend/src/locales/es.json +++ b/frontend/src/locales/es.json @@ -4,6 +4,10 @@ "banner.button.agree": "Estoy de acuerdo", "banner.privacyPolicy": "política de privacidad", "banner.text": "Utilizamos cookies y tecnologías similares para reconocer y analizar sus visitas, medir el uso y la actividad del tráfico. Puede obtener información sobre cómo usamos los datos sobre su visita o la información que proporciona leyendo nuestro {link}. Al hacer clic en \"Acepto\", acepta el uso de cookies.", + "comment.input.imageUpload.error": "Carga fallida.", + "comment.input.imageUpload.progress": "Cargando archivo...", + "comment.hashtags.help.managers": "Agregue «{hashtag}» para notificar a las personas que administran el proyecto su comentario.", + "comment.hashtags.help.author": "Añada «{hashtag}» para notificar al autor(a) del proyecto su comentario.", "mytasks.mainSection.title": "Mis tareas", "mytasks.contribution": "Contribución", "mytasks.filter.all": "Todos", @@ -276,8 +280,6 @@ "project.detail.sections.overview": "Visión general", "project.detail.sections.description": "Descripción", "project.detail.sections.questionsAndComments": "Preguntas y comentarios", - "project.detail.sections.questionsAndComments.help.managers": "Agregue «{hashtag}» para notificar a las personas que administran el proyecto su comentario.", - "project.detail.sections.questionsAndComments.help.author": "Añada «{hashtag}» para notificar al autor(a) del proyecto su comentario.", "project.detail.sections.contributions": "Contribuciones", "project.detail.sections.contributionsTimeline": "Cronología de contribuciones", "project.detail.sections.contributions.osmcha": "Conjunto de cambios en OSMCha", @@ -532,7 +534,7 @@ "project.selectTask.footer.button.mapSelectedTask": "Mapear tarea seleccionada", "project.selectTask.footer.button.mapAnotherTask": "Mapear otra tarea", "project.selectTask.footer.button.validateRandomTask": "Validar una tarea", - "project.selectTask.footer.button.validateSelectedTask": "Validar tarea seleccionada", + "project.selectTask.footer.button.validateSelectedTask": "{number, plural, one {Validar tarea seleccionada} other {Validar # tareas seleccionadas}}", "project.selectTask.footer.button.validateAnotherTask": "Validar otra tarea", "project.selectTask.footer.button.selectAnotherProject": "Selecciona otro proyecto", "project.selectTask.footer.button.resumeMapping": "Reanudar mapeo", @@ -711,8 +713,10 @@ "user.notifications.mentions": "Menciones por correos electrónicos", "user.notifications.mentions.description": "Reciba un correo electrónico cada vez que se mencione su nombre de usuario en un comentario.", "user.notifications.projects": "Actualizaciones del proyecto", + "user.notifications.tasks": "Correo electrónico de validación de tareas", "user.settings.required": "Campos requeridos", "user.notifications.projects.description": "Recibirá una notificación cuando un proyecto al que haya contribuido progrese.", + "user.notifications.task.description": "Reciba un correo electrónico cuando se valida una tarea a la que ha contribuido.", "user.notifications.comments": "Comentarios", "user.notifications.comments.description": "Reciba una notificación cada vez que alguien publique un comentario sobre proyectos o tareas a las que haya contribuido.", "user.settings.become_validator.button": "Aprender cómo", diff --git a/frontend/src/locales/fa_IR.json b/frontend/src/locales/fa_IR.json index 8e85a93ec4..d078966e76 100644 --- a/frontend/src/locales/fa_IR.json +++ b/frontend/src/locales/fa_IR.json @@ -4,6 +4,10 @@ "banner.button.agree": "موافقم", "banner.privacyPolicy": "سیاست حفظ حریم خصوصی", "banner.text": "", + "comment.input.imageUpload.error": "", + "comment.input.imageUpload.progress": "", + "comment.hashtags.help.managers": "", + "comment.hashtags.help.author": "", "mytasks.mainSection.title": "وظایف من", "mytasks.contribution": "مشارکت", "mytasks.filter.all": "همه", @@ -276,8 +280,6 @@ "project.detail.sections.overview": "بررسی اجمالی", "project.detail.sections.description": "توضیح", "project.detail.sections.questionsAndComments": "سوالات و نظرات", - "project.detail.sections.questionsAndComments.help.managers": "", - "project.detail.sections.questionsAndComments.help.author": "", "project.detail.sections.contributions": "مشارکتها", "project.detail.sections.contributionsTimeline": "جدول زمانی مشارکت‌ها", "project.detail.sections.contributions.osmcha": "", @@ -532,7 +534,7 @@ "project.selectTask.footer.button.mapSelectedTask": "", "project.selectTask.footer.button.mapAnotherTask": "", "project.selectTask.footer.button.validateRandomTask": "", - "project.selectTask.footer.button.validateSelectedTask": "", + "project.selectTask.footer.button.validateSelectedTask": "{number, plural, one {} other {}}", "project.selectTask.footer.button.validateAnotherTask": "", "project.selectTask.footer.button.selectAnotherProject": "انتخاب پروژه دیگر", "project.selectTask.footer.button.resumeMapping": "نقشه برداری را از سر بگیرید", @@ -711,8 +713,10 @@ "user.notifications.mentions": "", "user.notifications.mentions.description": "", "user.notifications.projects": "به روز رسانی پروژه", + "user.notifications.tasks": "", "user.settings.required": "", "user.notifications.projects.description": "", + "user.notifications.task.description": "", "user.notifications.comments": "نظر", "user.notifications.comments.description": "", "user.settings.become_validator.button": "", diff --git a/frontend/src/locales/fr.json b/frontend/src/locales/fr.json index d4065c7ce6..cdfc46e60f 100644 --- a/frontend/src/locales/fr.json +++ b/frontend/src/locales/fr.json @@ -4,6 +4,10 @@ "banner.button.agree": "Je suis d'accord", "banner.privacyPolicy": "politique de confidentialité", "banner.text": "Nous utilisons des cookies et des technologies similaires pour reconnaître et analyser vos visites, et mesurer l'utilisation et l'activité du trafic. Vous pouvez en savoir plus sur la façon dont nous utilisons les données relatives à votre visite ou les informations que vous fournissez en lisant notre {link}. En cliquant sur « J'accepte », vous consentez à l'utilisation de cookies.", + "comment.input.imageUpload.error": "Échec du chargement de l'image.", + "comment.input.imageUpload.progress": "", + "comment.hashtags.help.managers": "", + "comment.hashtags.help.author": "", "mytasks.mainSection.title": "Mes tâches", "mytasks.contribution": "Contribution", "mytasks.filter.all": "Tout voir", @@ -276,8 +280,6 @@ "project.detail.sections.overview": "Vue d'ensemble", "project.detail.sections.description": "Description", "project.detail.sections.questionsAndComments": "Questions et commentaires", - "project.detail.sections.questionsAndComments.help.managers": "", - "project.detail.sections.questionsAndComments.help.author": "", "project.detail.sections.contributions": "Contributions", "project.detail.sections.contributionsTimeline": "Chronologie des contributions", "project.detail.sections.contributions.osmcha": "Modifications dans OsmCha", @@ -532,7 +534,7 @@ "project.selectTask.footer.button.mapSelectedTask": "Cartographier la tâche sélectionnée", "project.selectTask.footer.button.mapAnotherTask": "Cartographier une autre tâche", "project.selectTask.footer.button.validateRandomTask": "Valider une tâche", - "project.selectTask.footer.button.validateSelectedTask": "Valider la tâche sélectionnée", + "project.selectTask.footer.button.validateSelectedTask": "{number, plural, one {} other {}}", "project.selectTask.footer.button.validateAnotherTask": "Valider une autre tâche", "project.selectTask.footer.button.selectAnotherProject": "Choisir un autre projet", "project.selectTask.footer.button.resumeMapping": "Reprendre la cartographie", @@ -711,8 +713,10 @@ "user.notifications.mentions": "", "user.notifications.mentions.description": "", "user.notifications.projects": "Mises-à-jour sur le projet", + "user.notifications.tasks": "", "user.settings.required": "Champs obligatoires", "user.notifications.projects.description": "Vous recevrez une notification lorsqu'un projet auquel vous avez contribué avance.", + "user.notifications.task.description": "", "user.notifications.comments": "Commentaires", "user.notifications.comments.description": "", "user.settings.become_validator.button": "Apprenez comment", diff --git a/frontend/src/locales/he.json b/frontend/src/locales/he.json index 82354df6d4..2db79b0a40 100644 --- a/frontend/src/locales/he.json +++ b/frontend/src/locales/he.json @@ -4,6 +4,10 @@ "banner.button.agree": "מקובל עלי", "banner.privacyPolicy": "מדיניות פרטיות", "banner.text": "אנו משתמשים בעוגיות ובטכנולוגיות דומות כדי לזהות ולנתח את הביקורים שלך ולמדוד תעבורה ופעילות. ניתן ללמוד עוד על האופן בו אנו משתמשים בנתונים על הביקור שלך או במידע שמסופק על ידיך בקריאת {link}. לחיצה על „מקובל עלי” מהווה את הסכמתך לשימוש בעוגיות.", + "comment.input.imageUpload.error": "העלאת התמונה נכשלה.", + "comment.input.imageUpload.progress": "", + "comment.hashtags.help.managers": "ניתן להוסיף „{hashtag}” כדי להודיע למנהלי המיזם על ההערה שלך.", + "comment.hashtags.help.author": "ניתן להוסיף „{hashtag}” כדי להודיע ליוצרי המיזם על ההערה שלך.", "mytasks.mainSection.title": "המשימות שלי", "mytasks.contribution": "תרומה", "mytasks.filter.all": "הכול", @@ -132,7 +136,7 @@ "project.typesOfMapping.landUse": "ניצול הקרקע", "project.typesOfMapping.waterways": "דרכי מים", "project.typesOfMapping.other": "אחר", - "project.typesOfMapping.pointsOfInterest": "", + "project.typesOfMapping.pointsOfInterest": "נקודות עניין", "notifications.mainSection.title": "התראות", "notifications.filter.all": "הכול", "notifications.filter.messages": "הודעות", @@ -276,8 +280,6 @@ "project.detail.sections.overview": "סקירה", "project.detail.sections.description": "תיאור", "project.detail.sections.questionsAndComments": "שאלות והערות", - "project.detail.sections.questionsAndComments.help.managers": "ניתן להוסיף „{hashtag}” כדי להודיע למנהלי המיזם על ההערה שלך.", - "project.detail.sections.questionsAndComments.help.author": "ניתן להוסיף „{hashtag}” כדי להודיע ליוצרי המיזם על ההערה שלך.", "project.detail.sections.contributions": "תרומות", "project.detail.sections.contributionsTimeline": "ציר זמן התרומות", "project.detail.sections.contributions.osmcha": "ערכות שינויים ב־OSMCha", @@ -532,7 +534,7 @@ "project.selectTask.footer.button.mapSelectedTask": "מיפוי משימה נבחרת", "project.selectTask.footer.button.mapAnotherTask": "מיפוי משימה נוספת", "project.selectTask.footer.button.validateRandomTask": "אימות משימה", - "project.selectTask.footer.button.validateSelectedTask": "אימות משימה נבחרת", + "project.selectTask.footer.button.validateSelectedTask": "{number, plural, one {אימות משימה נבחרת} two {אימות # משימות נבחרות} many {אימות # משימות נבחרות} other {אימות # משימות נבחרות}}", "project.selectTask.footer.button.validateAnotherTask": "אימות משימה אחרת", "project.selectTask.footer.button.selectAnotherProject": "בחירת מיזם נוסף", "project.selectTask.footer.button.resumeMapping": "להמשיך במיפוי", @@ -711,8 +713,10 @@ "user.notifications.mentions": "הודעות דוא״ל על אזכורים", "user.notifications.mentions.description": "לקבל הודעה על כל אזכור של שם המשתמש שלך בתגובה.", "user.notifications.projects": "עדכוני מיזם", + "user.notifications.tasks": "", "user.settings.required": "שדות נדרשים", "user.notifications.projects.description": "תישלח אליך התראה כאשר מיזם אליו תרמת יתקדם באופן כלשהו.", + "user.notifications.task.description": "", "user.notifications.comments": "הערות", "user.notifications.comments.description": "לקבל הודעה בכל פעם ששמך מוזכר בתגובות שמפורסמות במיזמים או במשימות שתרמת להם.", "user.settings.become_validator.button": "למידע כיצד", diff --git a/frontend/src/locales/hu.json b/frontend/src/locales/hu.json index 3f019fe642..30662c8400 100644 --- a/frontend/src/locales/hu.json +++ b/frontend/src/locales/hu.json @@ -4,6 +4,10 @@ "banner.button.agree": "Egyetértek", "banner.privacyPolicy": "adatkezelési irányelvek", "banner.text": "Látogatásai felismerésére és elemzésére, valamint a forgalom és a tevékenység mérésére sütiket és hasonló technológiákat alkalmazunk. {link} oldalunk elolvasásával megtudhatja, miként használjuk fel látogatása adatait vagy az ön által megadott adatokat. Az „Egyetértek” gombra kattintva hozzájárul a sütik használatához.", + "comment.input.imageUpload.error": "A kép feltöltése sikertelen.", + "comment.input.imageUpload.progress": "", + "comment.hashtags.help.managers": "", + "comment.hashtags.help.author": "", "mytasks.mainSection.title": "Feladataim", "mytasks.contribution": "Közreműködés", "mytasks.filter.all": "Összes", @@ -276,8 +280,6 @@ "project.detail.sections.overview": "Áttekintés", "project.detail.sections.description": "Leírás", "project.detail.sections.questionsAndComments": "Kérdések és megjegyzések", - "project.detail.sections.questionsAndComments.help.managers": "", - "project.detail.sections.questionsAndComments.help.author": "", "project.detail.sections.contributions": "Szerkesztések", "project.detail.sections.contributionsTimeline": "Szerkesztési idővonal", "project.detail.sections.contributions.osmcha": "Módosításkészletek az OSMCha-ba (OSM Changeset Analyzer, módosításkészlet-elemző)", @@ -532,7 +534,7 @@ "project.selectTask.footer.button.mapSelectedTask": "Kijelölt feladat térképezése", "project.selectTask.footer.button.mapAnotherTask": "Másik feladat térképezése", "project.selectTask.footer.button.validateRandomTask": "Feladat érvényesítése", - "project.selectTask.footer.button.validateSelectedTask": "Kijelölt feladat érvényesítése", + "project.selectTask.footer.button.validateSelectedTask": "{number, plural, one {} other {}}", "project.selectTask.footer.button.validateAnotherTask": "Másik feladat érvényesítése", "project.selectTask.footer.button.selectAnotherProject": "Másik projekt kijelölése", "project.selectTask.footer.button.resumeMapping": "Térképezés folytatása", @@ -711,8 +713,10 @@ "user.notifications.mentions": "", "user.notifications.mentions.description": "", "user.notifications.projects": "Projekt frissítései", + "user.notifications.tasks": "", "user.settings.required": "Szükséges mezők", "user.notifications.projects.description": "Értesítést kap, ha egy Ön által is szerkesztett projekt halad.", + "user.notifications.task.description": "", "user.notifications.comments": "Megjegyzések", "user.notifications.comments.description": "", "user.settings.become_validator.button": "Tanulás", diff --git a/frontend/src/locales/id.json b/frontend/src/locales/id.json index 89fe7951e9..7732d50932 100644 --- a/frontend/src/locales/id.json +++ b/frontend/src/locales/id.json @@ -4,6 +4,10 @@ "banner.button.agree": "", "banner.privacyPolicy": "", "banner.text": "", + "comment.input.imageUpload.error": "", + "comment.input.imageUpload.progress": "", + "comment.hashtags.help.managers": "", + "comment.hashtags.help.author": "", "mytasks.mainSection.title": "Task saya", "mytasks.contribution": "Kontribusi", "mytasks.filter.all": "Semua", @@ -276,8 +280,6 @@ "project.detail.sections.overview": "Gambaran umum", "project.detail.sections.description": "Deskripsi", "project.detail.sections.questionsAndComments": "Pertanyaan dan komentar", - "project.detail.sections.questionsAndComments.help.managers": "", - "project.detail.sections.questionsAndComments.help.author": "", "project.detail.sections.contributions": "Kontribusi", "project.detail.sections.contributionsTimeline": "Timeline Kontribusi", "project.detail.sections.contributions.osmcha": "", @@ -532,7 +534,7 @@ "project.selectTask.footer.button.mapSelectedTask": "Petakan task terpilih", "project.selectTask.footer.button.mapAnotherTask": "Petakan task lain", "project.selectTask.footer.button.validateRandomTask": "Validasi task", - "project.selectTask.footer.button.validateSelectedTask": "Validasi task terpilih", + "project.selectTask.footer.button.validateSelectedTask": "{number, plural, other {}}", "project.selectTask.footer.button.validateAnotherTask": "Validasi task lain", "project.selectTask.footer.button.selectAnotherProject": "Pilih proyek lain", "project.selectTask.footer.button.resumeMapping": "Lanjutkan pemetaan", @@ -711,8 +713,10 @@ "user.notifications.mentions": "", "user.notifications.mentions.description": "", "user.notifications.projects": "Perkembangan proyek", + "user.notifications.tasks": "", "user.settings.required": "", "user.notifications.projects.description": "Anda akan mendapat notifikasi ketika proyek yang Anda terlibat di dalamnya membuat progres.", + "user.notifications.task.description": "", "user.notifications.comments": "Komentar", "user.notifications.comments.description": "", "user.settings.become_validator.button": "Pelajari bagaimana", diff --git a/frontend/src/locales/it.json b/frontend/src/locales/it.json index 4940e531b0..7729be6ed5 100644 --- a/frontend/src/locales/it.json +++ b/frontend/src/locales/it.json @@ -4,6 +4,10 @@ "banner.button.agree": "approvo", "banner.privacyPolicy": "regole sulla privacy", "banner.text": "Usiamo cookies e tecnologie simili per riconoscere ed analizzare le tue visite, e misurare il traffico di utilizzo e l'attività nel sito. Puoi conoscere il modo in cui utilizziamo i tuoi dati o l'informazione che ci fornisci, leggendo qui {link}. Cliccando su \"approvo\", consenti l'uso dei cookies.", + "comment.input.imageUpload.error": "", + "comment.input.imageUpload.progress": "", + "comment.hashtags.help.managers": "", + "comment.hashtags.help.author": "", "mytasks.mainSection.title": "Compiti personali", "mytasks.contribution": "Contributo", "mytasks.filter.all": "Tutti", @@ -132,7 +136,7 @@ "project.typesOfMapping.landUse": "Landuse (Uso del suolo)", "project.typesOfMapping.waterways": "Corsi d'acqua", "project.typesOfMapping.other": "Altro", - "project.typesOfMapping.pointsOfInterest": "", + "project.typesOfMapping.pointsOfInterest": "Punti di interesse", "notifications.mainSection.title": "Notifiche", "notifications.filter.all": "Tutti", "notifications.filter.messages": "Messaggi", @@ -159,7 +163,7 @@ "notifications.error.tryagain": "Prova ancora", "notifications.refresh": "Aggiorna", "notifications.message.type.system": "Sistema", - "notifications.message.type.broadcast": "", + "notifications.message.type.broadcast": "Trasmissione", "notifications.message.type.mention_notification": "Menzione", "notifications.message.type.validation_notification": "Validazione", "notifications.message.type.invalidation_notification": "Annullamento", @@ -276,8 +280,6 @@ "project.detail.sections.overview": "Panoramica", "project.detail.sections.description": "Descrizione", "project.detail.sections.questionsAndComments": "Domande e commenti", - "project.detail.sections.questionsAndComments.help.managers": "", - "project.detail.sections.questionsAndComments.help.author": "", "project.detail.sections.contributions": "Contributi", "project.detail.sections.contributionsTimeline": "Cronologia dei contributi", "project.detail.sections.contributions.osmcha": "Gruppi di modifiche in OSMCha", @@ -348,7 +350,7 @@ "projects.actions.reset_all_tasks.title": "Resetta i compiti", "projects.actions.reset_all_tasks.button": "Resetta tutti i compiti", "projects.actions.reset_all_tasks.description": "Reimposta tutti i compiti del progetto come pronti alla mappatura, preservandone lo storico.", - "projects.actions.reset_all_tasks.success": "", + "projects.actions.reset_all_tasks.success": "Tutte le attività sono state reimpostate correttamente.", "projects.actions.reset_all_tasks.error": "", "projects.actions.clone_project.button": "Clona progetto", "projects.actions.clone_project.description": "", @@ -451,7 +453,7 @@ "project.nav.contributed": "Contributi", "project.nav.allprojects": "Tutti", "project.nav.active": "Attivo", - "project.nav.managed": "", + "project.nav.managed": "Gestito da me", "project.nav.created": "Creati da me", "project.nav.draft": "Bozza", "project.nav.archived": "Archiviato", @@ -467,9 +469,9 @@ "project.nav.apply": "Applica", "project.nav.clear": "Ripulisci", "project.results.retry": "Riprova", - "projects.stats.title": "", + "projects.stats.title": "Statistiche temporali", "projects.stats.average_mapping_time": "", - "projects.stats.average_validation_time": "", + "projects.stats.average_validation_time": "Tempo medio di convalida", "projects.stats.time_finish_mapping": "", "projects.stats.time_finish_validation": "", "project.stats.tasks.tatus": "Attività per stato", @@ -494,7 +496,7 @@ "project.tasks.josm_error.description": "", "project.tasks.lock_error.generic": "Non è stato possibile bloccare questo compito per te...", "project.tasks.lock_error.generic.description": "", - "project.tasks.lock_error.license.title": "", + "project.tasks.lock_error.license.title": "Questo progetto ha una licenza richiesta.", "project.tasks.lock_error.license.description": "", "project.tasks.lock_error.license.accept": "Accetta", "project.tasks.lock_error.cancel": "Annulla", @@ -521,7 +523,7 @@ "project.instructions": "Istruzioni", "project.changesetComment": "Commento al gruppo di modifiche", "project.contributions": "contribuzioni", - "project.contributions.registered": "", + "project.contributions.registered": "Registrato su", "project.imagery": "Foto aeree/satellitari", "project.imagery.tms": "Layer TMS personalizzato", "project.imagery.wms": "Livello WMS personalizzato", @@ -532,7 +534,7 @@ "project.selectTask.footer.button.mapSelectedTask": "Mappa il compito selezionato", "project.selectTask.footer.button.mapAnotherTask": "Mappa un altro compito", "project.selectTask.footer.button.validateRandomTask": "Valida un compito", - "project.selectTask.footer.button.validateSelectedTask": "Valida il compito selezionato", + "project.selectTask.footer.button.validateSelectedTask": "{number, plural, one {Valida il compito selezionato} other {Valida # compiti selezionati}}", "project.selectTask.footer.button.validateAnotherTask": "Valida un altro compito", "project.selectTask.footer.button.selectAnotherProject": "Scegli un altro progetto", "project.selectTask.footer.button.resumeMapping": "Riprendi la mappatura", @@ -564,7 +566,7 @@ "project.tasks.action.instructions.select_task": "", "project.input.placeholder.write_comment": "Scrivi un commento", "project.tasks.action.instructions.leave_comment": "Lascia un commento (opzionale)", - "project.tasks.action.instructions.submit_task": "", + "project.tasks.action.instructions.submit_task": "Invia il tuo lavoro", "project.tasks.action.comment.title": "Commento", "project.tasks.action.comment.input.placeholder": "", "project.tasks.action.selection.title": "Stato del task", @@ -711,15 +713,17 @@ "user.notifications.mentions": "", "user.notifications.mentions.description": "", "user.notifications.projects": "Aggiornamenti di progetto", + "user.notifications.tasks": "", "user.settings.required": "", "user.notifications.projects.description": "", + "user.notifications.task.description": "", "user.notifications.comments": "Commenti", "user.notifications.comments.description": "", "user.settings.become_validator.button": "", "user.welcome.title": "Benvenuto in Tasking Manager!", "user.interests.update.success": "", "user.interests.update.error": "", - "user.interests.h3": "", + "user.interests.h3": "Interessi", "user.interests.lead": "", "user.completeness.title": "Completa il tuo profilo", "user.completeness.lead.start": "", @@ -728,7 +732,7 @@ "user.completeness.button": "Completa il mio profilo", "user.help.card.title": "Bisogno di aiuto?", "user.help.card.howToMap": "Come mappare?", - "user.help.card.quickStart": "", + "user.help.card.quickStart": "Guida introduttiva", "user.help.card.whatIsOSM": "Cos'è OpenStreetMap?", "user.welcome.firstProject.title": "Contribuisci al tuo primo progetto", "user.welcome.firstProject.text1": "Sembra che tu non abbia ancora mappato nessun compito.", @@ -740,7 +744,7 @@ "user.osm.history.link": "", "user.osm.profile.edit.link": "", "user.settings.apiKey.title": "", - "user.settings.apiDocs": "", + "user.settings.apiDocs": "Documentazione API", "user.settings.apiKey.description": "", "user.settings.email.confirmation": "", "user.settings.email.resend": "Ripeti l'invio della mail di validazione", diff --git a/frontend/src/locales/ja.json b/frontend/src/locales/ja.json index 9d037f86af..83a5bc9dcd 100644 --- a/frontend/src/locales/ja.json +++ b/frontend/src/locales/ja.json @@ -4,6 +4,10 @@ "banner.button.agree": "承認する", "banner.privacyPolicy": "プライバシーポリシー", "banner.text": "本ウェブサイトでは、あなたの閲覧状況やトラフィックの利用状況等を計測するため、cookieあるいは類似の技術を利用しています。私達が利用するデータの利用法や用途についてはこちら {link} を御覧ください。\"承認する/I Agree\" をクリックすることで、cookieの利用に同意したとみなされます。", + "comment.input.imageUpload.error": "画像のアップロードに失敗しました。", + "comment.input.imageUpload.progress": "", + "comment.hashtags.help.managers": "", + "comment.hashtags.help.author": "", "mytasks.mainSection.title": "自分のタスク", "mytasks.contribution": "協力", "mytasks.filter.all": "すべて", @@ -276,8 +280,6 @@ "project.detail.sections.overview": "概要", "project.detail.sections.description": "解説", "project.detail.sections.questionsAndComments": "質問とコメント", - "project.detail.sections.questionsAndComments.help.managers": "", - "project.detail.sections.questionsAndComments.help.author": "", "project.detail.sections.contributions": "協力", "project.detail.sections.contributionsTimeline": "マッピング結果のタイムライン", "project.detail.sections.contributions.osmcha": "OSMChaで変更セットを表示", @@ -532,7 +534,7 @@ "project.selectTask.footer.button.mapSelectedTask": "選択したタスクをマッピング", "project.selectTask.footer.button.mapAnotherTask": "他のタスクをマッピング", "project.selectTask.footer.button.validateRandomTask": "タスクの検証", - "project.selectTask.footer.button.validateSelectedTask": "選択したタスクの検証", + "project.selectTask.footer.button.validateSelectedTask": "{number, plural, other {}}", "project.selectTask.footer.button.validateAnotherTask": "他のタスクを検証", "project.selectTask.footer.button.selectAnotherProject": "他のプロジェクトを選択", "project.selectTask.footer.button.resumeMapping": "マッピングの再開", @@ -711,8 +713,10 @@ "user.notifications.mentions": "", "user.notifications.mentions.description": "", "user.notifications.projects": "プロジェクト更新", + "user.notifications.tasks": "", "user.settings.required": "必須項目", "user.notifications.projects.description": "あなたが参加したプロジェクトが進行すると通知を受け取ります。", + "user.notifications.task.description": "", "user.notifications.comments": "コメント", "user.notifications.comments.description": "", "user.settings.become_validator.button": "解説", diff --git a/frontend/src/locales/mg.json b/frontend/src/locales/mg.json index c39163aff7..f6c337cbce 100644 --- a/frontend/src/locales/mg.json +++ b/frontend/src/locales/mg.json @@ -4,6 +4,10 @@ "banner.button.agree": "", "banner.privacyPolicy": "", "banner.text": "", + "comment.input.imageUpload.error": "", + "comment.input.imageUpload.progress": "", + "comment.hashtags.help.managers": "", + "comment.hashtags.help.author": "", "mytasks.mainSection.title": "", "mytasks.contribution": "", "mytasks.filter.all": "Izy rehetra", @@ -276,8 +280,6 @@ "project.detail.sections.overview": "", "project.detail.sections.description": "Fanoritsoritana", "project.detail.sections.questionsAndComments": "", - "project.detail.sections.questionsAndComments.help.managers": "", - "project.detail.sections.questionsAndComments.help.author": "", "project.detail.sections.contributions": "Fandraisana anjara", "project.detail.sections.contributionsTimeline": "", "project.detail.sections.contributions.osmcha": "", @@ -532,7 +534,7 @@ "project.selectTask.footer.button.mapSelectedTask": "", "project.selectTask.footer.button.mapAnotherTask": "", "project.selectTask.footer.button.validateRandomTask": "", - "project.selectTask.footer.button.validateSelectedTask": "", + "project.selectTask.footer.button.validateSelectedTask": "{number, plural, one {} other {}}", "project.selectTask.footer.button.validateAnotherTask": "", "project.selectTask.footer.button.selectAnotherProject": "", "project.selectTask.footer.button.resumeMapping": "", @@ -711,8 +713,10 @@ "user.notifications.mentions": "", "user.notifications.mentions.description": "", "user.notifications.projects": "", + "user.notifications.tasks": "", "user.settings.required": "", "user.notifications.projects.description": "", + "user.notifications.task.description": "", "user.notifications.comments": "", "user.notifications.comments.description": "", "user.settings.become_validator.button": "", diff --git a/frontend/src/locales/ml.json b/frontend/src/locales/ml.json index 9c84a50c1b..6dc27797ce 100644 --- a/frontend/src/locales/ml.json +++ b/frontend/src/locales/ml.json @@ -4,6 +4,10 @@ "banner.button.agree": "", "banner.privacyPolicy": "", "banner.text": "", + "comment.input.imageUpload.error": "", + "comment.input.imageUpload.progress": "", + "comment.hashtags.help.managers": "", + "comment.hashtags.help.author": "", "mytasks.mainSection.title": "", "mytasks.contribution": "", "mytasks.filter.all": "", @@ -276,8 +280,6 @@ "project.detail.sections.overview": "", "project.detail.sections.description": "", "project.detail.sections.questionsAndComments": "", - "project.detail.sections.questionsAndComments.help.managers": "", - "project.detail.sections.questionsAndComments.help.author": "", "project.detail.sections.contributions": "", "project.detail.sections.contributionsTimeline": "", "project.detail.sections.contributions.osmcha": "", @@ -532,7 +534,7 @@ "project.selectTask.footer.button.mapSelectedTask": "", "project.selectTask.footer.button.mapAnotherTask": "", "project.selectTask.footer.button.validateRandomTask": "", - "project.selectTask.footer.button.validateSelectedTask": "", + "project.selectTask.footer.button.validateSelectedTask": "{number, plural, one {} other {}}", "project.selectTask.footer.button.validateAnotherTask": "", "project.selectTask.footer.button.selectAnotherProject": "", "project.selectTask.footer.button.resumeMapping": "", @@ -711,8 +713,10 @@ "user.notifications.mentions": "", "user.notifications.mentions.description": "", "user.notifications.projects": "", + "user.notifications.tasks": "", "user.settings.required": "", "user.notifications.projects.description": "", + "user.notifications.task.description": "", "user.notifications.comments": "", "user.notifications.comments.description": "", "user.settings.become_validator.button": "", diff --git a/frontend/src/locales/nl_NL.json b/frontend/src/locales/nl_NL.json index 682ce63a5f..af1cd7771e 100644 --- a/frontend/src/locales/nl_NL.json +++ b/frontend/src/locales/nl_NL.json @@ -4,6 +4,10 @@ "banner.button.agree": "Ik ga akkoord", "banner.privacyPolicy": "privacy-beleid", "banner.text": "We gebruiken cookies en soortgelijke technologieën om u w bezoeken te herkennen en te analyseren, en gebruik en activiteiten te meten. U kunt zien hoe wij de gegevens over uw bezoek of informatie die u verschaft gebruiken door onze {link} te lezen. Door te klikken op \"Ik ga akkoord\" geeft u toestemming voor het gebruiken van cookies.", + "comment.input.imageUpload.error": "Uploaden van de afbeelding is mislukt.", + "comment.input.imageUpload.progress": "", + "comment.hashtags.help.managers": "", + "comment.hashtags.help.author": "", "mytasks.mainSection.title": "Mijn taken", "mytasks.contribution": "Bijdragen", "mytasks.filter.all": "Alle", @@ -276,8 +280,6 @@ "project.detail.sections.overview": "Overzicht", "project.detail.sections.description": "Omschrijving", "project.detail.sections.questionsAndComments": "Vragen en opmerkingen", - "project.detail.sections.questionsAndComments.help.managers": "", - "project.detail.sections.questionsAndComments.help.author": "", "project.detail.sections.contributions": "Bijdragen", "project.detail.sections.contributionsTimeline": "Tijdlijn bijdragen", "project.detail.sections.contributions.osmcha": "Wijzigingensets in OSMCha", @@ -532,7 +534,7 @@ "project.selectTask.footer.button.mapSelectedTask": "Geselecteerde taak in kaart brengen", "project.selectTask.footer.button.mapAnotherTask": "Een andere taak in kaart brengen", "project.selectTask.footer.button.validateRandomTask": "Een taak valideren", - "project.selectTask.footer.button.validateSelectedTask": "Geselecteerde taak valideren", + "project.selectTask.footer.button.validateSelectedTask": "{number, plural, one {} other {}}", "project.selectTask.footer.button.validateAnotherTask": "Een andere taak valideren", "project.selectTask.footer.button.selectAnotherProject": "Een ander project selecteren", "project.selectTask.footer.button.resumeMapping": "Doorgaan met in kaart brengen", @@ -711,8 +713,10 @@ "user.notifications.mentions": "Meldingen e-mails", "user.notifications.mentions.description": "Ontvang een e-mail, elke keer als uw gebruikersnaam wordt vermeld in een opmerking.", "user.notifications.projects": "Project updates", + "user.notifications.tasks": "", "user.settings.required": "Vereiste velden", "user.notifications.projects.description": "U krijgt een notificatie als een project, waar u aan heeft bijgedragen, voortgang boekt.", + "user.notifications.task.description": "", "user.notifications.comments": "Opmerkingen", "user.notifications.comments.description": "Ontvang een notificatie, elke keer als iemand een opmerking toevoegt aan een project, waar u aan heeft bijgedragen.", "user.settings.become_validator.button": "Leren hoe", diff --git a/frontend/src/locales/pt.json b/frontend/src/locales/pt.json index 6a2b6b6d34..47deb6f131 100644 --- a/frontend/src/locales/pt.json +++ b/frontend/src/locales/pt.json @@ -4,6 +4,10 @@ "banner.button.agree": "Concordo", "banner.privacyPolicy": "política de privacidade", "banner.text": "Usamos cookies e tecnologias semelhantes para reconhecer e analisar as tuas visitas e medir a utilização e a atividade do tráfego na rede. Podes saber como usamos os dados sobre a tua visita ou as informações que forneces consultando {link}. Ao clicares em “Concordo”, aceitas o uso de cookies.", + "comment.input.imageUpload.error": "Falha no envio da imagem.", + "comment.input.imageUpload.progress": "", + "comment.hashtags.help.managers": "Adiciona \"{hashtag}\" para notificar os gestores do projeto sobre o teu comentário.", + "comment.hashtags.help.author": "Adiciona \"{hashtag}\" para notificar o autor do projeto sobre o teu comentário.", "mytasks.mainSection.title": "As Minhas Tarefas", "mytasks.contribution": "Contribuição", "mytasks.filter.all": "Todas", @@ -132,7 +136,7 @@ "project.typesOfMapping.landUse": "Uso do solo", "project.typesOfMapping.waterways": "Cursos de água", "project.typesOfMapping.other": "Outro", - "project.typesOfMapping.pointsOfInterest": "", + "project.typesOfMapping.pointsOfInterest": "Pontos de interesse", "notifications.mainSection.title": "Notificações", "notifications.filter.all": "Todas", "notifications.filter.messages": "Mensagens", @@ -144,8 +148,8 @@ "notifications.navFilters.error": "Erro ao carregar o {xWord} para {yWord}", "notifications.navFilters.error.simple": "Erro ao carregar o {xWord}", "notifications.pagination.count": "A mostrar {number} de {total}", - "notifications.nav.new.one": "1 nova mensagem", - "notifications.nav.new.plural": "{n} novas mensagens", + "notifications.nav.new.one": "1 mensagem nova", + "notifications.nav.new.plural": "{n} mensagens novas", "notifications.nav.viewAll": "Ver todas", "notifications.nav.goToNotifications": "Ir para notificações", "notifications.nav.noUnread": "Sem mensagens por ler", @@ -172,7 +176,7 @@ "project.card.lastContribution": "Última contribuição", "project.card.percentMapped": "{n}% mapeado", "project.card.percentValidated": "{n}% validado", - "project.card.percentBadImagery": "{n}% imagens ruins", + "project.card.percentBadImagery": "{n}% imagens de má qualidade", "project.card.projectPriorityUrgent": "Urgente", "project.card.projectPriorityHigh": "Alta", "project.card.projectPriorityMedium": "Média", @@ -276,12 +280,10 @@ "project.detail.sections.overview": "Visão geral", "project.detail.sections.description": "Descrição", "project.detail.sections.questionsAndComments": "Perguntas e comentários", - "project.detail.sections.questionsAndComments.help.managers": "", - "project.detail.sections.questionsAndComments.help.author": "", "project.detail.sections.contributions": "Contribuições", "project.detail.sections.contributionsTimeline": "Cronograma de contribuições", "project.detail.sections.contributions.osmcha": "Conjuntos de alterações no OSMCha", - "project.detail.sections.contributions.changesets": "", + "project.detail.sections.contributions.changesets": "Conjuntos de alterações", "project.detail.sections.contributors": "Contribuidores", "project.detail.sections.relatedProjects": "Projetos relacionados", "project.detail.sections.contributions.timelineError": "Haverá um cronograma disponível após o mapeamento da primeira tarefa.", @@ -294,7 +296,7 @@ "project.permissions.teamsAndLevel": "Membros intermediários e avançados de {team}", "projects.data.download.aoi": "Transferir AOI", "projects.data.download.taskGrid": "Transferir Grelha de Tarefas", - "projects.link.stats": "", + "projects.link.stats": "Mais estatísticas", "project.share.twitter": "Contribui com o mapeamento do projeto #{id} em {site}", "project.share.facebook": "Publicar no Facebook", "project.share.linkedin": "Partilhar no LinkedIn", @@ -338,7 +340,7 @@ "projects.actions.validate_all_tasks.confirmation": "Tens a certeza de que queres validar todas as tarefas? Não poderás desfazer esta ação.", "projects.actions.validate_all_tasks.description": "Isto irá mudar o estado de todas as tarefas (exceto as “indisponíveis”) para “concluídas”. Usa isto apenas se tiveres a certeza do que estás a fazer.", "projects.actions.validate_all_tasks.success": "As tarefas foram validadas com êxito.", - "projects.actions.validate_all_tasks.error": "", + "projects.actions.validate_all_tasks.error": "A validação de todas as tarefas falhou por um motivo desconhecido.", "projects.actions.reset_bad_imagery_tasks.title": "Repor tarefas marcadas como indisponíveis", "projects.actions.reset_bad_imagery_tasks.confirmation": "Tens a certeza de que queres repor todas as tarefas marcadas como indisponíveis neste projeto? Não poderás desfazer esta ação.", "projects.actions.reset_bad_imagery_tasks.description": "Isto irá mudar o estado destas tarefas para “prontas para mapeamento”. Usa isto apenas se tiveres a certeza do que estás a fazer.", @@ -467,18 +469,18 @@ "project.nav.apply": "Aplicar", "project.nav.clear": "Limpar", "project.results.retry": "Repetir", - "projects.stats.title": "", - "projects.stats.average_mapping_time": "", - "projects.stats.average_validation_time": "", - "projects.stats.time_finish_mapping": "", - "projects.stats.time_finish_validation": "", - "project.stats.tasks.tatus": "", - "project.stats.tasks.needs_mapping": "", - "project.stats.tasks.needs_validation": "", + "projects.stats.title": "Estatísticas de tempo", + "projects.stats.average_mapping_time": "Tempo médio de mapeamento", + "projects.stats.average_validation_time": "Tempo médio de validação", + "projects.stats.time_finish_mapping": "Tempo para concluir mapeamento", + "projects.stats.time_finish_validation": "Tempo para concluir validação", + "project.stats.tasks.tatus": "Tarefas por estado", + "project.stats.tasks.needs_mapping": "Tarefas para mapear", + "project.stats.tasks.needs_validation": "Tarefas para validar", "project.stats.contributors.title": "Contribuidores", - "project.stats.contributors.total": "", - "project.stats.experience.title": "", - "project.stats.level.title": "", + "project.stats.contributors.total": "Número total de contribuidores", + "project.stats.experience.title": "Utilizadores por experiência no Gestor de Tarefas", + "project.stats.level.title": "Utilizadores por nível", "project.stats.contributors.mappers": "Mapeadores", "project.stats.contributors.validators": "Validadores", "project.stats.contributors.experience.1": "Menos de 1 mês", @@ -532,7 +534,7 @@ "project.selectTask.footer.button.mapSelectedTask": "Mapear tarefa selecionada", "project.selectTask.footer.button.mapAnotherTask": "Mapear outra tarefa", "project.selectTask.footer.button.validateRandomTask": "Validar uma tarefa", - "project.selectTask.footer.button.validateSelectedTask": "Validar tarefa selecionada", + "project.selectTask.footer.button.validateSelectedTask": "{number, plural, one {Validar tarefa selecionada} other {Validar # tarefas selecionadas}}", "project.selectTask.footer.button.validateAnotherTask": "Validar outra tarefa", "project.selectTask.footer.button.selectAnotherProject": "Selecionar outro projeto", "project.selectTask.footer.button.resumeMapping": "Retomar mapeamento", @@ -617,7 +619,7 @@ "project.level.intermediate": "Intermédio", "project.level.beginner": "Iniciado", "project.level.new_users": "Novos utilizadores", - "project.contributions.stats": "", + "project.contributions.stats": "Estatísticas", "project.permissions.error.title": "Não tens os requisitos para trabalhar neste projeto...", "project.permissions.error.userLevelToValidate": "Somente utilizadores com nível de experiência intermediário ou avançado podem validar este projeto.", "project.permissions.error.userLevelToMap": "Somente utilizadores com nível de experiência intermediário ou avançado podem mapear este projeto.", @@ -708,13 +710,15 @@ "user.settings.language.description": "Define o teu idioma preferido. Isto também afetará o idioma dos mapas que verás no Gestor de Tarefas.", "user.settings.become_validator": "Torna-te um validador", "user.settings.become_validator.description": "Os validadores verificam a qualidade das edições concluídas e fornecem informação aos mapeadores sobre como melhorar o mapeamento. Se fores um mapeador experiente, pede para te tornares um validador.", - "user.notifications.mentions": "", - "user.notifications.mentions.description": "", + "user.notifications.mentions": "E-mails de menções", + "user.notifications.mentions.description": "Receber um e-mail sempre que o teu nome de utilizador for mencionado num comentário.", "user.notifications.projects": "Atualizações do projeto", + "user.notifications.tasks": "", "user.settings.required": "Campos obrigatórios", "user.notifications.projects.description": "Recebes uma notificação quando um projeto no qual contribuíste for atualizado.", + "user.notifications.task.description": "", "user.notifications.comments": "Comentários", - "user.notifications.comments.description": "", + "user.notifications.comments.description": "Receber uma notificação sempre que alguém publicar um comentário em projetos ou tarefas onde contribuíste.", "user.settings.become_validator.button": "Vê como", "user.welcome.title": "Bem-vind@ ao Gestor de Tarefas!", "user.interests.update.success": "Interesses atualizados com sucesso.", @@ -778,11 +782,11 @@ "users.detail.validated": "Validou", "users.detail.finished": "Finalizou", "users.detail.topProjectsMappedTitle": "Os 5 projetos onde mais colaborou", - "users.detail.topProjectsMapped.error": "", + "users.detail.topProjectsMapped.error": "Não há projetos mapeados.", "users.detail.topCausesTitle": "Principais causas em que contribuiu", - "users.detail.projects.noData": "", + "users.detail.projects.noData": "Não há informação disponível, porque ainda não se mapeou nenhum projeto.", "users.detail.editsTitle": "Edições em números", - "users.detail.edits.error": "", + "users.detail.edits.error": "Ainda não há dados para mostrar. As edições no OpenStreetMap são atualizadas com um atraso de 1 hora.", "users.detail.topCountriesTitle": "Os 5 países onde mais mapeou", "users.detail.tasks": "tarefas", "users.detail.heatmapNoContribution": "Sem contributos", @@ -795,7 +799,7 @@ "notFound.page.title": "Página não encontrada", "notFound.project.title": "Projeto {id} não encontrado", "notFound.lead": "Confere o URL ou comunica este erro.", - "project.stats.timeline": "", + "project.stats.timeline": "Linha cronológica", "management.forbiddenAccess.title": "Não tens permissão para aceder à área de gestão.", "teamsAndOrgs.management.project.forbidden": "Não tens permissão para editar este projeto.", "teamsAndOrgs.management.team.forbidden": "Não tens permissão para editar esta equipa.", @@ -863,8 +867,8 @@ "pages.learn.videos.map_roads.description": "Aprende a mapear estradas no OpenStreetMap.", "pages.learn.videos.map_buildings.title": "Selecionar uma tarefa e mapear edifícios", "pages.learn.videos.map_buildings.description": "Aprende a mapear edifícios no OpenStreetMap.", - "pages.learn.validate.video.title": "", - "pages.learn.validate.video.description": "", + "pages.learn.validate.video.title": "Como validar", + "pages.learn.validate.video.description": "Aprende a validar projetos no Gestor de Tarefas", "pages.learn.validate_title": "Aprender a validar", "pages.learn.validate.intro": "A validação é uma parte importante do processo. Requer confiança nas tuas habilidades de mapeamento, bem como vontade de ajudar a treinar e aconselhar mapeadores mais inexperientes.", "pages.learn.validate.description": "Ter um segundo, terceiro ou quarto par de olhos sobre elementos mapeados é um passo importante para garantir a qualidade dos dados que estão a ser adicionados ao OpenStreetMap e que serão usados em todo o mundo.", @@ -912,10 +916,10 @@ "pages.about.floss.description": "O Gestor de Tarefas é um software livre e de código aberto desenvolvido pelo {hotLink}. O código da aplicação pode ser consultado em {code}, onde também poderás relatar problemas e contribuir.", "pages.edit_project.title": "Editar projeto", "pages.edit_project.buttons.save": "Guardar", - "pages.edit_project.buttons.access_project": "", - "pages.edit_project.buttons.project_page": "", - "pages.edit_project.buttons.task_selection_page": "", - "pages.edit_project.buttons.project_stats": "", + "pages.edit_project.buttons.access_project": "Aceder ao projeto", + "pages.edit_project.buttons.project_page": "Página do projeto", + "pages.edit_project.buttons.task_selection_page": "Página de seleção de tarefa", + "pages.edit_project.buttons.project_stats": "Estatísticas do projeto", "pages.edit_project.actions.update.success": "Projeto atualizado com sucesso.", "pages.edit_project.actions.update.error": "Houve uma falha ao guardar o projeto devido a um erro no servidor. Por favor, volta a tentar mais tarde ou contacta o administrador se o problema persistir.", "pages.edit_project.actions.missing_fields": "Houve uma falha ao guardar o projeto, porque tens de fornecer informações para estes campos obrigatórios:", @@ -929,5 +933,5 @@ "pages.edit_project.sections.settings": "Definições", "pages.edit_project.sections.actions": "Ações", "pages.edit_project.sections.custom_editor": "Editor Personalizado", - "pages.create_campaign.duplicate": "" + "pages.create_campaign.duplicate": "Já existe uma campanha com o mesmo nome" } \ No newline at end of file diff --git a/frontend/src/locales/pt_BR.json b/frontend/src/locales/pt_BR.json index c2e249da66..7d6e09bab2 100644 --- a/frontend/src/locales/pt_BR.json +++ b/frontend/src/locales/pt_BR.json @@ -4,6 +4,10 @@ "banner.button.agree": "Eu concordo", "banner.privacyPolicy": "política de privacidade", "banner.text": "Utilizamos cookies e tecnologias semelhantes para reconhecer e analisar as suas visitas e medir a utilização e a atividade do tráfego. Você pode saber como usamos os dados sobre sua visita ou informações fornecidas lendo nosso {link}. Ao clicar em “Concordo”, você aprova o uso de cookies.", + "comment.input.imageUpload.error": "Falha no upload da imagem.", + "comment.input.imageUpload.progress": "Enviando arquivo...", + "comment.hashtags.help.managers": "Adicione “{hashtag}” para notificar os/as administradores do projeto sobre seu comentário.", + "comment.hashtags.help.author": "Adicione “{hashtag}” para notificar o/a autor(a) do projeto sobre seu comentário.", "mytasks.mainSection.title": "Minhas Tarefas", "mytasks.contribution": "Contribuição", "mytasks.filter.all": "Todos", @@ -276,8 +280,6 @@ "project.detail.sections.overview": "Visão geral", "project.detail.sections.description": "Descrição", "project.detail.sections.questionsAndComments": "Perguntas e comentários", - "project.detail.sections.questionsAndComments.help.managers": "Adicione “{hashtag}” para notificar os/as administradores do projeto sobre seu comentário.", - "project.detail.sections.questionsAndComments.help.author": "Adicione “{hashtag}” para notificar o/a autor(a) do projeto sobre seu comentário.", "project.detail.sections.contributions": "Contribuições", "project.detail.sections.contributionsTimeline": "Cronograma de contribuições", "project.detail.sections.contributions.osmcha": "Conjuntos de alterações no OSMCha", @@ -532,7 +534,7 @@ "project.selectTask.footer.button.mapSelectedTask": "Mapear a tarefa selecionada", "project.selectTask.footer.button.mapAnotherTask": "Mapear outra tarefa", "project.selectTask.footer.button.validateRandomTask": "Validar uma tarefa", - "project.selectTask.footer.button.validateSelectedTask": "Validar tarefa selecionada", + "project.selectTask.footer.button.validateSelectedTask": "{number, plural, one {Validar tarefa selecionada} other {Validar # tarefas selecionadas}}", "project.selectTask.footer.button.validateAnotherTask": "Validar outra tarefa", "project.selectTask.footer.button.selectAnotherProject": "Selecione outro projeto", "project.selectTask.footer.button.resumeMapping": "Continuar mapeamento", @@ -711,8 +713,10 @@ "user.notifications.mentions": "Receber menções por email", "user.notifications.mentions.description": "Receba um e-mail sempre que seu nome de usuário for mencionado em um comentário.", "user.notifications.projects": "Atualizações do projeto", + "user.notifications.tasks": "Emails de validação de tarefas", "user.settings.required": "Campos obrigatórios", "user.notifications.projects.description": "Você recebe uma notificação quando um projeto para o qual você contribuiu faz progresso.", + "user.notifications.task.description": "Receba um e-mail quando uma tarefa para a qual você contribuiu for validada.", "user.notifications.comments": "Comentários", "user.notifications.comments.description": "Receba uma notificação sempre que alguém postar um comentário sobre projetos ou tarefas para as quais você contribuiu.", "user.settings.become_validator.button": "Saiba como", diff --git a/frontend/src/locales/sv.json b/frontend/src/locales/sv.json index 1d90416730..4e9e9628bb 100644 --- a/frontend/src/locales/sv.json +++ b/frontend/src/locales/sv.json @@ -4,6 +4,10 @@ "banner.button.agree": "", "banner.privacyPolicy": "", "banner.text": "", + "comment.input.imageUpload.error": "", + "comment.input.imageUpload.progress": "", + "comment.hashtags.help.managers": "", + "comment.hashtags.help.author": "", "mytasks.mainSection.title": "Mina uppgifter", "mytasks.contribution": "Bidrag", "mytasks.filter.all": "Alla", @@ -276,8 +280,6 @@ "project.detail.sections.overview": "Översikt", "project.detail.sections.description": "Beskrivning", "project.detail.sections.questionsAndComments": "Frågor och kommentarer", - "project.detail.sections.questionsAndComments.help.managers": "", - "project.detail.sections.questionsAndComments.help.author": "", "project.detail.sections.contributions": "Bidrag", "project.detail.sections.contributionsTimeline": "Tidslinje för bidrag", "project.detail.sections.contributions.osmcha": "Ändringar i OSMCha", @@ -532,7 +534,7 @@ "project.selectTask.footer.button.mapSelectedTask": "Kartera vald uppgift", "project.selectTask.footer.button.mapAnotherTask": "Kartera en annan uppgift", "project.selectTask.footer.button.validateRandomTask": "Validera en uppgift", - "project.selectTask.footer.button.validateSelectedTask": "Validera vald uppgift", + "project.selectTask.footer.button.validateSelectedTask": "{number, plural, one {} other {}}", "project.selectTask.footer.button.validateAnotherTask": "Validera en annan uppgift", "project.selectTask.footer.button.selectAnotherProject": "Välj ett annat projekt", "project.selectTask.footer.button.resumeMapping": "Återuppta kartläggning", @@ -711,8 +713,10 @@ "user.notifications.mentions": "", "user.notifications.mentions.description": "", "user.notifications.projects": "", + "user.notifications.tasks": "", "user.settings.required": "", "user.notifications.projects.description": "", + "user.notifications.task.description": "", "user.notifications.comments": "Kommentarer", "user.notifications.comments.description": "", "user.settings.become_validator.button": "", diff --git a/frontend/src/locales/sw.json b/frontend/src/locales/sw.json index adda3bced7..a799e093e6 100644 --- a/frontend/src/locales/sw.json +++ b/frontend/src/locales/sw.json @@ -4,6 +4,10 @@ "banner.button.agree": "", "banner.privacyPolicy": "", "banner.text": "", + "comment.input.imageUpload.error": "", + "comment.input.imageUpload.progress": "", + "comment.hashtags.help.managers": "", + "comment.hashtags.help.author": "", "mytasks.mainSection.title": "Kazi Zangu", "mytasks.contribution": "Mchango ", "mytasks.filter.all": "Zote", @@ -276,8 +280,6 @@ "project.detail.sections.overview": "Muhtasari", "project.detail.sections.description": "Maelezo", "project.detail.sections.questionsAndComments": "Maswali na maoni", - "project.detail.sections.questionsAndComments.help.managers": "", - "project.detail.sections.questionsAndComments.help.author": "", "project.detail.sections.contributions": "Michango", "project.detail.sections.contributionsTimeline": "Nyakati ya michango", "project.detail.sections.contributions.osmcha": "", @@ -532,7 +534,7 @@ "project.selectTask.footer.button.mapSelectedTask": "Chora kazi iliyochaguliwa", "project.selectTask.footer.button.mapAnotherTask": "Chora kazi nyingine", "project.selectTask.footer.button.validateRandomTask": "Thibitisha Kazi", - "project.selectTask.footer.button.validateSelectedTask": "Thibitisha chumba kilichochaguliwa", + "project.selectTask.footer.button.validateSelectedTask": "{number, plural, one {} other {}}", "project.selectTask.footer.button.validateAnotherTask": "Thibitisha kazi nyingine", "project.selectTask.footer.button.selectAnotherProject": "Chagua mradi mwingine", "project.selectTask.footer.button.resumeMapping": "Endelea kuchora", @@ -711,8 +713,10 @@ "user.notifications.mentions": "", "user.notifications.mentions.description": "", "user.notifications.projects": "Sasisho za mradi", + "user.notifications.tasks": "", "user.settings.required": "", "user.notifications.projects.description": "Unapata taarifa pale mradi uliochangia unapopata maendeleo.", + "user.notifications.task.description": "", "user.notifications.comments": "Maoni", "user.notifications.comments.description": "", "user.settings.become_validator.button": "Jifunze jinsi", diff --git a/frontend/src/locales/tl.json b/frontend/src/locales/tl.json index 9c84a50c1b..6dc27797ce 100644 --- a/frontend/src/locales/tl.json +++ b/frontend/src/locales/tl.json @@ -4,6 +4,10 @@ "banner.button.agree": "", "banner.privacyPolicy": "", "banner.text": "", + "comment.input.imageUpload.error": "", + "comment.input.imageUpload.progress": "", + "comment.hashtags.help.managers": "", + "comment.hashtags.help.author": "", "mytasks.mainSection.title": "", "mytasks.contribution": "", "mytasks.filter.all": "", @@ -276,8 +280,6 @@ "project.detail.sections.overview": "", "project.detail.sections.description": "", "project.detail.sections.questionsAndComments": "", - "project.detail.sections.questionsAndComments.help.managers": "", - "project.detail.sections.questionsAndComments.help.author": "", "project.detail.sections.contributions": "", "project.detail.sections.contributionsTimeline": "", "project.detail.sections.contributions.osmcha": "", @@ -532,7 +534,7 @@ "project.selectTask.footer.button.mapSelectedTask": "", "project.selectTask.footer.button.mapAnotherTask": "", "project.selectTask.footer.button.validateRandomTask": "", - "project.selectTask.footer.button.validateSelectedTask": "", + "project.selectTask.footer.button.validateSelectedTask": "{number, plural, one {} other {}}", "project.selectTask.footer.button.validateAnotherTask": "", "project.selectTask.footer.button.selectAnotherProject": "", "project.selectTask.footer.button.resumeMapping": "", @@ -711,8 +713,10 @@ "user.notifications.mentions": "", "user.notifications.mentions.description": "", "user.notifications.projects": "", + "user.notifications.tasks": "", "user.settings.required": "", "user.notifications.projects.description": "", + "user.notifications.task.description": "", "user.notifications.comments": "", "user.notifications.comments.description": "", "user.settings.become_validator.button": "", diff --git a/frontend/src/locales/tr.json b/frontend/src/locales/tr.json index 09cbc935f3..9e64e6b12e 100644 --- a/frontend/src/locales/tr.json +++ b/frontend/src/locales/tr.json @@ -4,6 +4,10 @@ "banner.button.agree": "Onaylıyorum", "banner.privacyPolicy": "gizlilik politikası", "banner.text": "Ziyaretlerinizi tanıyıp analiz etmek ve trafik kullanımı ile aktiviteleri ölçmek için çerezler ve benzeri teknolojiler kullanıyoruz. Ziyaretiniz hakkındaki verileri ve sizin sağladığınız bilgileri nasıl kullandığımızı, {link} sayfasını okuyarak öğrenebilirsiniz. \"Onaylıyorum\" seçeneğini tıklayarak çerez kullanımına izin vermiş olacaksınız.", + "comment.input.imageUpload.error": "Görüntü yükleme başarısız oldu.", + "comment.input.imageUpload.progress": "", + "comment.hashtags.help.managers": "", + "comment.hashtags.help.author": "", "mytasks.mainSection.title": "Görevlerim", "mytasks.contribution": "Katkı", "mytasks.filter.all": "Hepsi", @@ -276,8 +280,6 @@ "project.detail.sections.overview": "Genel Bakış", "project.detail.sections.description": "Tanım", "project.detail.sections.questionsAndComments": "Sorular ve yorumlar", - "project.detail.sections.questionsAndComments.help.managers": "", - "project.detail.sections.questionsAndComments.help.author": "", "project.detail.sections.contributions": "Katkılar", "project.detail.sections.contributionsTimeline": "Katkı zaman çizelgesi", "project.detail.sections.contributions.osmcha": "OSMCha'daki değişiklikler", @@ -532,7 +534,7 @@ "project.selectTask.footer.button.mapSelectedTask": "Seçili görevi haritala", "project.selectTask.footer.button.mapAnotherTask": "Başka görev haritala", "project.selectTask.footer.button.validateRandomTask": "Bir görevi doğrula", - "project.selectTask.footer.button.validateSelectedTask": "Seçili görevi doğrula", + "project.selectTask.footer.button.validateSelectedTask": "{number, plural, one {} other {}}", "project.selectTask.footer.button.validateAnotherTask": "Başka görev doğrula", "project.selectTask.footer.button.selectAnotherProject": "Başka proje seç", "project.selectTask.footer.button.resumeMapping": "Haritalamaya devam et", @@ -711,8 +713,10 @@ "user.notifications.mentions": "", "user.notifications.mentions.description": "", "user.notifications.projects": "Proje güncellemeleri", + "user.notifications.tasks": "", "user.settings.required": "Gerekli alanlar", "user.notifications.projects.description": "Katkıda bulunduğunuz bir proje ilerleme kaydettiğinde bildirim alırsınız.", + "user.notifications.task.description": "", "user.notifications.comments": "Yorumlar", "user.notifications.comments.description": "", "user.settings.become_validator.button": "Nasıl?", diff --git a/frontend/src/locales/uk.json b/frontend/src/locales/uk.json index e4b5ed0d8b..b401362681 100644 --- a/frontend/src/locales/uk.json +++ b/frontend/src/locales/uk.json @@ -4,6 +4,10 @@ "banner.button.agree": "Я погоджуюсь", "banner.privacyPolicy": "політикою конфіденційності", "banner.text": "Ми використовуємо реп'яшки та подібні технології для розпізнавання та аналізу ваших відвідувань, а також для вимірювання використання трафіку та дій. Ви можете дізнатись більше про те як ми використовуємо дані про ваші відвідування або інформацію, яку ви надаєте самі, ознайомившись з нашою {link}. Натискаючи \"Я погоджуюсь\" ви надаєте згоду на використання реп'яшків.", + "comment.input.imageUpload.error": "Помилка завантаження зображення.", + "comment.input.imageUpload.progress": "", + "comment.hashtags.help.managers": "Додайте \"{hashtag}\", щоб повідомити менеджерів проєкту про ваш коментар.", + "comment.hashtags.help.author": "Додайте \"{hashtag}\", щоб повідомити автора проєкту про ваш коментар.", "mytasks.mainSection.title": "Завдання", "mytasks.contribution": "Участь", "mytasks.filter.all": "Всі", @@ -132,7 +136,7 @@ "project.typesOfMapping.landUse": "Призначення землі", "project.typesOfMapping.waterways": "Водний шлях", "project.typesOfMapping.other": "Інші", - "project.typesOfMapping.pointsOfInterest": "", + "project.typesOfMapping.pointsOfInterest": "Точки інтересу", "notifications.mainSection.title": "Повідомлення", "notifications.filter.all": "Всі", "notifications.filter.messages": "Повідомлення", @@ -169,7 +173,7 @@ "notifications.message.type.project_chat_notification": "Чат проєкту", "notifications.message.type.project_activity_notification": "Зміни проєкту", "project.card.contributorCount": "{number} всього учасників", - "project.card.lastContribution": "Останній вклад", + "project.card.lastContribution": "Останній внесок", "project.card.percentMapped": "{n}% замаплено", "project.card.percentValidated": "{n}% перевірено", "project.card.percentBadImagery": "{n}% з поганим фоном", @@ -234,7 +238,7 @@ "project.detail.coordination.description": "Координація проєкту відбувається за участі {organisation}. Проєкт був створений {user}.", "project.detail.createdBy": "Проєкт створений {user}.", "project.detail.contributorCount": "{number} всього учасників", - "project.detail.lastContribution": "Останній вклад", + "project.detail.lastContribution": "Останній внесок", "project.detail.percentMapped": "Замаплено", "project.detail.percentValidated": "Перевірено", "project.detail.projectPriorityUrgent": "Терміново", @@ -276,12 +280,10 @@ "project.detail.sections.overview": "Огляд", "project.detail.sections.description": "Опис", "project.detail.sections.questionsAndComments": "Питання та коментарі", - "project.detail.sections.questionsAndComments.help.managers": "Додайте \"{hashtag}\", щоб повідомити менеджерів проєкту про ваш коментар.", - "project.detail.sections.questionsAndComments.help.author": "Додайте \"{hashtag}\", щоб повідомити автора проєкту про ваш коментар.", "project.detail.sections.contributions": "Участь", "project.detail.sections.contributionsTimeline": "Статистика участі", "project.detail.sections.contributions.osmcha": "Набори змін в OSMCha", - "project.detail.sections.contributions.changesets": "Набіри змін", + "project.detail.sections.contributions.changesets": "Набори змін", "project.detail.sections.contributors": "Учасники", "project.detail.sections.relatedProjects": "Пов'язані проєкти", "project.detail.sections.contributions.timelineError": "Шкала з'явиться як тільки перше завдання буде замаплене.", @@ -295,7 +297,7 @@ "projects.data.download.aoi": "Завантажити зону зацікавленості проєкту", "projects.data.download.taskGrid": "Завантажити решітку завдань", "projects.link.stats": "Докладна статистика", - "project.share.twitter": "Продовжити мапінг проєкту #{id} на {site}", + "project.share.twitter": "Зробіть свій внесок в мапінг проєкту #{id} на {site}", "project.share.facebook": "Оприлюднити на Facebook", "project.share.linkedin": "Поділитись на LinkedIn", "projects.formInputs.privacy.title": "Приватність", @@ -467,13 +469,13 @@ "project.nav.apply": "Застосувати", "project.nav.clear": "Очитстити", "project.results.retry": "Повторити", - "projects.stats.title": "Витрачено часу", + "projects.stats.title": "Потрібно часу", "projects.stats.average_mapping_time": "В середньому на мапінг", "projects.stats.average_validation_time": "В середньому на перевірку", "projects.stats.time_finish_mapping": "До завершення мапінгу", "projects.stats.time_finish_validation": "До завершення перевірки", "project.stats.tasks.tatus": "Завдання за станом", - "project.stats.tasks.needs_mapping": "Завдання на мапі", + "project.stats.tasks.needs_mapping": "Завдання для мапінгу", "project.stats.tasks.needs_validation": "Завдання для перевірки", "project.stats.contributors.title": "Учасники", "project.stats.contributors.total": "Всього учасників", @@ -532,7 +534,7 @@ "project.selectTask.footer.button.mapSelectedTask": "Мапити виділене завдання", "project.selectTask.footer.button.mapAnotherTask": "Мапити інше завдання", "project.selectTask.footer.button.validateRandomTask": "Перевірити завдання", - "project.selectTask.footer.button.validateSelectedTask": "Перевірити виділене завдання", + "project.selectTask.footer.button.validateSelectedTask": "{number, plural, one {Перевірити виділене завдання} few {Перевірити # виділених завдання} many {Перевірити # виділених завдань} other {Перевірити # виділених завдань}}", "project.selectTask.footer.button.validateAnotherTask": "Перевірити інше завдання", "project.selectTask.footer.button.selectAnotherProject": "Обрати інший проєкт", "project.selectTask.footer.button.resumeMapping": "Продовжити мапінг", @@ -711,8 +713,10 @@ "user.notifications.mentions": "Листи зі згадками", "user.notifications.mentions.description": "Отримувати листа кожного разу коли вас згадуватимуть в коментарях.", "user.notifications.projects": "Оновлення проекту", + "user.notifications.tasks": "", "user.settings.required": "Обов'язкові поля", "user.notifications.projects.description": "Ви отримуватимете оповіщення з будь-яких проєктів в яких ви взяли участь у разі їх подальшого розвитку.", + "user.notifications.task.description": "", "user.notifications.comments": "Коментарі", "user.notifications.comments.description": "Отримувати повідомлення кожного разу, як хтось залишатиме коментар в проєкті чи завданні до якого ви доклали зусиль.", "user.settings.become_validator.button": "Дізнатись як", @@ -912,7 +916,7 @@ "pages.about.floss.description": "Менеджер завдань був створений {hotLink} і він є вільним програмним забезпеченням з відкритими сирцями. Сирці знаходяться на {code}, де ви можете повідомити про проблеми та зробити свій внесок у його вдосконалення.", "pages.edit_project.title": "Змінити проєкт", "pages.edit_project.buttons.save": "Зберегти", - "pages.edit_project.buttons.access_project": "Дістатись проєкту", + "pages.edit_project.buttons.access_project": "Перейти до…", "pages.edit_project.buttons.project_page": "Сторінка проєкту", "pages.edit_project.buttons.task_selection_page": "Сторінка вибору завдань", "pages.edit_project.buttons.project_stats": "Статистика проєкту", diff --git a/frontend/src/locales/zh_TW.json b/frontend/src/locales/zh_TW.json index 37333ea2d0..853e95249c 100644 --- a/frontend/src/locales/zh_TW.json +++ b/frontend/src/locales/zh_TW.json @@ -4,6 +4,10 @@ "banner.button.agree": "我同意", "banner.privacyPolicy": "隱私政策", "banner.text": "我們使用 cookies 和其他類似技術來認得和分析你的造訪,以及計算網路流量和活動。你可以閱讀我們提供的連結 {link} 來瞭解我們怎麼運用你造訪或是你提供的資料,只要點「我同意」,你就同意我們使用 cookies。", + "comment.input.imageUpload.error": "", + "comment.input.imageUpload.progress": "", + "comment.hashtags.help.managers": "", + "comment.hashtags.help.author": "", "mytasks.mainSection.title": "我的任務", "mytasks.contribution": "貢獻狀況", "mytasks.filter.all": "全部", @@ -276,8 +280,6 @@ "project.detail.sections.overview": "", "project.detail.sections.description": "描述", "project.detail.sections.questionsAndComments": "", - "project.detail.sections.questionsAndComments.help.managers": "", - "project.detail.sections.questionsAndComments.help.author": "", "project.detail.sections.contributions": "", "project.detail.sections.contributionsTimeline": "", "project.detail.sections.contributions.osmcha": "", @@ -532,7 +534,7 @@ "project.selectTask.footer.button.mapSelectedTask": "", "project.selectTask.footer.button.mapAnotherTask": "", "project.selectTask.footer.button.validateRandomTask": "", - "project.selectTask.footer.button.validateSelectedTask": "", + "project.selectTask.footer.button.validateSelectedTask": "{number, plural, other {}}", "project.selectTask.footer.button.validateAnotherTask": "", "project.selectTask.footer.button.selectAnotherProject": "", "project.selectTask.footer.button.resumeMapping": "", @@ -711,8 +713,10 @@ "user.notifications.mentions": "", "user.notifications.mentions.description": "", "user.notifications.projects": "", + "user.notifications.tasks": "", "user.settings.required": "", "user.notifications.projects.description": "", + "user.notifications.task.description": "", "user.notifications.comments": "", "user.notifications.comments.description": "", "user.settings.become_validator.button": "", diff --git a/frontend/src/utils/htmlFromMarkdown.js b/frontend/src/utils/htmlFromMarkdown.js index 06fe639e11..b463443d0d 100644 --- a/frontend/src/utils/htmlFromMarkdown.js +++ b/frontend/src/utils/htmlFromMarkdown.js @@ -18,3 +18,22 @@ export const htmlFromMarkdown = (markdownText) => { }); return { __html: DOMPurify.sanitize(marked(markdownText)) }; }; + +export const formatUserNamesToLink = (text) => { + const regex = /@\[([^\]]+)\]/gi; + // Find usernames with a regular expression. They all start with '[@' and end with ']' + const usernames = text && text.match(regex); + if (usernames) { + for (let i = 0; i < usernames.length; i++) { + // Strip off the first two characters: '@[' + let username = usernames[i].substring(2, usernames[i].length); + // Strip off the last character + username = username.substring(0, username.length - 1); + text = text.replace( + usernames[i], + `@${username}`, + ); + } + } + return text; +}; diff --git a/frontend/src/utils/tests/htmlFromMarkdown.test.js b/frontend/src/utils/tests/htmlFromMarkdown.test.js new file mode 100644 index 0000000000..e265e2912e --- /dev/null +++ b/frontend/src/utils/tests/htmlFromMarkdown.test.js @@ -0,0 +1,23 @@ +import { htmlFromMarkdown, formatUserNamesToLink } from '../htmlFromMarkdown'; + +test('htmlFromMarkdown returns correct content', () => { + expect(htmlFromMarkdown('![test](https://a.co/img.jpg)').__html).toContain( + '

    test

    ', + ); + expect(htmlFromMarkdown('[test](https://a.co/)').__html).toContain( + '

    test

    ', + ); +}); + +test('formatUserNamesToLink returns correct content', () => { + expect( + formatUserNamesToLink( + 'Hello @[test_user]! Welcome to OpenStreetMap! Contact me at test@test.com.', + ), + ).toBe( + 'Hello @test_user! Welcome to OpenStreetMap! Contact me at test@test.com.', + ); + expect(formatUserNamesToLink('Hello @test, @test2], @[test3!')).toBe( + 'Hello @test, @test2], @[test3!', + ); +}); diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 8e51c47cd2..1792a5e9a1 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -1109,10 +1109,10 @@ resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46" integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA== -"@formatjs/intl-datetimeformat@^2.3.0": - version "2.3.0" - resolved "https://registry.yarnpkg.com/@formatjs/intl-datetimeformat/-/intl-datetimeformat-2.3.0.tgz#a0e58135164f9c23db451ea0cc48c9eed0268152" - integrity sha512-wPlVtqPJjXyEIHO05JZ8G+t9fV00ho2cL8BCaDlT8vJQ1V2fnBp3cx7jlKpKCV7wNZm3yEebTATv0e0T3WisWw== +"@formatjs/intl-datetimeformat@^2.3.1": + version "2.3.1" + resolved "https://registry.yarnpkg.com/@formatjs/intl-datetimeformat/-/intl-datetimeformat-2.3.1.tgz#dd5c4cb33cee812df42dd286e88ae662743a8679" + integrity sha512-i1H3hflcILP40ETiaaWcmLOdkt/iDbfn/2Z8aCWss/IgP2b7r/mCXbgaRJh1dh6pCpmbLp5g4lpIrkvUKuhUtA== dependencies: "@formatjs/intl-getcanonicallocales" "^1.3.1" "@formatjs/intl-utils" "^3.8.2" @@ -1754,10 +1754,10 @@ "@babel/runtime" "^7.5.4" "@types/testing-library__react-hooks" "^3.3.0" -"@testing-library/react@^10.4.7": - version "10.4.7" - resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-10.4.7.tgz#fc14847fb70a5e93576b8f7f0d1490ead02a9061" - integrity sha512-hUYbum3X2f1ZKusKfPaooKNYqE/GtPiQ+D2HJaJ4pkxeNJQFVUEvAvEh9+3QuLdBeTWkDMNY5NSijc5+pGdM4Q== +"@testing-library/react@^10.4.8": + version "10.4.8" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-10.4.8.tgz#5eb730291b8fd81cdb2d8877770d060b044ae4a4" + integrity sha512-clgpFR6QHiRRcdhFfAKDhH8UXpNASyfkkANhtCsCVBnai+O+mK1rGtMES+Apc7ql5Wyxu7j8dcLiC4pV5VblHA== dependencies: "@babel/runtime" "^7.10.3" "@testing-library/dom" "^7.17.1" @@ -2852,6 +2852,11 @@ atob@^2.1.2: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== +attr-accept@^2.0.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.1.tgz#89b48de019ed4342f1865626b4389c666b3ed231" + integrity sha512-GpefLMsbH5ojNgfTW+OBin2xKzuHfyeNA+qCktzZojBhbA/lPZdCFMWdwk5ajb989Ok7ZT+EADqvW3TAFNMjhA== + autoprefixer@^9.6.1: version "9.7.6" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.7.6.tgz#63ac5bbc0ce7934e6997207d5bb00d68fa8293a4" @@ -5526,6 +5531,13 @@ file-loader@4.3.0: loader-utils "^1.2.3" schema-utils "^2.5.0" +file-selector@^0.1.12: + version "0.1.12" + resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.1.12.tgz#fe726547be219a787a9dcc640575a04a032b1fd0" + integrity sha512-Kx7RTzxyQipHuiqyZGf+Nz4vY9R1XGxuQl/hLoJwq+J4avk/9wxxgZyHKtbyIPJmbD4A66DWGYfyykWNpcYutQ== + dependencies: + tslib "^1.9.0" + file-uri-to-path@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" @@ -8431,10 +8443,10 @@ ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -msw@^0.20.1: - version "0.20.1" - resolved "https://registry.yarnpkg.com/msw/-/msw-0.20.1.tgz#34804ab934905b54a2c5189d360c44faf6801945" - integrity sha512-mrQb4LFmZ6KrADb1Hl2T4yQhxwCkhDz/HRRRLl+RgguUBJ+8aqDtXxGo23Eagw+Q4zrWIyJ//fENv4H6GBeTig== +msw@^0.20.4: + version "0.20.4" + resolved "https://registry.yarnpkg.com/msw/-/msw-0.20.4.tgz#96a2b9e08bd33814607cb5f00eb0bfab95dd36ac" + integrity sha512-L/g+LnT1w+oLrpmGJiHSInsa9scnaKffqIkGR+9s8EPVgp5PyA2Y/E1E37vJYN2xvoPToPm2Mnibr18RJCrhmQ== dependencies: "@open-draft/until" "^1.0.3" "@types/cookie" "^0.4.0" @@ -8444,7 +8456,7 @@ msw@^0.20.1: headers-utils "^1.2.0" node-fetch "^2.6.0" node-match-path "^0.4.4" - node-request-interceptor "^0.3.3" + node-request-interceptor "^0.3.4" statuses "^2.0.0" yargs "^15.4.1" @@ -8619,10 +8631,10 @@ node-releases@^1.1.52, node-releases@^1.1.53: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.53.tgz#2d821bfa499ed7c5dffc5e2f28c88e78a08ee3f4" integrity sha512-wp8zyQVwef2hpZ/dJH7SfSrIPD6YoJz6BDQDpGEkcA0s3LpAQoxBIYmfIq6QAhC1DhwsyCgTaTTcONwX8qzCuQ== -node-request-interceptor@^0.3.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/node-request-interceptor/-/node-request-interceptor-0.3.3.tgz#4227763d1d8d696fc15f7d6e1fe0ed1dd8c9d86d" - integrity sha512-NLFD4F7sB72WSVI7YYQzaRa7PkhdbXL1cPNvu2LlGkmnLbE11EEKrRmie5gDOKy3xfk22xXPk654CbiFwaBLdA== +node-request-interceptor@^0.3.4: + version "0.3.5" + resolved "https://registry.yarnpkg.com/node-request-interceptor/-/node-request-interceptor-0.3.5.tgz#4b26159617829c9a70643012c0fdc3ae4c78ae43" + integrity sha512-lBwA3v00hxCW2xhhZHZ1ab5JMHoAnzgtbeaUXqTufRh7mpAG93ZOChj0btMDB1VZd+CKhCbtigsxCjmerKa2+w== dependencies: "@open-draft/until" "^1.0.3" debug "^4.1.1" @@ -10470,6 +10482,15 @@ react-dom@^16.13.1, react-dom@^16.9.0: prop-types "^15.6.2" scheduler "^0.19.1" +react-dropzone@^11.0.2: + version "11.0.2" + resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-11.0.2.tgz#1a4084f520c2eafbeb24026760b3ee8f3759cfd3" + integrity sha512-/Wde9Il1aJ1FtWllg3N2taIeJh4aftx6UGUG8R1TmLnZit2RnDcEjcKwEEbKwgLXTTh8QQpiZWQJq45jTy1jCA== + dependencies: + attr-accept "^2.0.0" + file-selector "^0.1.12" + prop-types "^15.7.2" + react-error-overlay@^6.0.7: version "6.0.7" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.7.tgz#1dcfb459ab671d53f660a991513cb2f0a0553108" @@ -10489,12 +10510,12 @@ react-input-autosize@^2.2.2: dependencies: prop-types "^15.5.8" -react-intl@^5.4.5: - version "5.4.5" - resolved "https://registry.yarnpkg.com/react-intl/-/react-intl-5.4.5.tgz#5f81241d01da3a7a3133931606594bcea1783e74" - integrity sha512-XFs4Q70p17AVSLYBMopgNtWe537+U6SuXM1is7b9ThqZnZkAXqhJcXUA/0OsDFSIDilQ82NvVO1NAgEBXkRBFQ== +react-intl@^5.4.7: + version "5.4.7" + resolved "https://registry.yarnpkg.com/react-intl/-/react-intl-5.4.7.tgz#a9d52e8d39b4d1a71178b6d71c434c2c0d8448b2" + integrity sha512-oiR/w6KdVTFGa2LlycQYfumbbE0/YRymhmvj8diGYo3vvDjlZjjB1V/bzaGNWPhbNWwJXeMH+WHVylATHMCYbQ== dependencies: - "@formatjs/intl-datetimeformat" "^2.3.0" + "@formatjs/intl-datetimeformat" "^2.3.1" "@formatjs/intl-displaynames" "^3.1.7" "@formatjs/intl-listformat" "^4.0.0" "@formatjs/intl-numberformat" "^5.3.3" @@ -10622,10 +10643,10 @@ react-test-renderer@^16.13.1: react-is "^16.8.6" scheduler "^0.19.1" -react-tooltip@^4.2.7: - version "4.2.7" - resolved "https://registry.yarnpkg.com/react-tooltip/-/react-tooltip-4.2.7.tgz#245b26aa23bc8ca877f9086ad169207d77281894" - integrity sha512-z5T3gNplT76rkT7ZImfx/uXfBtS+x7+WK2H8MVZ5skSwETDDx0hs0+P0jwL5gYDaBvsZ7LYiCBzAOd3tN85CMA== +react-tooltip@^4.2.8: + version "4.2.8" + resolved "https://registry.yarnpkg.com/react-tooltip/-/react-tooltip-4.2.8.tgz#270858fee46fab73b66de316271aa94145f7446b" + integrity sha512-pDWa0/khTAgIfldp95tHgyuYyBhWNlfaU2LF9ubAKxpoqNe15uyf+uLlnhK/Lstb6FU8E8/SL28Wp6oEO9xw3g== dependencies: prop-types "^15.7.2" uuid "^7.0.3" diff --git a/requirements.txt b/requirements.txt index c3bce80a5f..10e47efa7a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -31,6 +31,7 @@ idna==2.10 itsdangerous==1.1.0 Jinja2==2.11.2 Mako==1.1.3 +markdown==3.2.2 MarkupSafe==1.1.1 mccabe==0.6.1 newrelic==5.14.1.144 diff --git a/tests/backend/unit/services/messaging/test_template_service.py b/tests/backend/unit/services/messaging/test_template_service.py index a422b35187..f7c0be8ce5 100644 --- a/tests/backend/unit/services/messaging/test_template_service.py +++ b/tests/backend/unit/services/messaging/test_template_service.py @@ -1,10 +1,12 @@ import unittest from backend.services.messaging.template_service import ( - templace_var_replacing, + template_var_replacing, get_template, clean_html, + format_username_link, ) +from flask import current_app class TestTemplateService(unittest.TestCase): @@ -17,7 +19,7 @@ def test_variable_replacing(self): ["[ORG_CODE]", "HOT"], ["[ORG_NAME]", "Organization Test"], ] - processed_content = templace_var_replacing(content, replace_list) + processed_content = template_var_replacing(content, replace_list) # Assert self.assertIn("test_user", processed_content) self.assertIn("http://localhost:30/verify.html#1234", processed_content) @@ -36,3 +38,16 @@ def test_clean_html(self): ), "Welcome to Tasking Manager!", ) + + def test_format_username_link(self): + base_url = current_app.config["APP_BASE_URL"] + self.assertEqual( + format_username_link("try @[yo] @[us2]! [t](http://a.c)"), + f'try @yo @us2! [t](http://a.c)', + ) + self.assertEqual( + format_username_link( + "testing @user! Write me at i@we.com [test](http://link.com)" + ), + "testing @user! Write me at i@we.com [test](http://link.com)", + )