Skip to content

Commit 635b094

Browse files
ColdHeatMiłosz Skaza
and
Miłosz Skaza
authored
Add Translations layer (CTFd#2288)
* Add rough translations support into CTFd * Add `flask-babel` dependency * Adds language column to users table * Closes CTFd#570 --------- Co-authored-by: Miłosz Skaza <milosz.skaza@ctfd.io>
1 parent 2474d60 commit 635b094

File tree

24 files changed

+1660
-108
lines changed

24 files changed

+1660
-108
lines changed

CTFd/__init__.py

+6
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import jinja2
88
from flask import Flask, Request
99
from flask.helpers import safe_join
10+
from flask_babel import Babel
1011
from flask_migrate import upgrade
1112
from jinja2 import FileSystemLoader
1213
from jinja2.sandbox import SandboxedEnvironment
@@ -28,6 +29,7 @@
2829
from CTFd.utils.migrations import create_database, migrations, stamp_latest_revision
2930
from CTFd.utils.sessions import CachingSessionInterface
3031
from CTFd.utils.updates import update_check
32+
from CTFd.utils.user import get_locale
3133

3234
__version__ = "3.5.2"
3335
__channel__ = "oss"
@@ -208,6 +210,10 @@ def create_app(config="CTFd.config.Config"):
208210
# Register Flask-Migrate
209211
migrations.init_app(app, db)
210212

213+
babel = Babel()
214+
babel.locale_selector_func = get_locale
215+
babel.init_app(app)
216+
211217
# Alembic sqlite support is lacking so we should just create_all anyway
212218
if url.drivername.startswith("sqlite"):
213219
# Enable foreign keys for SQLite. This must be before the

CTFd/admin/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import csv # noqa: I001
22
import datetime
3-
from io import StringIO
43
import os
4+
from io import StringIO
55

66
from flask import Blueprint, abort
77
from flask import current_app as app

CTFd/constants/languages.py

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from CTFd.constants import RawEnum
2+
3+
4+
class Languages(str, RawEnum):
5+
ENGLISH = "en"
6+
GERMAN = "de"
7+
POLISH = "pl"
8+
9+
10+
LANGUAGE_NAMES = {
11+
"en": "English",
12+
"de": "Deutsch",
13+
"pl": "Polski",
14+
}
15+
16+
SELECT_LANGUAGE_LIST = [("", "")] + [
17+
(str(lang), LANGUAGE_NAMES.get(str(lang))) for lang in Languages
18+
]

CTFd/constants/users.py

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"hidden",
1717
"banned",
1818
"verified",
19+
"language",
1920
"team_id",
2021
"created",
2122
],

CTFd/forms/auth.py

+13-12
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from flask_babel import lazy_gettext as _l
12
from wtforms import PasswordField, StringField
23
from wtforms.fields.html5 import EmailField
34
from wtforms.validators import InputRequired
@@ -15,11 +16,11 @@
1516
def RegistrationForm(*args, **kwargs):
1617
class _RegistrationForm(BaseForm):
1718
name = StringField(
18-
"User Name", validators=[InputRequired()], render_kw={"autofocus": True}
19+
_l("User Name"), validators=[InputRequired()], render_kw={"autofocus": True}
1920
)
20-
email = EmailField("Email", validators=[InputRequired()])
21-
password = PasswordField("Password", validators=[InputRequired()])
22-
submit = SubmitField("Submit")
21+
email = EmailField(_l("Email"), validators=[InputRequired()])
22+
password = PasswordField(_l("Password"), validators=[InputRequired()])
23+
submit = SubmitField(_l("Submit"))
2324

2425
@property
2526
def extra(self):
@@ -35,27 +36,27 @@ def extra(self):
3536

3637
class LoginForm(BaseForm):
3738
name = StringField(
38-
"User Name or Email",
39+
_l("User Name or Email"),
3940
validators=[InputRequired()],
4041
render_kw={"autofocus": True},
4142
)
42-
password = PasswordField("Password", validators=[InputRequired()])
43-
submit = SubmitField("Submit")
43+
password = PasswordField(_l("Password"), validators=[InputRequired()])
44+
submit = SubmitField(_l("Submit"))
4445

4546

4647
class ConfirmForm(BaseForm):
47-
submit = SubmitField("Resend Confirmation Email")
48+
submit = SubmitField(_l("Resend Confirmation Email"))
4849

4950

5051
class ResetPasswordRequestForm(BaseForm):
5152
email = EmailField(
52-
"Email", validators=[InputRequired()], render_kw={"autofocus": True}
53+
_l("Email"), validators=[InputRequired()], render_kw={"autofocus": True}
5354
)
54-
submit = SubmitField("Submit")
55+
submit = SubmitField(_l("Submit"))
5556

5657

5758
class ResetPasswordForm(BaseForm):
5859
password = PasswordField(
59-
"Password", validators=[InputRequired()], render_kw={"autofocus": True}
60+
_l("Password"), validators=[InputRequired()], render_kw={"autofocus": True}
6061
)
61-
submit = SubmitField("Submit")
62+
submit = SubmitField(_l("Submit"))

CTFd/forms/self.py

+13-10
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
from flask import session
2+
from flask_babel import lazy_gettext as _l
23
from wtforms import PasswordField, SelectField, StringField
34
from wtforms.fields.html5 import DateField, URLField
45

6+
from CTFd.constants.languages import SELECT_LANGUAGE_LIST
57
from CTFd.forms import BaseForm
68
from CTFd.forms.fields import SubmitField
79
from CTFd.forms.users import attach_custom_user_fields, build_custom_user_fields
@@ -11,14 +13,15 @@
1113

1214
def SettingsForm(*args, **kwargs):
1315
class _SettingsForm(BaseForm):
14-
name = StringField("User Name")
15-
email = StringField("Email")
16-
password = PasswordField("Password")
17-
confirm = PasswordField("Current Password")
18-
affiliation = StringField("Affiliation")
19-
website = URLField("Website")
20-
country = SelectField("Country", choices=SELECT_COUNTRIES_LIST)
21-
submit = SubmitField("Submit")
16+
name = StringField(_l("User Name"))
17+
email = StringField(_l("Email"))
18+
language = SelectField(_l("Language"), choices=SELECT_LANGUAGE_LIST)
19+
password = PasswordField(_l("Password"))
20+
confirm = PasswordField(_l("Current Password"))
21+
affiliation = StringField(_l("Affiliation"))
22+
website = URLField(_l("Website"))
23+
country = SelectField(_l("Country"), choices=SELECT_COUNTRIES_LIST)
24+
submit = SubmitField(_l("Submit"))
2225

2326
@property
2427
def extra(self):
@@ -46,5 +49,5 @@ def get_field_kwargs():
4649

4750

4851
class TokensForm(BaseForm):
49-
expiration = DateField("Expiration")
50-
submit = SubmitField("Generate")
52+
expiration = DateField(_l("Expiration"))
53+
submit = SubmitField(_l("Generate"))

CTFd/forms/setup.py

+34-23
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from flask_babel import lazy_gettext as _l
12
from wtforms import (
23
FileField,
34
HiddenField,
@@ -18,62 +19,72 @@
1819

1920
class SetupForm(BaseForm):
2021
ctf_name = StringField(
21-
"Event Name", description="The name of your CTF event/workshop"
22+
_l("Event Name"), description=_l("The name of your CTF event/workshop")
2223
)
2324
ctf_description = TextAreaField(
24-
"Event Description", description="Description for the CTF"
25+
_l("Event Description"), description=_l("Description for the CTF")
2526
)
2627
user_mode = RadioField(
27-
"User Mode",
28-
choices=[("teams", "Team Mode"), ("users", "User Mode")],
28+
_l("User Mode"),
29+
choices=[("teams", _l("Team Mode")), ("users", _l("User Mode"))],
2930
default="teams",
30-
description="Controls whether users join together in teams to play (Team Mode) or play as themselves (User Mode)",
31+
description=_l(
32+
"Controls whether users join together in teams to play (Team Mode) or play as themselves (User Mode)"
33+
),
3134
validators=[InputRequired()],
3235
)
3336

3437
name = StringField(
35-
"Admin Username",
36-
description="Your username for the administration account",
38+
_l("Admin Username"),
39+
description=_l("Your username for the administration account"),
3740
validators=[InputRequired()],
3841
)
3942
email = EmailField(
40-
"Admin Email",
41-
description="Your email address for the administration account",
43+
_l("Admin Email"),
44+
description=_l("Your email address for the administration account"),
4245
validators=[InputRequired()],
4346
)
4447
password = PasswordField(
45-
"Admin Password",
46-
description="Your password for the administration account",
48+
_l("Admin Password"),
49+
description=_l("Your password for the administration account"),
4750
validators=[InputRequired()],
4851
)
4952

5053
ctf_logo = FileField(
51-
"Logo",
52-
description="Logo to use for the website instead of a CTF name. Used as the home page button. Optional.",
54+
_l("Logo"),
55+
description=_l(
56+
"Logo to use for the website instead of a CTF name. Used as the home page button. Optional."
57+
),
5358
)
5459
ctf_banner = FileField(
55-
"Banner", description="Banner to use for the homepage. Optional."
60+
_l("Banner"), description=_l("Banner to use for the homepage. Optional.")
5661
)
5762
ctf_small_icon = FileField(
58-
"Small Icon",
59-
description="favicon used in user's browsers. Only PNGs accepted. Must be 32x32px. Optional.",
63+
_l("Small Icon"),
64+
description=_l(
65+
"favicon used in user's browsers. Only PNGs accepted. Must be 32x32px. Optional."
66+
),
6067
)
6168
ctf_theme = SelectField(
62-
"Theme",
63-
description="CTFd Theme to use. Can be changed later.",
69+
_l("Theme"),
70+
description=_l("CTFd Theme to use. Can be changed later."),
6471
choices=list(zip(get_themes(), get_themes())),
6572
default=DEFAULT_THEME,
6673
validators=[InputRequired()],
6774
)
6875
theme_color = HiddenField(
69-
"Theme Color",
70-
description="Color used by theme to control aesthetics. Requires theme support. Optional.",
76+
_l("Theme Color"),
77+
description=_l(
78+
"Color used by theme to control aesthetics. Requires theme support. Optional."
79+
),
7180
)
7281

7382
start = StringField(
74-
"Start Time", description="Time when your CTF is scheduled to start. Optional."
83+
_l("Start Time"),
84+
description=_l("Time when your CTF is scheduled to start. Optional."),
7585
)
7686
end = StringField(
77-
"End Time", description="Time when your CTF is scheduled to end. Optional."
87+
_l("End Time"),
88+
description=_l("Time when your CTF is scheduled to end. Optional."),
7889
)
79-
submit = SubmitField("Finish")
90+
submit = SubmitField(_l("Finish"))

0 commit comments

Comments
 (0)