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 @@
{% for key, value in event.event_details.items() %}
{% if value is not none %}
@@ -15,11 +15,13 @@
{% endif %}
{% if event.changed_state %}
- Changes:
+ {{ "audit_log.events.default.changes" | translate }}
{% for key, value in event.changed_state.items() %}
diff --git a/templates/components/usa_header.html b/templates/components/usa_header.html
index 61086563..5b0af029 100644
--- a/templates/components/usa_header.html
+++ b/templates/components/usa_header.html
@@ -1,10 +1,14 @@
{% from "components/tooltip.html" import Tooltip %}
diff --git a/templates/footer.html b/templates/footer.html
index 9b2c9d94..7d19e27e 100644
--- a/templates/footer.html
+++ b/templates/footer.html
@@ -2,16 +2,20 @@
diff --git a/templates/fragments/edit_project_form.html b/templates/fragments/edit_project_form.html
index 0d35b2de..f490a7e8 100644
--- a/templates/fragments/edit_project_form.html
+++ b/templates/fragments/edit_project_form.html
@@ -1,6 +1,6 @@
{% from "components/text_input.html" import TextInput %}
-{% set title_text = 'Edit {} project'.format(project.name) if project else 'Add a new project' %}
+{% set title_text = ('fragments.edit_project_form.existing_project_title' | translate({ "project_name": project.name })) if project else ('fragments.edit_project_form.new_project_title' | translate) %}
{{ form.csrf_token }}
@@ -10,7 +10,7 @@
- AT-AT allows you to organize your workspace into multiple projects, each of which may have environments.
+ {{ "fragments.edit_project_form.explain" | translate }}
{{ TextInput(form.name) }}
{{ TextInput(form.description, paragraph=True) }}
diff --git a/templates/fragments/edit_user_form.html b/templates/fragments/edit_user_form.html
index 1e3e692e..706165d0 100644
--- a/templates/fragments/edit_user_form.html
+++ b/templates/fragments/edit_user_form.html
@@ -23,11 +23,20 @@
{{ OptionsInput(form.service_branch) }}
{{ OptionsInput(form.citizenship) }}
{{ OptionsInput(form.designation) }}
- {{ DateInput(form.date_latest_training,tooltip="When was the last time you completed the IA training? Information Assurance (IA) training is an important step in cyber awareness.",placeholder="MM / DD / YYYY", validation="date") }}
+ {{
+ DateInput(
+ form.date_latest_training,
+ tooltip=("fragments.edit_user_form.date_last_training_tooltip" | translate),
+ placeholder="MM / DD / YYYY",
+ validation="date"
+ )
+ }}
- The CCPO will review and respond to your request in 3 business days. You’ll be notified via email or phone. Please note if your request is for over $1M of JEDI cloud resources it will require a manual review by the CCPO.
+ {{ "fragments.pending_ccpo_acceptance_alert.paragraph_1" | translate }}
- While your request is being reviewed, your next step is to create a Task Order (TO) associated with the JEDI Cloud. Please contact a Contracting Officer (KO), Contracting Officer Representative (COR), or a Financial Manager to help with this step.
+ {{ "fragments.pending_ccpo_acceptance_alert.paragraph_2" | translate }}
- The CCPO will review and respond to your Financial Verification submission in 3 business days. You will be notified via email or phone.
+ {{ "fragments.pending_ccpo_approval_modal.paragraph_1" | translate }}
- Once the financial verification is approved you will be invited to create your JEDI Workspace and set-up your projects. Click here for more details.
+ {{ "fragments.pending_ccpo_approval_modal.paragraph_2" | translate }}
- The next step is to create a Task Order associated with JEDI Cloud.
- Please contact a Contracting Officer (KO), Contracting Officer
- Representative (COR), or a Financial Manager to help with this step.
+ {{ "fragments.pending_financial_verification.paragraph_1" | translate }}
- Once the Task Order has been created, you will be asked to provide
- details about the task order in the Financial Verification step.
+ {{ "fragments.pending_financial_verification.paragraph_2" | translate }}