Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More server i18n #30

Merged
merged 19 commits into from
Nov 30, 2024
Merged
3 changes: 3 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ SECRET_KEY=CHANGE_ME
ADMIN_EMAIL=change_me@change_me.com
ADMIN_INITIAL_PASSWORD=change_me

# i18n locale for server-context
BABEL_DEFAULT_LOCALE=en

# mongodb database
MONGODB_URI='mongodb://moeflow:PLEASE_CHANGE_THIS@moeflow-mongodb:27017/moeflow?authSource=admin'

Expand Down
4 changes: 3 additions & 1 deletion app/apis/me.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,9 @@
p = MoePagination()
teams = self.current_user.teams(skip=p.skip, limit=p.limit, word=word)
# 获取团队用户关系
relations = TeamUserRelation.objects(group__in=teams, user=self.current_user)
relations: list[TeamUserRelation] = TeamUserRelation.objects(

Check warning on line 316 in app/apis/me.py

View check run for this annotation

Codecov / codecov/patch

app/apis/me.py#L316

Added line #L316 was not covered by tests
group__in=teams, user=self.current_user
)
Comment on lines +316 to +318
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ensure Type Hint Compatibility with Python Versions Below 3.9

The use of list[TeamUserRelation] as a type hint requires Python 3.9 or above. If your project supports earlier Python versions, consider using List[TeamUserRelation] from the typing module for compatibility.

Apply this diff to fix the type hinting:

- relations: list[TeamUserRelation] = TeamUserRelation.objects(
+ relations: List[TeamUserRelation] = TeamUserRelation.objects(

Additionally, import List from the typing module at the beginning of the file:

from typing import List

# 构建字典用于快速匹配
team_roles_data = {}
for relation in relations:
Expand Down
2 changes: 1 addition & 1 deletion app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
# -----------
# i18n
# -----------
BABEL_DEFAULT_LOCALE = "zh"
BABEL_DEFAULT_LOCALE = env.get("BABEL_DEFAULT_LOCALE", "zh")
BABEL_DEFAULT_TIMEZONE = "UTC"
# -----------
# 其他设置
Expand Down
2 changes: 1 addition & 1 deletion app/core/rbac.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import logging
from app.exceptions import UserNotExistError, CreatorCanNotLeaveError

from flask_babel import gettext, lazy_gettext
from app.translations import gettext, lazy_gettext
from mongoengine import (
BooleanField,
DateTimeField,
Expand Down
25 changes: 18 additions & 7 deletions app/factory.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import logging
from celery import Celery
import celery
from flask import Flask
from flask_apikit import APIKit
from flask_babel import Babel
Expand All @@ -8,7 +8,7 @@
import app.config as _app_config
from app.services.oss import OSS
from .apis import register_apis
from app.translations import get_locale
from app.translations import get_request_locale

from app.models import connect_db

Expand Down Expand Up @@ -43,22 +43,33 @@ def create_flask_app(app: Flask) -> Flask:

def init_flask_app(app: Flask):
register_apis(app)
babel.init_app(app, locale_selector=get_locale)
babel.init_app(
app,
locale_selector=get_request_locale,
default_locale=app_config["BABEL_DEFAULT_LOCALE"],
)
apikit.init_app(app)
logger.info(f"----- build id: {app_config['BUILD_ID']}")
with app.app_context():
logger.debug(
"站点支持语言: " + str([str(i) for i in babel.list_translations()])
"Server locale translations: "
+ str([str(i) for i in babel.list_translations()])
)
oss.init(app.config) # 文件储存


def create_celery(app: Flask) -> Celery:
# 通过app配置创建celery实例
created = Celery(
def create_celery(app: Flask) -> celery.Celery:
# see https://flask.palletsprojects.com/en/stable/patterns/celery/
class FlaskTask(celery.Task):
def __call__(self, *args: object, **kwargs: object) -> object:
with app.app_context():
return self.run(*args, **kwargs)

created = celery.Celery(
app.name,
broker=app.config["CELERY_BROKER_URL"],
backend=app.config["CELERY_BACKEND_URL"],
task_cls=FlaskTask,
**app.config["CELERY_BACKEND_SETTINGS"],
)
created.conf.update({"app_config": app.config})
Expand Down
2 changes: 1 addition & 1 deletion app/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from mongoengine import connect

logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
logger.setLevel(logging.INFO)


def connect_db(config):
Expand Down
4 changes: 2 additions & 2 deletions app/models/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -1221,8 +1221,8 @@ class Source(Document):
meta = {"indexes": ["file", "blank", "rank"]}

@classmethod
def by_id(cls, id) -> "Source":
source = cls.objects(id=id).first()
def by_id(cls, id_: str | ObjectId) -> "Source":
source = cls.objects(id=id_).first()
Comment on lines +1224 to +1225
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ensure compatibility with Python versions when using union type annotations with |.

The use of str | ObjectId for type annotations requires Python 3.10 or newer. If the codebase needs to support earlier Python versions, consider using Union[str, ObjectId] from the typing module.

Apply this diff to ensure compatibility:

+from typing import Union
...
     @classmethod
-    def by_id(cls, id_: str | ObjectId) -> "Source":
+    def by_id(cls, id_: Union[str, ObjectId]) -> "Source":
         source = cls.objects(id=id_).first()
         if source is None:
             raise SourceNotExistError

Committable suggestion skipped: line range outside the PR's diff.

if source is None:
raise SourceNotExistError
return source
Expand Down
8 changes: 4 additions & 4 deletions app/models/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,11 +310,11 @@
self.delete()

@classmethod
def by_id(cls, id):
set = cls.objects(id=id).first()
if set is None:
def by_id(cls, id: ObjectId):
project_set = cls.objects(id=id).first()
if project_set is None:

Check warning on line 315 in app/models/project.py

View check run for this annotation

Codecov / codecov/patch

app/models/project.py#L314-L315

Added lines #L314 - L315 were not covered by tests
raise ProjectSetNotExistError()
return set
return project_set

Check warning on line 317 in app/models/project.py

View check run for this annotation

Codecov / codecov/patch

app/models/project.py#L317

Added line #L317 was not covered by tests
Comment on lines +313 to +317
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid using id as a parameter name to prevent shadowing the built-in function.

Using id as a parameter name shadows the built-in function id(). This can lead to confusion and potential bugs. Consider renaming the parameter to id_ for clarity.

Apply this diff to rename the parameter and its usage:

 @classmethod
-def by_id(cls, id: ObjectId):
-    project_set = cls.objects(id=id).first()
+def by_id(cls, id_: ObjectId):
+    project_set = cls.objects(id=id_).first()
     if project_set is None:
         raise ProjectSetNotExistError()
     return project_set
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def by_id(cls, id: ObjectId):
project_set = cls.objects(id=id).first()
if project_set is None:
raise ProjectSetNotExistError()
return set
return project_set
def by_id(cls, id_: ObjectId):
project_set = cls.objects(id=id_).first()
if project_set is None:
raise ProjectSetNotExistError()
return project_set


def to_api(self):
"""
Expand Down
4 changes: 2 additions & 2 deletions app/models/team.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from typing import List

from flask import current_app
from flask_babel import lazy_gettext, gettext
from app.translations import lazy_gettext, gettext
from mongoengine import (
CASCADE,
DENY,
Expand Down Expand Up @@ -470,7 +470,7 @@ def to_api(self, user=None):
# 如果给了 role 则获取用户相关信息(角色等)
role = None
if user:
role = user.get_role(self)
role: TeamRole | None = user.get_role(self)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid Using Union Operator in Type Annotations for Python Versions Below 3.10

The type hint TeamRole | None utilizes the union operator |, which was introduced in Python 3.10. If the project needs to maintain compatibility with earlier Python versions, use Optional[TeamRole] from the typing module instead.

Apply this diff to fix the type hinting:

- role: TeamRole | None = user.get_role(self)
+ role: Optional[TeamRole] = user.get_role(self)

Also, import Optional from the typing module:

from typing import Optional

if role:
role = role.to_api()
return {
Expand Down
51 changes: 24 additions & 27 deletions app/tasks/import_from_labelplus.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,35 +56,32 @@ def import_from_labelplus_task(project_id):
)
return f"失败:创建者不存在,Project {project_id}"
try:
with app.app_context():
if target and creator:
if target and creator:
project.update(
import_from_labelplus_percent=0,
import_from_labelplus_status=ImportFromLabelplusStatus.RUNNING,
)
labelplus_data = load_from_labelplus(project.import_from_labelplus_txt)
file_count = len(labelplus_data)
Comment on lines +64 to +65
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Handle empty data to prevent division by zero

If labelplus_data is empty, file_count becomes zero, leading to a potential ZeroDivisionError during progress calculation. Introduce a check to handle the scenario where there are no files to import.

Apply this diff to handle the zero files case:

             labelplus_data = load_from_labelplus(project.import_from_labelplus_txt)
             file_count = len(labelplus_data)
+            if file_count == 0:
+                project.update(
+                    import_from_labelplus_status=ImportFromLabelplusStatus.ERROR,
+                    import_from_labelplus_error_type=ImportFromLabelplusErrorType.NO_FILES,
+                )
+                return f"失败:没有文件可导入,Project {project_id}"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
labelplus_data = load_from_labelplus(project.import_from_labelplus_txt)
file_count = len(labelplus_data)
labelplus_data = load_from_labelplus(project.import_from_labelplus_txt)
file_count = len(labelplus_data)
if file_count == 0:
project.update(
import_from_labelplus_status=ImportFromLabelplusStatus.ERROR,
import_from_labelplus_error_type=ImportFromLabelplusErrorType.NO_FILES,
)
return f"失败:没有文件可导入,Project {project_id}"

for file_index, labelplus_file in enumerate(labelplus_data):
file = project.create_file(labelplus_file["file_name"])
for labelplus_label in labelplus_file["labels"]:
source = file.create_source(
content="",
x=labelplus_label["x"],
y=labelplus_label["y"],
position_type=SourcePositionType.IN
if labelplus_label["position_type"] == SourcePositionType.IN
else SourcePositionType.OUT,
)
source.create_translation(
content=labelplus_label["translation"],
target=target,
user=creator,
)
project.update(
import_from_labelplus_percent=0,
import_from_labelplus_status=ImportFromLabelplusStatus.RUNNING,
import_from_labelplus_percent=int((file_index / file_count) * 100)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Correct progress percentage calculation

The calculation int((file_index / file_count) * 100) does not reach 100% on the last iteration because file_index starts from 0. Adjust the calculation to ensure accurate progress reporting.

Apply this diff to fix the progress percentage calculation:

                         import_from_labelplus_percent=int((file_index / file_count) * 100)
+                        import_from_labelplus_percent=int(((file_index + 1) / file_count) * 100)

This change ensures that the progress reaches 100% upon completion.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import_from_labelplus_percent=int((file_index / file_count) * 100)
import_from_labelplus_percent=int(((file_index + 1) / file_count) * 100)

)
labelplus_data = load_from_labelplus(project.import_from_labelplus_txt)
file_count = len(labelplus_data)
for file_index, labelplus_file in enumerate(labelplus_data):
file = project.create_file(labelplus_file["file_name"])
for labelplus_label in labelplus_file["labels"]:
source = file.create_source(
content="",
x=labelplus_label["x"],
y=labelplus_label["y"],
position_type=SourcePositionType.IN
if labelplus_label["position_type"] == SourcePositionType.IN
else SourcePositionType.OUT,
)
source.create_translation(
content=labelplus_label["translation"],
target=target,
user=creator,
)
project.update(
import_from_labelplus_percent=int(
(file_index / file_count) * 100
)
)
except Exception:
logger.exception(Exception)
project.update(
Expand Down
49 changes: 24 additions & 25 deletions app/tasks/output_team_projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,32 +37,31 @@
app = Flask(__name__)
app.config.from_object(celery.conf.app_config)

with app.app_context():
OUTPUT_WAIT_SECONDS = celery.conf.app_config.get("OUTPUT_WAIT_SECONDS", 60 * 5)
current_user = User.by_id(current_user_id)
OUTPUT_WAIT_SECONDS = celery.conf.app_config.get("OUTPUT_WAIT_SECONDS", 60 * 5)
current_user = User.by_id(current_user_id)

Check warning on line 41 in app/tasks/output_team_projects.py

View check run for this annotation

Codecov / codecov/patch

app/tasks/output_team_projects.py#L40-L41

Added lines #L40 - L41 were not covered by tests

team = Team.by_id(team_id)
for project in team.projects(status=ProjectStatus.WORKING):
for target in project.targets():
# 等待一定时间后允许再次导出
last_output = target.outputs().first()
if last_output and (
datetime.datetime.utcnow() - last_output.create_time
< datetime.timedelta(seconds=OUTPUT_WAIT_SECONDS)
):
continue
# 删除三个导出之前的
old_targets = target.outputs().skip(2)
Output.delete_real_files(old_targets)
old_targets.delete()
# 创建新target
output = Output.create(
project=project,
target=target,
user=current_user,
type=OutputTypes.ALL,
)
output_project(str(output.id))
team = Team.by_id(team_id)
for project in team.projects(status=ProjectStatus.WORKING):
for target in project.targets():

Check warning on line 45 in app/tasks/output_team_projects.py

View check run for this annotation

Codecov / codecov/patch

app/tasks/output_team_projects.py#L43-L45

Added lines #L43 - L45 were not covered by tests
# 等待一定时间后允许再次导出
last_output = target.outputs().first()
if last_output and (

Check warning on line 48 in app/tasks/output_team_projects.py

View check run for this annotation

Codecov / codecov/patch

app/tasks/output_team_projects.py#L47-L48

Added lines #L47 - L48 were not covered by tests
datetime.datetime.utcnow() - last_output.create_time
< datetime.timedelta(seconds=OUTPUT_WAIT_SECONDS)
):
continue

Check warning on line 52 in app/tasks/output_team_projects.py

View check run for this annotation

Codecov / codecov/patch

app/tasks/output_team_projects.py#L52

Added line #L52 was not covered by tests
# 删除三个导出之前的
old_targets = target.outputs().skip(2)
Output.delete_real_files(old_targets)
old_targets.delete()

Check warning on line 56 in app/tasks/output_team_projects.py

View check run for this annotation

Codecov / codecov/patch

app/tasks/output_team_projects.py#L54-L56

Added lines #L54 - L56 were not covered by tests
# 创建新target
output = Output.create(

Check warning on line 58 in app/tasks/output_team_projects.py

View check run for this annotation

Codecov / codecov/patch

app/tasks/output_team_projects.py#L58

Added line #L58 was not covered by tests
project=project,
target=target,
user=current_user,
type=OutputTypes.ALL,
)
output_project(str(output.id))

Check warning on line 64 in app/tasks/output_team_projects.py

View check run for this annotation

Codecov / codecov/patch

app/tasks/output_team_projects.py#L64

Added line #L64 was not covered by tests

return f"成功:已创建 Team <{str(team.id)}> 所有项目的导出任务"

Expand Down
28 changes: 23 additions & 5 deletions app/translations/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,30 @@
from typing import Optional
from flask import g, request
from app.constants.locale import Locale
import flask_babel

logger = logging.getLogger(__name__)
# logger.setLevel(logging.DEBUG)
logger.setLevel(logging.INFO)


def get_locale() -> Optional[str]:
def get_request_locale() -> Optional[str]:
current_user = g.get("current_user")
req_header = f"{request.method} {request.path}"
if (
current_user
and current_user.locale
and current_user.locale != "auto"
and current_user.locale in Locale.ids()
):
# NOTE User.locale is not used
logging.debug("locale from user %s", current_user.locale)
# NOTE User.locale is not used , so this won't get called
logging.debug(

Check warning on line 21 in app/translations/__init__.py

View check run for this annotation

Codecov / codecov/patch

app/translations/__init__.py#L21

Added line #L21 was not covered by tests
"%s set locale=%s from user pref", req_header, current_user.locale
)
return current_user.locale
# "zh" locale asssets is created from hardcoded strings
# "en" locale are machine translated
best_match = request.accept_languages.best_match(["zh", "en"], default="en")
logging.debug("locale from req %s", best_match)
logging.debug("%s set locale=%s from req", req_header, best_match)
return best_match


Expand All @@ -32,3 +36,17 @@
# if current_user:
# if current_user.timezone:
# return current_user.timezone


def gettext(msgid: str):
translated = flask_babel.gettext(msgid)
logger.debug(
f"get_text({msgid}, locale={flask_babel.get_locale()}) -> {translated}"
)
return translated


def lazy_gettext(msgid: str):
translated = flask_babel.LazyString(lambda: gettext(msgid))
# logger.debug(f"lazy_get_text({msgid}) -> {translated}")
return translated
Loading