diff --git a/.env.sample b/.env.sample index be27a1b..a6c7f31 100644 --- a/.env.sample +++ b/.env.sample @@ -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' diff --git a/app/apis/me.py b/app/apis/me.py index 5987422..51e4daf 100644 --- a/app/apis/me.py +++ b/app/apis/me.py @@ -313,7 +313,9 @@ def get(self): 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( + group__in=teams, user=self.current_user + ) # 构建字典用于快速匹配 team_roles_data = {} for relation in relations: diff --git a/app/config.py b/app/config.py index 5e90466..3c39207 100644 --- a/app/config.py +++ b/app/config.py @@ -36,7 +36,7 @@ # ----------- # i18n # ----------- -BABEL_DEFAULT_LOCALE = "zh" +BABEL_DEFAULT_LOCALE = env.get("BABEL_DEFAULT_LOCALE", "zh") BABEL_DEFAULT_TIMEZONE = "UTC" # ----------- # 其他设置 diff --git a/app/core/rbac.py b/app/core/rbac.py index b3d2a0f..e0befe6 100644 --- a/app/core/rbac.py +++ b/app/core/rbac.py @@ -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, diff --git a/app/factory.py b/app/factory.py index 69b8cc0..0c6131d 100644 --- a/app/factory.py +++ b/app/factory.py @@ -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 @@ -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 @@ -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}) diff --git a/app/models/__init__.py b/app/models/__init__.py index 97c19f9..f8c7cf0 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -6,7 +6,7 @@ from mongoengine import connect logger = logging.getLogger(__name__) -logger.setLevel(logging.DEBUG) +logger.setLevel(logging.INFO) def connect_db(config): diff --git a/app/models/file.py b/app/models/file.py index 46169a9..e9d3d68 100644 --- a/app/models/file.py +++ b/app/models/file.py @@ -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() if source is None: raise SourceNotExistError return source diff --git a/app/models/project.py b/app/models/project.py index a3893ff..bdc6db0 100644 --- a/app/models/project.py +++ b/app/models/project.py @@ -310,11 +310,11 @@ def clear(self): 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: raise ProjectSetNotExistError() - return set + return project_set def to_api(self): """ diff --git a/app/models/team.py b/app/models/team.py index 9360233..32e66e7 100644 --- a/app/models/team.py +++ b/app/models/team.py @@ -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, @@ -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) if role: role = role.to_api() return { diff --git a/app/tasks/import_from_labelplus.py b/app/tasks/import_from_labelplus.py index 95d3999..0a4c336 100644 --- a/app/tasks/import_from_labelplus.py +++ b/app/tasks/import_from_labelplus.py @@ -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) + 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) ) - 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( diff --git a/app/tasks/output_team_projects.py b/app/tasks/output_team_projects.py index 417ce2d..184995e 100644 --- a/app/tasks/output_team_projects.py +++ b/app/tasks/output_team_projects.py @@ -37,32 +37,31 @@ def output_team_projects_task(team_id, current_user_id): 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) - 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(): + # 等待一定时间后允许再次导出 + 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)) return f"成功:已创建 Team <{str(team.id)}> 所有项目的导出任务" diff --git a/app/translations/__init__.py b/app/translations/__init__.py index ed6d801..f40f4e9 100644 --- a/app/translations/__init__.py +++ b/app/translations/__init__.py @@ -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( + "%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 @@ -32,3 +36,17 @@ def get_locale() -> Optional[str]: # 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 diff --git a/app/translations/en/LC_MESSAGES/messages.po b/app/translations/en/LC_MESSAGES/messages.po index 7b1e519..b60c075 100644 --- a/app/translations/en/LC_MESSAGES/messages.po +++ b/app/translations/en/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2024-11-06 03:24+0900\n" +"POT-Creation-Date: 2024-11-30 01:30+0900\n" "PO-Revision-Date: 2017-04-14 15:38+0800\n" "Last-Translator: FULL NAME \n" "Language: en_US\n" @@ -464,7 +464,7 @@ msgstr "Therefore, all users can apply to join." #: app/core/rbac.py:69 msgid "无需审核" -msgstr "No review required." +msgstr "Auto approve." #: app/core/rbac.py:70 msgid "用户申请后直接加入" @@ -472,7 +472,7 @@ msgstr "Users join directly after applying." #: app/core/rbac.py:73 msgid "管理员审核" -msgstr "Admin Review" +msgstr "Need Admin Review" #: app/core/rbac.py:74 msgid "管理员同意申请后加入" @@ -492,7 +492,7 @@ msgstr "Modify Settings" #: app/core/rbac.py:102 msgid "新建角色" -msgstr "Create Character" +msgstr "Create Role" #: app/core/rbac.py:103 msgid "删除角色" @@ -508,7 +508,7 @@ msgstr "Invite User" #: app/core/rbac.py:107 msgid "邀请时仅可设置比自己角色等级低的用户" -msgstr "You can only invite users with a lower role level than your own." +msgstr "You can only invite users as less powerful role than your own." #: app/core/rbac.py:110 msgid "删除用户" @@ -516,7 +516,7 @@ msgstr "Delete User" #: app/core/rbac.py:111 msgid "仅可删除比自己角色等级低的用户" -msgstr "Only users with a lower role level than your own can be deleted." +msgstr "You can only remove users with less powerful role than your own." #: app/core/rbac.py:114 msgid "修改用户角色" @@ -524,7 +524,7 @@ msgstr "Modify User Role" #: app/core/rbac.py:115 msgid "仅可修改比自己角色等级低的角色" -msgstr "Can only modify characters with a lower role level than your own." +msgstr "You can only modify users with less powerful role than your own." #: app/core/rbac.py:117 msgid "修改用户备注" @@ -532,9 +532,7 @@ msgstr "Edit User Note" #: app/core/rbac.py:286 app/core/rbac.py:324 msgid "角色等级不能大于等于自己的角色等级" -msgstr "" -"Character level cannot be greater than or equal to your own character " -"level." +msgstr "Role level must be lower than your own role." #: app/core/rbac.py:288 app/core/rbac.py:326 msgid "角色权限不能多于自己的角色权限" @@ -550,7 +548,7 @@ msgstr "Cannot delete system role." #: app/core/rbac.py:390 app/core/rbac.py:391 msgid "此项目/团队不允许申请加入" -msgstr "This project/team does not allow requests to join." +msgstr "This project/team is not open for joining." #: app/core/rbac.py:466 msgid "用户不存在于团队" @@ -582,7 +580,7 @@ msgstr "Can only assign roles to users with a lower role level than yours." #: app/core/rbac.py:513 msgid "只能为用户设置比您角色等级低的角色" -msgstr "You can only assign roles to users that are lower than your role level." +msgstr "You can only assign roles that are lower than your own." #: app/core/rbac.py:541 msgid "默认角色不能设置为创建者" @@ -634,7 +632,7 @@ msgstr "This email is already registered." #: app/exceptions/auth.py:105 app/exceptions/team.py:38 msgid "仅可使用中文/日文/韩文/英文/数字/_" -msgstr "Only Chinese/Japanese/Korean/English/numbers/_ can be used." +msgstr "Only Chinese/Japanese/Korean/English alphabets/numbers/_ can be used." #: app/exceptions/auth.py:115 msgid "此昵称已被使用" @@ -996,7 +994,7 @@ msgstr "Rejected by others." #: app/models/application.py:89 msgid "已被他人允许" -msgstr "Allowed by others." +msgstr "Already approved by others." #: app/models/file.py:101 msgid "文件名为空" @@ -1328,15 +1326,15 @@ msgstr "Member" #: app/models/team.py:257 msgid "见习成员" -msgstr "Trainee Member" +msgstr "Trainee" #: app/models/user.py:189 msgid "令牌格式错误,应形如 Bearer x.x.x" -msgstr "Token format error, should be like Bearer x.x.x." +msgstr "Illegal auth token. It should be like Bearer x.x.x." #: app/models/user.py:204 msgid "密码已修改,请重新登录" -msgstr "Password has been changed, please log in again." +msgstr "Password updated. Please log in again." #: app/models/user.py:405 msgid "此用户已有申请,已直接加入" @@ -1348,37 +1346,35 @@ msgstr "This user is a member of the project team and has been directly added." #: app/models/user.py:418 msgid "邀请成功,请等待用户确认" -msgstr "Invitation successful, please wait for user confirmation." +msgstr "Invited. Please wait for the user to confirm." #: app/models/user.py:449 msgid "加入成功,管理员继承自团队" -msgstr "Joined successfully, administrator inherited from the team." +msgstr "Joined successfully as administrator due to team settings." #: app/models/user.py:462 msgid "管理员先前邀请了您,已直接加入" -msgstr "" -"The administrator previously invited you, and you have been directly " -"added." +msgstr "The administrator previously invited you, so you have been directly added." #: app/models/user.py:469 msgid "项目不允许申请加入" -msgstr "The project does not allow requests to join." +msgstr "The project is not open for joining." #: app/models/user.py:475 msgid "加入成功" -msgstr "Joined successfully." +msgstr "Successfully joined." #: app/models/user.py:478 msgid "申请成功,请等待管理员审核" -msgstr "Application successful, please wait for administrator review." +msgstr "Application Sent. Please wait for administrator to review." #: app/models/v_code.py:304 msgid "重置您的安全邮箱" -msgstr "Reset your security email." +msgstr "Reset your recovery email." #: app/models/v_code.py:305 msgid "确认您的安全邮箱" -msgstr "Confirm your secure email." +msgstr "Confirm your recovery email." #: app/models/v_code.py:306 msgid "重置您的密码" @@ -1394,7 +1390,7 @@ msgstr "Cannot be empty." #: app/validators/custom_validate.py:35 msgid "此项不可选" -msgstr "This option is not selectable." +msgstr "Option not available." #: app/validators/custom_validate.py:76 app/validators/custom_validate.py:79 #: app/validators/custom_validate.py:109 app/validators/custom_validate.py:124 @@ -1447,3 +1443,6 @@ msgstr "No valid parameters." #~ msgid "此邮箱已存在,您可以直接登录或尝试其他邮箱" #~ msgstr "" +#~ msgid "站点支持语言: " +#~ msgstr "" + diff --git a/app/translations/zh/LC_MESSAGES/messages.po b/app/translations/zh/LC_MESSAGES/messages.po index 3e44e43..257fb17 100644 --- a/app/translations/zh/LC_MESSAGES/messages.po +++ b/app/translations/zh/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2024-11-06 03:24+0900\n" +"POT-Creation-Date: 2024-11-30 01:30+0900\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language: zh\n" @@ -1381,3 +1381,6 @@ msgstr "没有有效参数" #~ msgid "见习成员" #~ msgstr "见习成员" +#~ msgid "站点支持语言: " +#~ msgstr "" +