🌐 支持i18n (#501)

*  支持 i18n #410

* 🚨 更改noqa方式

* 📝 添加 CONTRIBUTING.md 文件

* 🌐 将 i18n 默认语言设置为 en-US

* 📝 添加英文版 CONTRIBUTING.md
This commit is contained in:
呵呵です
2024-10-26 18:29:51 +08:00
committed by GitHub
parent a0fd9eaed3
commit b2d5a1e729
17 changed files with 374 additions and 15 deletions

67
CONTRIBUTING.en-US.md Normal file
View File

@@ -0,0 +1,67 @@
# How to Contribute?
## Setting Up the Environment
### For Developers with Basic Python Knowledge
First, you need [Python **3.10**](https://www.python.org/) and [Poetry](https://python-poetry.org/).
Then, you need to clone this repository to your local machine using the `git clone` command, and install dependencies using `poetry install --sync`.
### For Developers with Limited Python Knowledge
Here are **my recommended** best practices:
```bash
# Install uv
# Please refer to https://docs.astral.sh/uv/getting-started/installation/
# Set up the basic Python environment
uv python install 3.10
uv tool install poetry
# Clone the repository
git clone https://github.com/A-Minos/nonebot-plugin-tetris-stats.git
cd nonebot-plugin-tetris-stats
# Install dependencies
uv run --no-project --python 3.10 poetry env use python
poetry install --sync
```
## Development
### Code Development
1. For static code analysis, use [ruff](https://docs.astral.sh/ruff/). You can install the corresponding plugin for your IDE or use the command line with `ruff check ./nonebot_plugin_tetris_stats/` to check the code.
2. For code formatting, use [ruff](https://docs.astral.sh/ruff/). You can install the corresponding plugin for your IDE or use the command line with `ruff format ./nonebot_plugin_tetris_stats/` to format the code.
3. For type checking, use both [basedpyright](https://docs.basedpyright.com/latest/) and [mypy](https://www.mypy-lang.org/). You can install the corresponding plugins for your IDE or use the following commands in the terminal to check the code:
```bash
# basedpyright
basedpyright ./nonebot_plugin_tetris_stats/
# mypy
mypy ./nonebot_plugin_tetris_stats/
```
### Internationalization
This project uses [Tarina](https://github.com/ArcletProject/Tarina) for internationalization support.
#### Adding a New Language
1. Navigate to the `./nonebot_plugin_tetris_stats/i18n/` directory.
2. Run `tarina-lang create {language_code}` * Please note that the language code should preferably follow the [IETF language tag](https://en.wikipedia.org/wiki/IETF_language_tag) standard.
3. Edit the generated `./nonebot_plugin_tetris_stats/i18n/{language_code}.json` file.
#### Updating an Existing Language
1. Navigate to the `./nonebot_plugin_tetris_stats/i18n/` directory.
2. Edit the corresponding `./nonebot_plugin_tetris_stats/i18n/{language_code}.json` file.
#### Adding New Entries
1. Navigate to the `./nonebot_plugin_tetris_stats/i18n/` directory.
2. Edit the `.template.json` file.
3. Run `tarina-lang schema && tarina-lang model`.
4. Modify the language files, adding new entries at least to `en-US.json`.

68
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,68 @@
# 我该如何参与开发?
## 配置环境
### 对于有一定 Python 基础的开发者
首先你需要 [Python **3.10**](https://www.python.org/) 以及 [Poetry](https://python-poetry.org/)。
然后你需要使用`git clone`命令将本仓库克隆到本地,然后使用`poetry install --sync`安装依赖。
### 对于基础不是很好的开发者
以下是**我认为的**最佳实践:
```bash
# 安装 uv
# 请参考 https://docs.astral.sh/uv/getting-started/installation/
# 配置基础 Python 环境
uv python install 3.10
uv tool install poetry
# 克隆仓库
git clone https://github.com/A-Minos/nonebot-plugin-tetris-stats.git
cd nonebot-plugin-tetris-stats
# 安装依赖
uv run --no-project --python 3.10 poetry env use python
poetry install --sync
```
## 开发
### 代码开发
1. 代码静态检查使用 [ruff](https://docs.astral.sh/ruff/)你可以为你的ide安装对应插件来使用也可以在命令行使用`ruff check ./nonebot_plugin_tetris_stats/`来检查代码。
2. 代码格式化使用 [ruff](https://docs.astral.sh/ruff/)你可以为你的ide安装对应插件来使用也可以在命令行使用`ruff format ./nonebot_plugin_tetris_stats/`来格式化代码。
3. 类型检查同时使用 [basedpyright](https://docs.basedpyright.com/latest/) 和 [mypy](https://www.mypy-lang.org/)你可以为你的ide安装对应插件来使用。
也可以在命令行使用下面的命令来检查代码:
```bash
# basedpyright
basedpyright ./nonebot_plugin_tetris_stats/
# mypy
mypy ./nonebot_plugin_tetris_stats/
```
### 国际化
本项目使用 [Tarina](https://github.com/ArcletProject/Tarina) 提供国际化支持。
#### 添加新的语言
1. 进入 `./nonebot_plugin_tetris_stats/i18n/` 目录。
2. 运行 `tarina-lang create {语言代码}` * 请注意,语言代码最好符合 [IETF语言标签](https://zh.wikipedia.org/wiki/IETF%E8%AF%AD%E8%A8%80%E6%A0%87%E7%AD%BE) 的规范。
3. 编辑生成的 `./nonebot_plugin_tetris_stats/i18n/{语言代码}.json` 文件。
#### 更新已有语言
1. 进入 `./nonebot_plugin_tetris_stats/i18n/` 目录。
2. 编辑对应的 `./nonebot_plugin_tetris_stats/i18n/{语言代码}.json` 文件。
#### 添加新的条目
1. 进入 `./nonebot_plugin_tetris_stats/i18n/` 目录。
2. 编辑 `.template.json` 文件。
3. 运行 `tarina-lang schema && tarina-lang model`
4. 修改语言文件,至少为`en-US.json`添加新的条目。

View File

@@ -7,6 +7,7 @@ from nonebot.typing import T_Handler
from nonebot_plugin_alconna import AlcMatches, Alconna, At, CommandMeta, on_alconna from nonebot_plugin_alconna import AlcMatches, Alconna, At, CommandMeta, on_alconna
from .. import ns from .. import ns
from ..i18n.model import Lang
from ..utils.exception import MessageFormatError, NeedCatchError from ..utils.exception import MessageFormatError, NeedCatchError
command: Alconna = Alconna( command: Alconna = Alconna(
@@ -30,7 +31,7 @@ def add_block_handlers(handler: Callable[[T_Handler], T_Handler]) -> None:
@handler @handler
async def _(bot: Bot, matcher: Matcher, target: At): async def _(bot: Bot, matcher: Matcher, target: At):
if isinstance(target, At) and target.target == bot.self_id: if isinstance(target, At) and target.target == bot.self_id:
await matcher.finish('不能查询bot的信息') await matcher.finish(Lang.interaction.wrong.query_bot())
from . import tetrio, top, tos # noqa: F401, E402 from . import tetrio, top, tos # noqa: F401, E402

View File

@@ -1 +0,0 @@
CANT_VERIFY_MESSAGE = '* 由于无法验证绑定信息, 不能保证查询到的用户为本人\n'

View File

@@ -14,10 +14,10 @@ from nonebot_plugin_user import get_user
from sqlalchemy import select from sqlalchemy import select
from ....db import query_bind_info, trigger from ....db import query_bind_info, trigger
from ....i18n import Lang
from ....utils.exception import FallbackError from ....utils.exception import FallbackError
from ....utils.typing import Me from ....utils.typing import Me
from ... import add_block_handlers, alc from ... import add_block_handlers, alc
from ...constant import CANT_VERIFY_MESSAGE
from .. import command, get_player from .. import command, get_player
from ..api import Player from ..api import Player
from ..constant import GAME_TYPE from ..constant import GAME_TYPE
@@ -113,9 +113,10 @@ async def _( # noqa: PLR0913
) )
if bind is None: if bind is None:
await matcher.finish('未查询到绑定信息') await matcher.finish('未查询到绑定信息')
message = UniMessage(CANT_VERIFY_MESSAGE)
player = Player(user_id=bind.game_account, trust=True) player = Player(user_id=bind.game_account, trust=True)
await (message + await make_query_result(player, template or 'v1')).finish() await (
UniMessage.i18n(Lang.interaction.warning.unverified) + await make_query_result(player, template or 'v1')
).finish()
@alc.assign('TETRIO.query') @alc.assign('TETRIO.query')

View File

@@ -13,6 +13,7 @@ from nonebot_plugin_user import get_user
from yarl import URL from yarl import URL
from ....db import query_bind_info, trigger from ....db import query_bind_info, trigger
from ....i18n import Lang
from ....utils.exception import RecordNotFoundError from ....utils.exception import RecordNotFoundError
from ....utils.host import HostPage, get_self_netloc from ....utils.host import HostPage, get_self_netloc
from ....utils.metrics import get_metrics from ....utils.metrics import get_metrics
@@ -22,7 +23,6 @@ from ....utils.render.schemas.tetrio.record.base import Finesse, Max, Mini, Tspi
from ....utils.render.schemas.tetrio.record.blitz import Record, Statistic from ....utils.render.schemas.tetrio.record.blitz import Record, Statistic
from ....utils.screenshot import screenshot from ....utils.screenshot import screenshot
from ....utils.typing import Me from ....utils.typing import Me
from ...constant import CANT_VERIFY_MESSAGE
from .. import alc from .. import alc
from ..api.player import Player from ..api.player import Player
from ..constant import GAME_TYPE from ..constant import GAME_TYPE
@@ -60,9 +60,10 @@ async def _(
) )
if bind is None: if bind is None:
await matcher.finish('未查询到绑定信息') await matcher.finish('未查询到绑定信息')
message = UniMessage(CANT_VERIFY_MESSAGE)
player = Player(user_id=bind.game_account, trust=True) player = Player(user_id=bind.game_account, trust=True)
await (message + UniMessage.image(raw=await make_blitz_image(player))).finish() await (
UniMessage.i18n(Lang.interaction.warning.unverified) + UniMessage.image(raw=await make_blitz_image(player))
).finish()
@alc.assign('TETRIO.record.blitz') @alc.assign('TETRIO.record.blitz')

View File

@@ -13,6 +13,7 @@ from nonebot_plugin_user import get_user
from yarl import URL from yarl import URL
from ....db import query_bind_info, trigger from ....db import query_bind_info, trigger
from ....i18n import Lang
from ....utils.exception import RecordNotFoundError from ....utils.exception import RecordNotFoundError
from ....utils.host import HostPage, get_self_netloc from ....utils.host import HostPage, get_self_netloc
from ....utils.metrics import get_metrics from ....utils.metrics import get_metrics
@@ -22,7 +23,6 @@ from ....utils.render.schemas.tetrio.record.base import Finesse, Max, Mini, Stat
from ....utils.render.schemas.tetrio.record.sprint import Record from ....utils.render.schemas.tetrio.record.sprint import Record
from ....utils.screenshot import screenshot from ....utils.screenshot import screenshot
from ....utils.typing import Me from ....utils.typing import Me
from ...constant import CANT_VERIFY_MESSAGE
from .. import alc from .. import alc
from ..api.player import Player from ..api.player import Player
from ..constant import GAME_TYPE from ..constant import GAME_TYPE
@@ -60,9 +60,10 @@ async def _(
) )
if bind is None: if bind is None:
await matcher.finish('未查询到绑定信息') await matcher.finish('未查询到绑定信息')
message = UniMessage(CANT_VERIFY_MESSAGE)
player = Player(user_id=bind.game_account, trust=True) player = Player(user_id=bind.game_account, trust=True)
await (message + UniMessage.image(raw=await make_sprint_image(player))).finish() await (
UniMessage.i18n(Lang.interaction.warning.unverified) + UniMessage.image(raw=await make_sprint_image(player))
).finish()
@alc.assign('TETRIO.record.sprint') @alc.assign('TETRIO.record.sprint')

View File

@@ -8,6 +8,7 @@ from nonebot_plugin_session_orm import get_session_persist_id # type: ignore[im
from nonebot_plugin_user import get_user from nonebot_plugin_user import get_user
from ...db import query_bind_info, trigger from ...db import query_bind_info, trigger
from ...i18n import Lang
from ...utils.exception import FallbackError from ...utils.exception import FallbackError
from ...utils.host import HostPage, get_self_netloc from ...utils.host import HostPage, get_self_netloc
from ...utils.metrics import TetrisMetricsBasicWithLPM, get_metrics from ...utils.metrics import TetrisMetricsBasicWithLPM, get_metrics
@@ -18,7 +19,6 @@ from ...utils.render.schemas.top_info import Data as InfoData
from ...utils.render.schemas.top_info import Info from ...utils.render.schemas.top_info import Info
from ...utils.screenshot import screenshot from ...utils.screenshot import screenshot
from ...utils.typing import Me from ...utils.typing import Me
from ..constant import CANT_VERIFY_MESSAGE
from . import alc from . import alc
from .api import Player from .api import Player
from .api.schemas.user_profile import Data, UserProfile from .api.schemas.user_profile import Data, UserProfile
@@ -44,7 +44,7 @@ async def _(event: Event, matcher: Matcher, target: At | Me, event_session: Even
if bind is None: if bind is None:
await matcher.finish('未查询到绑定信息') await matcher.finish('未查询到绑定信息')
await ( await (
UniMessage(CANT_VERIFY_MESSAGE) UniMessage.i18n(Lang.interaction.warning.unverified)
+ await make_query_result(await Player(user_name=bind.game_account, trust=True).get_profile()) + await make_query_result(await Player(user_name=bind.game_account, trust=True).get_profile())
).finish() ).finish()

View File

@@ -14,6 +14,7 @@ from nonebot_plugin_user import get_user
from nonebot_plugin_userinfo import EventUserInfo, UserInfo from nonebot_plugin_userinfo import EventUserInfo, UserInfo
from ...db import query_bind_info, trigger from ...db import query_bind_info, trigger
from ...i18n import Lang
from ...utils.exception import RequestError from ...utils.exception import RequestError
from ...utils.host import HostPage, get_self_netloc from ...utils.host import HostPage, get_self_netloc
from ...utils.image import get_avatar from ...utils.image import get_avatar
@@ -24,7 +25,6 @@ from ...utils.render.schemas.base import People, Ranking
from ...utils.render.schemas.tos_info import Info, Multiplayer, Radar from ...utils.render.schemas.tos_info import Info, Multiplayer, Radar
from ...utils.screenshot import screenshot from ...utils.screenshot import screenshot
from ...utils.typing import Me, Number from ...utils.typing import Me, Number
from ..constant import CANT_VERIFY_MESSAGE
from . import alc from . import alc
from .api import Player from .api import Player
from .api.schemas.user_info import UserInfoSuccess from .api.schemas.user_info import UserInfoSuccess
@@ -124,7 +124,7 @@ async def _(
) )
if bind is None: if bind is None:
await matcher.finish('未查询到绑定信息') await matcher.finish('未查询到绑定信息')
message = CANT_VERIFY_MESSAGE message = UniMessage.i18n(Lang.interaction.warning.unverified)
player = Player(teaid=bind.game_account, trust=True) player = Player(teaid=bind.game_account, trust=True)
user_info, game_data = await gather(player.get_info(), get_game_data(player)) user_info, game_data = await gather(player.get_info(), get_game_data(player))
if game_data is not None: if game_data is not None:

View File

@@ -0,0 +1,5 @@
{
"default": "en-US",
"frozen": [],
"require": []
}

View File

@@ -0,0 +1,72 @@
{
"title": "Lang Schema",
"description": "Schema for lang file",
"type": "object",
"properties": {
"interaction": {
"title": "Interaction",
"description": "Scope 'interaction' of lang item",
"type": "object",
"additionalProperties": false,
"properties": {
"wrong": {
"title": "Wrong",
"description": "Scope 'wrong' of lang item",
"type": "object",
"additionalProperties": false,
"properties": {
"query_bot": {
"title": "query_bot",
"description": "value of lang item type 'query_bot'",
"type": "string"
}
}
},
"warning": {
"title": "Warning",
"description": "Scope 'warning' of lang item",
"type": "object",
"additionalProperties": false,
"properties": {
"unverified": {
"title": "unverified",
"description": "value of lang item type 'unverified'",
"type": "string"
}
}
}
}
},
"error": {
"title": "Error",
"description": "Scope 'error' of lang item",
"type": "object",
"additionalProperties": false,
"properties": {
"MessageFormatError": {
"title": "Messageformaterror",
"description": "Scope 'MessageFormatError' of lang item",
"type": "object",
"additionalProperties": false,
"properties": {
"TETR.IO": {
"title": "TETR.IO",
"description": "value of lang item type 'TETR.IO'",
"type": "string"
},
"TOS": {
"title": "TOS",
"description": "value of lang item type 'TOS'",
"type": "string"
},
"TOP": {
"title": "TOP",
"description": "value of lang item type 'TOP'",
"type": "string"
}
}
}
}
}
}
}

View File

@@ -0,0 +1,16 @@
{
"$schema": ".template.schema.json",
"scopes": [
{
"scope": "interaction",
"types": [
{ "subtype": "wrong", "types": ["query_bot"] },
{ "subtype": "warning", "types": ["unverified"] }
]
},
{
"scope": "error",
"types": [{ "subtype": "MessageFormatError", "types": ["TETR.IO", "TOS", "TOP"] }]
}
]
}

View File

@@ -0,0 +1,54 @@
{
"title": "Template",
"description": "Template for lang items to generate schema for lang files",
"type": "object",
"properties": {
"scopes": {
"title": "Scopes",
"description": "All scopes of lang items",
"type": "array",
"uniqueItems": true,
"items": {
"title": "Scope",
"description": "First level of all lang items",
"type": "object",
"properties": {
"scope": {
"type": "string",
"description": "Scope name"
},
"types": {
"type": "array",
"description": "All types of lang items",
"uniqueItems": true,
"items": {
"oneOf": [
{
"type": "string",
"description": "Value of lang item"
},
{
"type": "object",
"properties": {
"subtype": {
"type": "string",
"description": "Subtype name of lang item"
},
"types": {
"type": "array",
"description": "All subtypes of lang items",
"uniqueItems": true,
"items": {
"$ref": "#/properties/scopes/items/properties/types/items"
}
}
}
}
]
}
}
}
}
}
}
}

View File

@@ -0,0 +1,12 @@
# This file is @generated by tarina.lang CLI tool
# It is not intended for manual editing.
# ruff: noqa: E402, F401, PLC0414
from pathlib import Path
from tarina.lang import lang # type: ignore[import-untyped]
lang.load(Path(__file__).parent)
from .model import Lang as Lang

View File

@@ -0,0 +1,16 @@
{
"$schema": ".lang.schema.json",
"interaction": {
"wrong": { "query_bot": "Can't query bot's information" },
"warning": {
"unverified": "* Because I can't verify account linking information, I can't guarantee the info I found is yourself/themself."
}
},
"error": {
"MessageFormatError": {
"TETR.IO": "Username/ID is invalid",
"TOS": "Username/ID is invalid",
"TOP": "Username is invalid"
}
}
}

View File

@@ -0,0 +1,32 @@
# This file is @generated by tarina.lang CLI tool
# It is not intended for manual editing.
from tarina.lang.model import LangItem, LangModel # type: ignore[import-untyped]
class InteractionWrong:
query_bot: LangItem = LangItem('interaction', 'wrong.query_bot')
class InteractionWarning:
unverified: LangItem = LangItem('interaction', 'warning.unverified')
class Interaction:
wrong = InteractionWrong
warning = InteractionWarning
class ErrorMessageformaterror:
TETR_IO: LangItem = LangItem('error', 'MessageFormatError.TETR.IO')
TOS: LangItem = LangItem('error', 'MessageFormatError.TOS')
TOP: LangItem = LangItem('error', 'MessageFormatError.TOP')
class Error:
MessageFormatError = ErrorMessageformaterror
class Lang(LangModel):
interaction = Interaction
error = Error

View File

@@ -0,0 +1,14 @@
{
"$schema": ".lang.schema.json",
"interaction": {
"wrong": { "query_bot": "不能查询bot的信息" },
"warning": { "unverified": "* 由于无法验证绑定信息, 不能保证查询到的用户为本人" }
},
"error": {
"MessageFormatError": {
"TETR.IO": "用户名/ID不合法",
"TOS": "用户名/ID不合法",
"TOP": "用户名不合法"
}
}
}