diff --git a/atst/filters.py b/atst/filters.py index 11906c30..1d3a04aa 100644 --- a/atst/filters.py +++ b/atst/filters.py @@ -1,5 +1,6 @@ import re import datetime +from atst.utils.localization import translate from flask import current_app as app, render_template from jinja2.exceptions import TemplateNotFound @@ -108,3 +109,4 @@ def register_filters(app): app.jinja_env.filters["dateFromString"] = dateFromString app.jinja_env.filters["pageWindow"] = pageWindow app.jinja_env.filters["renderAuditEvent"] = renderAuditEvent + app.jinja_env.filters["translate"] = translate diff --git a/atst/forms/ccpo_review.py b/atst/forms/ccpo_review.py index b727cea0..08bdd78f 100644 --- a/atst/forms/ccpo_review.py +++ b/atst/forms/ccpo_review.py @@ -5,20 +5,31 @@ from wtforms.validators import Email, Optional from .forms import CacheableForm from .validators import Name, PhoneNumber +from atst.utils.localization import translate + class CCPOReviewForm(CacheableForm): comment = TextAreaField( - "Instructions or comments", - description="Provide instructions or notes for additional information that is necessary to approve the request here. The requestor may then re-submit the updated request or initiate contact outside of AT-AT if further discussion is required. This message will be shared with the person making the JEDI request..", + translate("forms.ccpo_review.comment_label"), + description=("forms.ccpo_review.comment_description"), + ) + fname_mao = StringField( + translate("forms.ccpo_review.fname_mao_label"), validators=[Optional(), Name()] + ) + lname_mao = StringField( + translate("forms.ccpo_review.lname_mao_label"), validators=[Optional(), Name()] ) - fname_mao = StringField("First Name (optional)", validators=[Optional(), Name()]) - lname_mao = StringField("Last Name (optional)", validators=[Optional(), Name()]) email_mao = EmailField( - "Mission Owner e-mail (optional)", validators=[Optional(), Email()] + translate("forms.ccpo_review.email_mao_label"), validators=[Optional(), Email()] ) phone_mao = TelField( - "Mission Owner phone number (optional)", validators=[Optional(), PhoneNumber()] + translate("forms.ccpo_review.phone_mao_label"), + validators=[Optional(), PhoneNumber()], + ) + phone_ext_mao = StringField(translate("forms.ccpo_review.phone_ext_mao_label")) + fname_ccpo = StringField( + translate("forms.ccpo_review.fname_ccpo_label"), validators=[Optional(), Name()] + ) + lname_ccpo = StringField( + translate("forms.ccpo_review.lname_ccpo_label"), validators=[Optional(), Name()] ) - phone_ext_mao = StringField("Extension (optional)") - fname_ccpo = StringField("First Name (optional)", validators=[Optional(), Name()]) - lname_ccpo = StringField("Last Name (optional)", validators=[Optional(), Name()]) diff --git a/atst/forms/edit_member.py b/atst/forms/edit_member.py index 83333632..974621c1 100644 --- a/atst/forms/edit_member.py +++ b/atst/forms/edit_member.py @@ -2,6 +2,7 @@ from flask_wtf import FlaskForm from wtforms.validators import Required from atst.forms.fields import SelectField +from atst.utils.localization import translate from .data import WORKSPACE_ROLES @@ -11,5 +12,7 @@ class EditMemberForm(FlaskForm): # that the user is a member of workspace_role = SelectField( - "Workspace Role", choices=WORKSPACE_ROLES, validators=[Required()] + translate("forms.edit_member.workspace_role_label"), + choices=WORKSPACE_ROLES, + validators=[Required()], ) diff --git a/atst/forms/edit_user.py b/atst/forms/edit_user.py index 6d406942..729f41fa 100644 --- a/atst/forms/edit_user.py +++ b/atst/forms/edit_user.py @@ -8,27 +8,34 @@ from .fields import SelectField from .forms import CacheableForm from .data import SERVICE_BRANCHES from atst.models.user import User +from atst.utils.localization import translate from .validators import Name, DateRange, PhoneNumber USER_FIELDS = { - "first_name": StringField("First Name", validators=[Name()]), - "last_name": StringField("Last Name", validators=[Name()]), + "first_name": StringField( + translate("forms.edit_user.first_name_label"), validators=[Name()] + ), + "last_name": StringField( + translate("forms.edit_user.last_name_label"), validators=[Name()] + ), "email": EmailField( - "E-mail Address", - description="Enter your preferred contact e-mail address", + translate("forms.edit_user.email_label"), + description=translate("forms.edit_user.email_description"), validators=[Email()], ), - "phone_number": TelField("Phone Number", validators=[PhoneNumber()]), + "phone_number": TelField( + translate("forms.edit_user.phone_number_label"), validators=[PhoneNumber()] + ), "phone_ext": StringField("Extension"), "service_branch": SelectField( - "Service Branch or Agency", - description="Which service or organization do you belong to within the DoD?", + translate("forms.edit_user.service_branch_label"), + description=translate("forms.edit_user.service_branch_description"), choices=SERVICE_BRANCHES, ), "citizenship": RadioField( - description="What is your citizenship status?", + description=translate("forms.edit_user.citizenship_description"), choices=[ ("United States", "United States"), ("Foreign National", "Foreign National"), @@ -36,8 +43,8 @@ USER_FIELDS = { ], ), "designation": RadioField( - "Designation of Person", - description="What is your designation within the DoD?", + translate("forms.edit_user.designation_label"), + description=translate("forms.edit_user.designation_description"), choices=[ ("military", "Military"), ("civilian", "Civilian"), @@ -45,8 +52,8 @@ USER_FIELDS = { ], ), "date_latest_training": DateField( - "Latest Information Assurance (IA) Training Completion Date", - description='To complete the training, you can find it in Information Assurance Cyber Awareness Challange website.', + translate("forms.edit_user.date_latest_training_label"), + description=translate("forms.edit_user.date_latest_training_description"), validators=[ DateRange( lower_bound=pendulum.duration(years=1), diff --git a/atst/forms/exceptions.py b/atst/forms/exceptions.py index 43fb99cf..2435e8ba 100644 --- a/atst/forms/exceptions.py +++ b/atst/forms/exceptions.py @@ -1,6 +1,9 @@ +from atst.utils.localization import translate + + class FormValidationError(Exception): - message = "Form validation failed." + message = translate("forms.exceptions.message") def __init__(self, form): self.form = form diff --git a/atst/forms/financial.py b/atst/forms/financial.py index 42d3e332..0d614997 100644 --- a/atst/forms/financial.py +++ b/atst/forms/financial.py @@ -8,6 +8,7 @@ from werkzeug.datastructures import FileStorage from .fields import NewlineListField, SelectField, NumberStringField from atst.forms.forms import CacheableForm +from atst.utils.localization import translate from .data import FUNDING_TYPES from .validators import DateRange @@ -46,24 +47,26 @@ class TaskOrderForm(CacheableForm): return valid number = StringField( - "Task Order Number associated with this request", - description="Include the original Task Order number (including the 000X at the end). Do not include any modification numbers. Note that there may be a lag between approving a task order and when it becomes available in our system.", + translate("forms.financial.number_label"), + description=translate("forms.financial.number_description"), validators=[InputRequired()], ) funding_type = SelectField( - description="What is the source of funding?", + description=translate("forms.financial.funding_type_description"), choices=FUNDING_TYPES, validators=[InputRequired()], coerce=coerce_choice, render_kw={"required": False}, ) - funding_type_other = StringField("If other, please specify") + funding_type_other = StringField( + translate("forms.financial.funding_type_other_label") + ) expiration_date = DateField( - "Task Order Expiration Date", - description="Please enter the expiration date for the Task Order", + translate("forms.financial.expiration_date_label"), + description=translate("forms.financial.expiration_date_description"), validators=[ InputRequired(), DateRange( @@ -76,51 +79,51 @@ class TaskOrderForm(CacheableForm): ) clin_0001 = NumberStringField( - "
CLIN 0001
-
Unclassified IaaS and PaaS Amount
", + translate("forms.financial.clin_0001_label"), validators=[InputRequired()], - description="Review your task order document, the amounts for each CLIN must match exactly here", + description=translate("forms.financial.clin_0001_description"), filters=[number_to_int], ) clin_0003 = NumberStringField( - "
CLIN 0003
-
Unclassified Cloud Support Package
", + translate("forms.financial.clin_0003_label"), validators=[InputRequired()], - description="Review your task order document, the amounts for each CLIN must match exactly here", + description=translate("forms.financial.clin_0003_description"), filters=[number_to_int], ) clin_1001 = NumberStringField( - "
CLIN 1001
-
Unclassified IaaS and PaaS Amount
OPTION PERIOD 1
", + translate("forms.financial.clin_1001_label"), validators=[InputRequired()], - description="Review your task order document, the amounts for each CLIN must match exactly here", + description=translate("forms.financial.clin_1001_description"), filters=[number_to_int], ) clin_1003 = NumberStringField( - "
CLIN 1003
-
Unclassified Cloud Support Package
OPTION PERIOD 1
", + translate("forms.financial.clin_1003_label"), validators=[InputRequired()], - description="Review your task order document, the amounts for each CLIN must match exactly here", + description=translate("forms.financial.clin_1003_description"), filters=[number_to_int], ) clin_2001 = NumberStringField( - "
CLIN 2001
-
Unclassified IaaS and PaaS Amount
OPTION PERIOD 2
", + translate("forms.financial.clin_2001_label"), validators=[InputRequired()], - description="Review your task order document, the amounts for each CLIN must match exactly here", + description=translate("forms.financial.clin_2001_description"), filters=[number_to_int], ) clin_2003 = NumberStringField( - "
CLIN 2003
-
Unclassified Cloud Support Package
OPTION PERIOD 2
", + translate("forms.financial.clin_2003_label"), validators=[InputRequired()], - description="Review your task order document, the amounts for each CLIN must match exactly here", + description=translate("forms.financial.clin_2003_description"), filters=[number_to_int], ) pdf = FileField( - "Upload a copy of your Task Order", + translate("forms.financial.pdf_label"), validators=[ - FileAllowed(["pdf"], "Only PDF documents can be uploaded."), + FileAllowed(["pdf"], translate("forms.financial.pdf_allowed_description")), InputRequired(), ], render_kw={"required": False}, @@ -129,42 +132,60 @@ class TaskOrderForm(CacheableForm): class RequestFinancialVerificationForm(CacheableForm): uii_ids = NewlineListField( - "Unique Item Identifier (UII)s related to your application(s) if you already have them.", - description="If you have more than one UII, place each one on a new line.", + translate("forms.financial.uii_ids_label"), + description=translate("forms.financial.uii_ids_description"), ) pe_id = StringField( - "Program Element Number", - description="PE numbers help the Department of Defense identify which offices' budgets are contributing towards this resource use.
It should be 7 digits followed by 1-3 letters, and should have a zero as the first and third digits.", + translate("forms.financial.pe_id_label"), + description=translate("forms.financial.pe_id_description"), validators=[InputRequired()], ) treasury_code = StringField( - "Program Treasury Code", - description="Program Treasury Code (or Appropriations Code) identifies resource types.
It should be a four digit or six digit number, optionally prefixed by one or more zeros.", + translate("forms.financial.treasury_code_label"), + description=translate("forms.financial.treasury_code_description"), validators=[InputRequired(), Regexp(TREASURY_CODE_REGEX)], ) ba_code = StringField( - "Program Budget Activity (BA) Code", - description="BA Code is used to identify the purposes, projects, or types of activities financed by the appropriation fund.
It should be two digits, followed by an optional letter.", + translate("forms.financial.ba_code_label"), + description=translate("forms.financial.ba_code_description"), validators=[InputRequired(), Regexp(BA_CODE_REGEX)], ) - fname_co = StringField("KO First Name", validators=[InputRequired()]) - lname_co = StringField("KO Last Name", validators=[InputRequired()]) + fname_co = StringField( + translate("forms.financial.fname_co_label"), validators=[InputRequired()] + ) + lname_co = StringField( + translate("forms.financial.lname_co_label"), validators=[InputRequired()] + ) - email_co = EmailField("KO Email", validators=[InputRequired(), Email()]) + email_co = EmailField( + translate("forms.financial.email_co_label"), + validators=[InputRequired(), Email()], + ) - office_co = StringField("KO Office", validators=[InputRequired()]) + office_co = StringField( + translate("forms.financial.office_co_label"), validators=[InputRequired()] + ) - fname_cor = StringField("COR First Name", validators=[InputRequired()]) + fname_cor = StringField( + translate("forms.financial.fname_cor_label"), validators=[InputRequired()] + ) - lname_cor = StringField("COR Last Name", validators=[InputRequired()]) + lname_cor = StringField( + translate("forms.financial.lname_cor_label"), validators=[InputRequired()] + ) - email_cor = EmailField("COR Email", validators=[InputRequired(), Email()]) + email_cor = EmailField( + translate("forms.financial.email_cor_label"), + validators=[InputRequired(), Email()], + ) - office_cor = StringField("COR Office", validators=[InputRequired()]) + office_cor = StringField( + translate("forms.financial.office_cor_label"), validators=[InputRequired()] + ) def reset(self): """ diff --git a/atst/forms/internal_comment.py b/atst/forms/internal_comment.py index 7711ff04..11ad15aa 100644 --- a/atst/forms/internal_comment.py +++ b/atst/forms/internal_comment.py @@ -2,12 +2,13 @@ from wtforms.fields import TextAreaField from wtforms.validators import InputRequired from .forms import CacheableForm +from atst.utils.localization import translate class InternalCommentForm(CacheableForm): text = TextAreaField( - "CCPO Internal Notes", + translate("forms.internal_comment.text_label"), default="", - description="Add comments or notes for internal CCPO reference and follow-up here. These comments will not be visible to the person making the JEDI request.", + description=translate("forms.internal_comment.text_description"), validators=[InputRequired()], ) diff --git a/atst/forms/new_member.py b/atst/forms/new_member.py index 5b53c839..0fae4728 100644 --- a/atst/forms/new_member.py +++ b/atst/forms/new_member.py @@ -5,20 +5,30 @@ from wtforms.validators import Required, Email, Length from atst.forms.validators import IsNumber from atst.forms.fields import SelectField +from atst.utils.localization import translate from .data import WORKSPACE_ROLES class NewMemberForm(FlaskForm): - first_name = StringField(label="First Name", validators=[Required()]) - last_name = StringField(label="Last Name", validators=[Required()]) - email = EmailField("Email Address", validators=[Required(), Email()]) - dod_id = StringField("DOD ID", validators=[Required(), Length(min=10), IsNumber()]) + first_name = StringField( + label=translate("forms.new_member.first_name_label"), validators=[Required()] + ) + last_name = StringField( + label=translate("forms.new_member.last_name_label"), validators=[Required()] + ) + email = EmailField( + translate("forms.new_member.email_label"), validators=[Required(), Email()] + ) + dod_id = StringField( + translate("forms.new_member.dod_id_label"), + validators=[Required(), Length(min=10), IsNumber()], + ) workspace_role = SelectField( - "Workspace Role", + translate("forms.new_member.workspace_role_label"), choices=WORKSPACE_ROLES, validators=[Required()], default="", - description="The workspace role controls whether a member is permitted to organize a workspace into projects and environments, add members to this workspace, and view billing information.", + description=translate("forms.new_member.workspace_role_description"), ) diff --git a/atst/forms/new_request.py b/atst/forms/new_request.py index 73f005f5..ff89d58d 100644 --- a/atst/forms/new_request.py +++ b/atst/forms/new_request.py @@ -14,6 +14,7 @@ from .data import ( ) from .validators import DateRange, IsNumber from atst.domain.requests import Requests +from atst.utils.localization import translate class DetailsOfUseForm(CacheableForm): @@ -43,120 +44,126 @@ class DetailsOfUseForm(CacheableForm): # Details of Use: General dod_component = SelectField( - "DoD Component", - description="Identify the DoD component that is requesting access to the JEDI Cloud", + translate("forms.new_request.dod_component_label"), + description=translate("forms.new_request.dod_component_description"), choices=SERVICE_BRANCHES, validators=[InputRequired()], ) jedi_usage = TextAreaField( - "JEDI Usage", - description="Your answer will help us provide tangible examples to DoD leadership how and why commercial cloud resources are accelerating the Department's missions", + translate("forms.new_request.jedi_usage_label"), + description=translate("forms.new_request.jedi_usage_description"), validators=[InputRequired()], ) # Details of Use: Cloud Readiness num_software_systems = IntegerField( - "Number of Software Systems", - description="Estimate the number of software systems that will be supported by this JEDI Cloud access request", + translate("forms.new_request.num_software_systems_label"), + description=translate("forms.new_request.num_software_systems_description"), ) jedi_migration = RadioField( - "JEDI Migration", - description="Are you using the JEDI Cloud to migrate existing systems?", + translate("forms.new_request.jedi_migration_label"), + description=translate("forms.new_request.jedi_migration_description"), choices=[("yes", "Yes"), ("no", "No")], default="", ) rationalization_software_systems = RadioField( - description="Have you completed a “rationalization” of your software systems to move to the cloud?", + description=translate( + "forms.new_request.rationalization_software_systems_description" + ), choices=[("yes", "Yes"), ("no", "No"), ("In Progress", "In Progress")], default="", ) technical_support_team = RadioField( - description="Are you working with a technical support team experienced in cloud migrations?", + description=translate("forms.new_request.technical_support_team_description"), choices=[("yes", "Yes"), ("no", "No")], default="", ) organization_providing_assistance = RadioField( # this needs to be updated to use checkboxes instead of radio - description="If you are receiving migration assistance, what is the type of organization providing assistance?", + description=translate( + "forms.new_request.organization_providing_assistance_description" + ), choices=ASSISTANCE_ORG_TYPES, default="", ) engineering_assessment = RadioField( - description="Have you completed an engineering assessment of your systems for cloud readiness?", + description=translate("forms.new_request.engineering_assessment_description"), choices=[("yes", "Yes"), ("no", "No"), ("In Progress", "In Progress")], default="", ) data_transfers = SelectField( - description="How much data is being transferred to the cloud?", + description=translate("forms.new_request.data_transfers_description"), choices=DATA_TRANSFER_AMOUNTS, validators=[DataRequired()], ) expected_completion_date = SelectField( - description="When do you expect to complete your migration to the JEDI Cloud?", + description=translate("forms.new_request.expected_completion_date_description"), choices=COMPLETION_DATE_RANGES, validators=[DataRequired()], ) cloud_native = RadioField( - description="Are your software systems being developed cloud native?", + description=translate("forms.new_request.cloud_native_description"), choices=[("yes", "Yes"), ("no", "No")], default="", ) # Details of Use: Financial Usage estimated_monthly_spend = IntegerField( - "Estimated Monthly Spend", - description='Use the JEDI CSP Calculator to estimate your monthly cloud resource usage and enter the dollar amount below. Note these estimates are for initial approval only. After the request is approved, you will be asked to provide a valid Task Order number with specific CLIN amounts for cloud services.', + translate("forms.new_request.estimated_monthly_spend_label"), + description=translate("forms.new_request.estimated_monthly_spend_description"), ) dollar_value = IntegerField( - "Total Spend", - description="What is your total expected budget for this JEDI Cloud Request?", + translate("forms.new_request.dollar_value_label"), + description=translate("forms.new_request.dollar_value_description"), ) number_user_sessions = IntegerField( - description="How many user sessions do you expect on these systems each day?" + description=translate("forms.new_request.number_user_sessions_description") ) average_daily_traffic = IntegerField( - "Average Daily Traffic (Number of Requests)", - description="What is the average daily traffic you expect the systems under this cloud contract to use?", + translate("forms.new_request.average_daily_traffic_label"), + description=translate("forms.new_request.average_daily_traffic_description"), ) average_daily_traffic_gb = IntegerField( - "Average Daily Traffic (GB)", - description="What is the average daily traffic you expect the systems under this cloud contract to use?", + translate("forms.new_request.average_daily_traffic_gb_label"), + description=translate("forms.new_request.average_daily_traffic_gb_description"), ) start_date = DateField( - description="When do you expect to start using the JEDI Cloud (not for billing purposes)?", + description=translate("forms.new_request.start_date_label"), validators=[ InputRequired(), DateRange( lower_bound=pendulum.duration(days=1), upper_bound=None, - message="Must be a date in the future.", + message=translate( + "forms.new_request.start_date_date_range_validation_message" + ), ), ], format="%m/%d/%Y", ) name = StringField( - "Name Your Request", - description="This name serves as a reference for your initial request and the associated workspace that will be created once this request is approved. You may edit this name later.", + translate("forms.new_request.name_label"), + description=translate("forms.new_request.name_description"), validators=[ InputRequired(), Length( min=4, max=100, - message="Request names must be at least 4 and not more than 100 characters", + message=translate("forms.new_request.name_length_validation_message"), ), ], ) @@ -187,21 +194,29 @@ class WorkspaceOwnerForm(CacheableForm): return super().validate(*args, **kwargs) am_poc = BooleanField( - "I am the Workspace Owner", + translate("forms.new_request.am_poc_label"), default=False, false_values=(False, "false", "False", "no", ""), ) - fname_poc = StringField("First Name", validators=[InputRequired()]) + fname_poc = StringField( + translate("forms.new_request.fname_poc_label"), validators=[InputRequired()] + ) - lname_poc = StringField("Last Name", validators=[InputRequired()]) + lname_poc = StringField( + translate("forms.new_request.lname_poc_label"), validators=[InputRequired()] + ) - email_poc = EmailField("Email Address", validators=[InputRequired(), Email()]) + email_poc = EmailField( + translate("forms.new_request.email_poc_label"), + validators=[InputRequired(), Email()], + ) dodid_poc = StringField( - "DoD ID", validators=[InputRequired(), Length(min=10), IsNumber()] + translate("forms.new_request.dodid_poc_label"), + validators=[InputRequired(), Length(min=10), IsNumber()], ) class ReviewAndSubmitForm(CacheableForm): - reviewed = BooleanField("I have reviewed this data and it is correct.") + reviewed = BooleanField(translate("forms.new_request.reviewed_label")) diff --git a/atst/forms/project.py b/atst/forms/project.py index 8d858a59..7ed30a00 100644 --- a/atst/forms/project.py +++ b/atst/forms/project.py @@ -2,21 +2,34 @@ from flask_wtf import FlaskForm from wtforms.fields import StringField, TextAreaField, FieldList from wtforms.validators import Required from atst.forms.validators import ListItemRequired, ListItemsUnique +from atst.utils.localization import translate class ProjectForm(FlaskForm): - name = StringField(label="Project Name", validators=[Required()]) - description = TextAreaField(label="Description", validators=[Required()]) + name = StringField( + label=translate("forms.project.name_label"), validators=[Required()] + ) + description = TextAreaField( + label=translate("forms.project.description_label"), validators=[Required()] + ) class NewProjectForm(ProjectForm): EMPTY_ENVIRONMENT_NAMES = ["", None] environment_names = FieldList( - StringField(label="Environment Name"), + StringField(label=translate("forms.project.environment_names_label")), validators=[ - ListItemRequired(message="Provide at least one environment name."), - ListItemsUnique(message="Environment names must be unique."), + ListItemRequired( + message=translate( + "forms.project.environment_names_required_validation_message" + ) + ), + ListItemsUnique( + message=translate( + "forms.project.environment_names_unique_validation_message" + ) + ), ], ) diff --git a/atst/forms/validators.py b/atst/forms/validators.py index b91b346d..3485c210 100644 --- a/atst/forms/validators.py +++ b/atst/forms/validators.py @@ -2,6 +2,7 @@ import re from wtforms.validators import ValidationError import pendulum from datetime import datetime +from atst.utils.localization import translate def DateRange(lower_bound=None, upper_bound=None, message=None): @@ -27,7 +28,7 @@ def DateRange(lower_bound=None, upper_bound=None, message=None): return _date_range -def IsNumber(message="Please enter a valid number."): +def IsNumber(message=translate("forms.validators.is_number_message")): def _is_number(form, field): try: int(field.data) @@ -37,7 +38,7 @@ def IsNumber(message="Please enter a valid number."): return _is_number -def PhoneNumber(message="Please enter a valid 5 or 10 digit phone number."): +def PhoneNumber(message=translate("forms.validators.phone_number_message")): def _is_phone_number(form, field): digits = re.sub(r"\D", "", field.data) if len(digits) not in [5, 10]: @@ -50,9 +51,7 @@ def PhoneNumber(message="Please enter a valid 5 or 10 digit phone number."): return _is_phone_number -def Name( - message="This field accepts letters, numbers, commas, apostrophes, hyphens, and periods." -): +def Name(message=translate("forms.validators.name_message")): def _name(form, field): match = re.match(r"[\w \,\.\'\-]+", field.data) if not match or match.group() != field.data: @@ -61,7 +60,10 @@ def Name( return _name -def ListItemRequired(message="Please provide at least one.", empty_values=("", None)): +def ListItemRequired( + message=translate("forms.validators.list_item_required_message"), + empty_values=("", None), +): def _list_item_required(form, field): non_empty_values = [v for v in field.data if v not in empty_values] if len(non_empty_values) == 0: @@ -70,7 +72,7 @@ def ListItemRequired(message="Please provide at least one.", empty_values=("", N return _list_item_required -def ListItemsUnique(message="Items must be unique"): +def ListItemsUnique(message=translate("forms.validators.list_items_unique_message")): def _list_items_unique(form, field): if len(field.data) > len(set(field.data)): raise ValidationError(message) diff --git a/atst/forms/workspace.py b/atst/forms/workspace.py index 5434676f..972af45f 100644 --- a/atst/forms/workspace.py +++ b/atst/forms/workspace.py @@ -2,16 +2,17 @@ from wtforms.fields import StringField from wtforms.validators import Length from .forms import CacheableForm +from atst.utils.localization import translate class WorkspaceForm(CacheableForm): name = StringField( - "Workspace Name", + translate("forms.workspace.name_label"), validators=[ Length( min=4, max=100, - message="Workspace names must be at least 4 and not more than 50 characters", + message=translate("forms.workspace.name_length_validation_message"), ) ], ) diff --git a/atst/utils/localization.py b/atst/utils/localization.py new file mode 100644 index 00000000..eff45b6a --- /dev/null +++ b/atst/utils/localization.py @@ -0,0 +1,35 @@ +import yaml +import os +from functools import lru_cache +from atst.utils import getattr_path + +ENV = os.getenv("FLASK_ENV", "dev") + + +class LocalizationInvalidKeyError(Exception): + def __init__(self, key, variables): + self.key = key + self.variables = variables + + def __str__(self): + return "Requested {key} and variables {variables} with but an error occured".format( + key=self.key, variables=self.variables + ) + + +localizations_cache_size = 1 if ENV == "dev" else None + + +@lru_cache(maxsize=localizations_cache_size) +def load_cached_translations_file(file_name): + return open(file_name).read() + + +def translate(key, variables={}): + translations = yaml.safe_load(load_cached_translations_file("translations.yaml")) + value = getattr_path(translations, key) + + if value == None: + raise LocalizationInvalidKeyError(key, variables) + + return value.format(**variables).replace("\n", "") diff --git a/templates/audit_log/audit_log.html b/templates/audit_log/audit_log.html index 9290ba21..73a7443c 100644 --- a/templates/audit_log/audit_log.html +++ b/templates/audit_log/audit_log.html @@ -6,7 +6,7 @@
-

Activity History

+

{{ "audit_log.header_title" | translate }}

{% with %} @@ -29,11 +33,11 @@ {% include "fragments/flash.html" %} {% endwith %} - {{ Alert('Certificate Selection', - message='When you are prompted to select a certificate, please select E-mail Certificate from the provided choices.', + {{ Alert(("login.certificate_selection.title" | translate), + message=("login.certificate_selection.message" | translate), actions=[ { - 'label': 'Learn More', + 'label': ("login.certificate_selection.learn_more" | translate), 'icon': 'help', 'href': '/help' } diff --git a/templates/navigation/topbar.html b/templates/navigation/topbar.html index 19464a6f..ce3561bc 100644 --- a/templates/navigation/topbar.html +++ b/templates/navigation/topbar.html @@ -5,7 +5,9 @@ {% if not workspace %} {{ Icon('shield', classes='topbar__link-icon') }} - JEDI Cloud + + {{ "navigation.topbar.jedi_cloud_link_text" | translate }} + {% else %} @@ -22,13 +24,15 @@ v-on:click='props.toggle' class="topbar__link topbar__workspace-menu__toggle" v-bind:class="{ 'topbar__workspace-menu__toggle--open': props.isVisible }"> - {{ ("Workspace " + workspace.name) }} + {{ "navigation.topbar.named_workspace" | translate({ "workspace": workspace.name }) }} @@ -60,7 +68,7 @@ {{ Icon('avatar', classes='topbar__link-icon') }} - + {{ Icon('logout', classes='topbar__link-icon') }} diff --git a/templates/navigation/workspace_navigation.html b/templates/navigation/workspace_navigation.html index dd63a6a3..9dc27119 100644 --- a/templates/navigation/workspace_navigation.html +++ b/templates/navigation/workspace_navigation.html @@ -3,12 +3,12 @@