feat: 国际化支持,英文源串+中文翻译

全部44处qsTr源串改为英文,新建translations/jnetapplet_zh_CN.ts
中文翻译文件。CMake增加LinguistTools+qt_add_translation编译
.ts->.qm,init()按系统语言加载.qm并安装到qApp。

中文系统显示中文翻译,英文系统显示英文源串。
This commit is contained in:
2026-07-24 11:40:12 +08:00
parent 43fdcc8b0a
commit fba506d9d8
9 changed files with 318 additions and 48 deletions
+15 -1
View File
@@ -6,7 +6,7 @@ set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)
find_package(Qt6 REQUIRED COMPONENTS Core Quick DBus Network)
find_package(Qt6 REQUIRED COMPONENTS Core Quick DBus Network LinguistTools)
find_package(Dtk6 REQUIRED COMPONENTS Core)
find_package(DDEShell REQUIRED)
@@ -23,6 +23,17 @@ set_target_properties(space.jokul.JNetApplet PROPERTIES PREFIX "")
# 将 project(VERSION) 暴露为编译宏,供 C++ version() 兜底使用
target_compile_definitions(space.jokul.JNetApplet PRIVATE PROJECT_VERSION="${PROJECT_VERSION}")
# 翻译文件:编译 .ts -> .qm,安装到插件目录的 translations/ 子目录
# 设计原因:QML 中 qsTr() 源串为英文,中文翻译通过 .qm 文件在运行时加载
set(TS_FILES ${CMAKE_CURRENT_SOURCE_DIR}/translations/jnetapplet_zh_CN.ts)
qt_add_translation(QM_FILES ${TS_FILES})
# 翻译文件安装路径,暴露为编译宏供 C++ 加载
target_compile_definitions(space.jokul.JNetApplet PRIVATE
TRANSLATIONS_DIR="/usr/share/dde-shell/${PLUGIN_ID}/translations"
)
# 确保 .qm 在构建时生成(qt_add_translation 默认仅 install 时触发)
add_custom_target(translations ALL DEPENDS ${QM_FILES})
# 头文件搜索路径指向 src/,使 #include "networkmonitorapplet.h" 可被外部解析
target_include_directories(space.jokul.JNetApplet PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
@@ -52,3 +63,6 @@ install(FILES package/networkview.qml DESTINATION /usr/share/dde-shell/${PLUGIN_
# 设计原因:拆分出的子组件需随主 QML 一起安装到 dde-shell 插件目录,
# 否则 networkview.qml 的 import "components" 在运行时找不到类型
install(DIRECTORY package/components DESTINATION /usr/share/dde-shell/${PLUGIN_ID})
# 安装编译后的翻译文件
install(FILES ${QM_FILES} DESTINATION /usr/share/dde-shell/${PLUGIN_ID}/translations)
+3 -2
View File
@@ -75,8 +75,9 @@
~~项目结构只列 AboutWindow.qml,功能特性未提及设置窗口/字体颜色/流量波动图/IPv6/深色模式,README 写"从弹窗卸载"实际在设置窗口。~~
已更新 README.md 和 AGENTS.md 的功能特性列表、项目结构(补全 6 个组件)、C++ Backend 属性列表(移除 interfaceStats,新增 ipv6Address/version/textColor/speedHistory 等)。
**16. `qsTr()` 源字符串为中文,无翻译基础设施**
QML 中 `qsTr("网络速度监控")`以中文为源串,但项目`.ts` 翻译文件、无 `lupdate`/`lrelease` 构建步骤、C++ 无翻译加载逻辑。`qsTr()` 实质为空操作。若仅面向中文用户可接受;若计划国际化,需补全 i18n 基础设施并以英文为源串。
**16. ~~`qsTr()` 源字符串为中文,无翻译基础设施~~ ✅ 已修复**
~~QML 中 `qsTr()` 以中文为源串,无 `.ts` 翻译文件、无 `lupdate`/`lrelease` 构建步骤、C++ 无翻译加载逻辑。~~
已将全部 44 处 `qsTr` 源串改为英文,新建 `translations/jnetapplet_zh_CN.ts` 中文翻译文件。CMake 增加 `LinguistTools` + `qt_add_translation` 编译 `.ts` -> `.qm``init()` 中按系统语言加载 `.qm` 并安装到 `qApp`
**17. C++ 后端无单元测试**
项目无任何测试。`calculateSpeed``readNetworkStats` 的正则解析、`isPhysicalInterface``niceCeil`(QML)等纯逻辑函数适合且应该有单元测试覆盖。
+8 -8
View File
@@ -83,7 +83,7 @@ Window {
anchors.left: parent.left
anchors.leftMargin: 16
anchors.verticalCenter: parent.verticalCenter
text: qsTr("关于")
text: qsTr("About")
font.pixelSize: 15
font.weight: Font.Bold
color: root.textPrimary
@@ -130,7 +130,7 @@ Window {
spacing: 2
Text {
text: qsTr("网络速度监控")
text: qsTr("Network Speed Monitor")
font.pixelSize: 18
font.weight: Font.Bold
color: root.textPrimary
@@ -179,7 +179,7 @@ Window {
spacing: 0
Text {
text: qsTr("版本")
text: qsTr("Version")
font.pixelSize: 12
color: "#0081FF"
Layout.preferredWidth: 56
@@ -200,7 +200,7 @@ Window {
spacing: 0
Text {
text: qsTr("作者")
text: qsTr("Author")
font.pixelSize: 12
color: "#0081FF"
Layout.preferredWidth: 56
@@ -221,7 +221,7 @@ Window {
spacing: 0
Text {
text: qsTr("描述")
text: qsTr("Description")
font.pixelSize: 12
color: "#0081FF"
Layout.preferredWidth: 56
@@ -229,7 +229,7 @@ Window {
}
Text {
text: qsTr("监控网络速度和流量")
text: qsTr("Monitor network speed and traffic")
font.pixelSize: 12
color: root.textPrimary
Layout.fillWidth: true
@@ -243,7 +243,7 @@ Window {
spacing: 0
Text {
text: qsTr("仓库")
text: qsTr("Repository")
font.pixelSize: 12
color: "#0081FF"
Layout.preferredWidth: 56
@@ -281,7 +281,7 @@ Window {
Text {
id: copiedHintText
anchors.centerIn: parent
text: qsTr("已复制")
text: qsTr("Copied")
font.pixelSize: 10
color: "#22A34A"
}
+7 -7
View File
@@ -95,7 +95,7 @@ Control {
spacing: 6
Text {
text: qsTr("网络速度监控")
text: qsTr("Network Speed Monitor")
font.pixelSize: 15
font.weight: Font.Bold
color: popup.primaryText
@@ -126,7 +126,7 @@ Control {
}
Text {
text: popup.ipAddress ? qsTr("IPv4") + popup.ipAddress : ""
text: popup.ipAddress ? qsTr("IPv4: ") + popup.ipAddress : ""
font.pixelSize: 13
color: popup.secondaryText
visible: popup.ipAddress !== ""
@@ -135,7 +135,7 @@ Control {
// IPv6 全球地址行,无 IPv6 时隐藏不占位
Text {
text: popup.ipv6Address ? qsTr("IPv6") + popup.ipv6Address : ""
text: popup.ipv6Address ? qsTr("IPv6: ") + popup.ipv6Address : ""
font.pixelSize: 13
color: popup.secondaryText
visible: popup.ipv6Address !== ""
@@ -187,7 +187,7 @@ Control {
}
Text {
text: qsTr("下载")
text: qsTr("Download")
font.pixelSize: 11
color: popup.secondaryText
Layout.alignment: Qt.AlignHCenter
@@ -244,7 +244,7 @@ Control {
}
Text {
text: qsTr("上传")
text: qsTr("Upload")
font.pixelSize: 11
color: popup.secondaryText
Layout.alignment: Qt.AlignHCenter
@@ -279,7 +279,7 @@ Control {
Item { Layout.fillWidth: true }
Text {
text: qsTr("总计")
text: qsTr("Total")
font.pixelSize: 11
font.weight: Font.Bold
color: popup.secondaryText
@@ -456,7 +456,7 @@ Control {
}
Text {
text: qsTr("未检测到网络接口")
text: qsTr("No network interface detected")
font.pixelSize: 12
color: popup.secondaryText
Layout.alignment: Qt.AlignHCenter
+20 -20
View File
@@ -51,7 +51,7 @@ Window {
readonly property color selText: isDarkMode ? "#64B5F6" : "#1565C0"
// 卸载按钮文字:复制命令后临时显示提示,定时器到期后还原
property string uninstallButtonText: qsTr("卸载插件")
property string uninstallButtonText: qsTr("Uninstall Plugin")
width: 350
height: 390
@@ -70,15 +70,15 @@ Window {
// 设计原因:用户面对多个网口时难以仅凭 enp3s0/wlp3s0 等命名判断用途,
// 加一行类型说明(有线/无线/VPN 等)降低认知负担
function interfaceDescription(name) {
if (name === "lo") return qsTr("本地回环")
if (name === "Meta") return qsTr("虚拟接口")
if (/^enp|^eth/.test(name)) return qsTr("有线网络")
if (/^wlp|^wlan/.test(name)) return qsTr("无线网络")
if (/^docker|^veth/.test(name)) return qsTr("容器网络")
if (/^br/.test(name)) return qsTr("桥接")
if (name === "lo") return qsTr("Loopback")
if (name === "Meta") return qsTr("Virtual Interface")
if (/^enp|^eth/.test(name)) return qsTr("Wired Network")
if (/^wlp|^wlan/.test(name)) return qsTr("Wireless Network")
if (/^docker|^veth/.test(name)) return qsTr("Container Network")
if (/^br/.test(name)) return qsTr("Bridge")
if (/^tun|^tap/.test(name)) return qsTr("VPN")
if (/^virbr/.test(name)) return qsTr("虚拟桥接")
return qsTr("其他")
if (/^virbr/.test(name)) return qsTr("Virtual Bridge")
return qsTr("Other")
}
// 根据接口名返回类型图标(Unicode 符号),用于网络接口列表每行左侧
@@ -139,7 +139,7 @@ Window {
anchors.left: parent.left
anchors.leftMargin: 16
anchors.verticalCenter: parent.verticalCenter
text: qsTr("设置")
text: qsTr("Settings")
font.pixelSize: 15
font.weight: Font.Bold
color: root.textPrimary
@@ -193,7 +193,7 @@ Window {
// 网络接口选择区
Text {
text: qsTr("网络接口")
text: qsTr("Network Interface")
font.pixelSize: 13
font.weight: Font.Bold
color: root.textSecondary
@@ -346,7 +346,7 @@ Window {
// 无接口提示:在卡片内居中
Text {
anchors.centerIn: parent
text: qsTr("未检测到网络接口")
text: qsTr("No network interface detected")
font.pixelSize: 12
color: root.textTertiary
visible: networkInterfaces.length === 0
@@ -357,7 +357,7 @@ Window {
// 设计原因:用户选择的颜色通过 applet.textColor 持久化到
// ~/.config/jnetapplet/settings.ini,重启 dde-shell 后仍生效
Text {
text: qsTr("字体颜色")
text: qsTr("Font Color")
font.pixelSize: 13
font.weight: Font.Bold
color: root.textSecondary
@@ -380,7 +380,7 @@ Window {
Layout.fillWidth: true
Layout.preferredHeight: 40
// 提示状态时背景和边框变绿
readonly property bool isStatus: uninstallButtonText !== qsTr("卸载插件")
readonly property bool isStatus: uninstallButtonText !== qsTr("Uninstall Plugin")
color: isStatus
? Qt.rgba(22/255, 163/255, 74/255, 0.08)
: (uninstallMouse.containsMouse ? Qt.rgba(220/255, 38/255, 38/255, 0.15) : Qt.rgba(220/255, 38/255, 38/255, 0.08))
@@ -443,7 +443,7 @@ Window {
anchors.margins: 20
Text {
text: qsTr("警告")
text: qsTr("Warning")
font.pixelSize: 16
font.weight: Font.Bold
color: accentColor
@@ -451,7 +451,7 @@ Window {
}
Text {
text: qsTr("以下命令用于卸载插件并重启 dde-shell,请复制到终端中执行:")
text: qsTr("The following command uninstalls the plugin and restarts dde-shell. Please copy and run it in terminal:")
font.pixelSize: 12
color: root.textPrimary
Layout.alignment: Qt.AlignHCenter
@@ -496,7 +496,7 @@ Window {
Text {
anchors.centerIn: parent
text: qsTr("取消")
text: qsTr("Cancel")
font.pixelSize: 13
color: root.textPrimary
}
@@ -519,7 +519,7 @@ Window {
Text {
anchors.centerIn: parent
text: qsTr("复制命令")
text: qsTr("Copy Command")
font.pixelSize: 13
font.weight: Font.Medium
color: "white"
@@ -536,7 +536,7 @@ Window {
clipboardHelper.copy()
uninstallConfirmDialog.close()
// 卸载按钮文字临时替换为复制成功提示,5 秒后还原
uninstallButtonText = qsTr("卸载命令已复制到剪贴板,请在终端中粘贴执行")
uninstallButtonText = qsTr("Uninstall command copied to clipboard. Please paste and run it in terminal.")
statusHideTimer.start()
}
}
@@ -557,6 +557,6 @@ Window {
Timer {
id: statusHideTimer
interval: 5000
onTriggered: uninstallButtonText = qsTr("卸载插件")
onTriggered: uninstallButtonText = qsTr("Uninstall Plugin")
}
}
+3 -3
View File
@@ -177,7 +177,7 @@ Window {
anchors.left: parent.left
anchors.leftMargin: 16
anchors.verticalCenter: parent.verticalCenter
text: qsTr("流量波动图")
text: qsTr("Traffic Chart")
font.pixelSize: 15
font.weight: Font.Bold
color: root.textPrimary
@@ -421,7 +421,7 @@ Window {
ctx.font = "13px sans-serif"
ctx.textAlign = "center"
ctx.textBaseline = "middle"
ctx.fillText(qsTr("暂无历史数据"), pl + pw / 2, pt + ph / 2)
ctx.fillText(qsTr("No history data"), pl + pw / 2, pt + ph / 2)
return
}
@@ -578,7 +578,7 @@ Window {
Text {
anchors.centerIn: parent
text: root.hoverHint.length > 0
? root.hoverHint : qsTr("鼠标移到曲线上查看详情")
? root.hoverHint : qsTr("Hover over the curve for details")
font.pixelSize: 11
color: root.hoverHint.length > 0 ? root.textPrimary : root.textTertiary
}
+6 -6
View File
@@ -222,9 +222,9 @@ AppletItem {
id: toolTip
text: root.ready
? (root.activeInterface
+ " · IPv4" + (root.ipAddress || qsTr(""))
+ (root.ipv6Address ? "\nIPv6" + root.ipv6Address : ""))
: qsTr("未检测到网络接口")
+ " · " + qsTr("IPv4: ") + (root.ipAddress || qsTr("None"))
+ (root.ipv6Address ? "\n" + qsTr("IPv6: ") + root.ipv6Address : ""))
: qsTr("No network interface detected")
toolTipX: DockPanelPositioner.x
toolTipY: DockPanelPositioner.y
}
@@ -300,7 +300,7 @@ AppletItem {
id: contextMenu
Platform.MenuItem {
text: qsTr("设置")
text: qsTr("Settings")
onTriggered: {
settingsWindow.show()
settingsWindow.raise()
@@ -310,7 +310,7 @@ AppletItem {
// 流量波动图:屏幕居中独立窗口,展示活动接口最近 30 分钟网速趋势
Platform.MenuItem {
text: qsTr("流量波动图")
text: qsTr("Traffic Chart")
onTriggered: {
trafficChartWindow.show()
trafficChartWindow.raise()
@@ -321,7 +321,7 @@ AppletItem {
Platform.MenuSeparator {}
Platform.MenuItem {
text: qsTr("关于")
text: qsTr("About")
onTriggered: {
aboutWindow.show()
aboutWindow.raise()
+14
View File
@@ -14,6 +14,9 @@
#include <QColor>
#include <QSettings>
#include <QStandardPaths>
#include <QTranslator>
#include <QLocale>
#include <QCoreApplication>
DS_BEGIN_NAMESPACE
@@ -65,6 +68,17 @@ bool NetworkMonitorApplet::load()
bool NetworkMonitorApplet::init()
{
// 加载翻译文件:根据系统语言加载对应的 .qm 文件
// 设计原因:QML 源串为英文,中文翻译通过 .qm 文件在运行时加载。
// 翻译上下文为 QML 文件名(如 "networkview"、"AboutWindow"),
// 安装到 QCoreApplication 后 qsTr() 自动查找匹配。
QTranslator *translator = new QTranslator(this);
const QString locale = QLocale::system().name();
if (translator->load(QStringLiteral("jnetapplet_%1.qm").arg(locale),
QStringLiteral(TRANSLATIONS_DIR))) {
qApp->installTranslator(translator);
}
detectInterfaces();
// 启动定时刷新
+241
View File
@@ -0,0 +1,241 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="zh_CN">
<context>
<name>AboutWindow</name>
<message>
<location filename="../package/components/AboutWindow.qml" line="86"/>
<source>About</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/AboutWindow.qml" line="133"/>
<source>Network Speed Monitor</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/AboutWindow.qml" line="182"/>
<source>Version</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/AboutWindow.qml" line="203"/>
<source>Author</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/AboutWindow.qml" line="224"/>
<source>Description</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/AboutWindow.qml" line="232"/>
<source>Monitor network speed and traffic</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/AboutWindow.qml" line="246"/>
<source>Repository</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/AboutWindow.qml" line="284"/>
<source>Copied</source>
<translation></translation>
</message>
</context>
<context>
<name>NetworkPopup</name>
<message>
<location filename="../package/components/NetworkPopup.qml" line="98"/>
<source>Network Speed Monitor</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/NetworkPopup.qml" line="129"/>
<source>IPv4: </source>
<translation>IPv4</translation>
</message>
<message>
<location filename="../package/components/NetworkPopup.qml" line="138"/>
<source>IPv6: </source>
<translation>IPv6</translation>
</message>
<message>
<location filename="../package/components/NetworkPopup.qml" line="190"/>
<source>Download</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/NetworkPopup.qml" line="247"/>
<source>Upload</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/NetworkPopup.qml" line="282"/>
<source>Total</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/NetworkPopup.qml" line="459"/>
<source>No network interface detected</source>
<translation></translation>
</message>
</context>
<context>
<name>SettingsWindow</name>
<message>
<location filename="../package/components/SettingsWindow.qml" line="54"/>
<location filename="../package/components/SettingsWindow.qml" line="383"/>
<location filename="../package/components/SettingsWindow.qml" line="560"/>
<source>Uninstall Plugin</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/SettingsWindow.qml" line="73"/>
<source>Loopback</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/SettingsWindow.qml" line="74"/>
<source>Virtual Interface</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/SettingsWindow.qml" line="75"/>
<source>Wired Network</source>
<translation>线</translation>
</message>
<message>
<location filename="../package/components/SettingsWindow.qml" line="76"/>
<source>Wireless Network</source>
<translation>线</translation>
</message>
<message>
<location filename="../package/components/SettingsWindow.qml" line="77"/>
<source>Container Network</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/SettingsWindow.qml" line="78"/>
<source>Bridge</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/SettingsWindow.qml" line="79"/>
<source>VPN</source>
<translation>VPN</translation>
</message>
<message>
<location filename="../package/components/SettingsWindow.qml" line="80"/>
<source>Virtual Bridge</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/SettingsWindow.qml" line="81"/>
<source>Other</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/SettingsWindow.qml" line="142"/>
<source>Settings</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/SettingsWindow.qml" line="196"/>
<source>Network Interface</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/SettingsWindow.qml" line="349"/>
<source>No network interface detected</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/SettingsWindow.qml" line="360"/>
<source>Font Color</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/SettingsWindow.qml" line="446"/>
<source>Warning</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/SettingsWindow.qml" line="454"/>
<source>The following command uninstalls the plugin and restarts dde-shell. Please copy and run it in terminal:</source>
<translation> dde-shell</translation>
</message>
<message>
<location filename="../package/components/SettingsWindow.qml" line="499"/>
<source>Cancel</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/SettingsWindow.qml" line="522"/>
<source>Copy Command</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/SettingsWindow.qml" line="539"/>
<source>Uninstall command copied to clipboard. Please paste and run it in terminal.</source>
<translation></translation>
</message>
</context>
<context>
<name>TrafficChartWindow</name>
<message>
<location filename="../package/components/TrafficChartWindow.qml" line="180"/>
<source>Traffic Chart</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/TrafficChartWindow.qml" line="424"/>
<source>No history data</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/TrafficChartWindow.qml" line="581"/>
<source>Hover over the curve for details</source>
<translation>线</translation>
</message>
</context>
<context>
<name>networkview</name>
<message>
<location filename="../package/networkview.qml" line="225"/>
<source>IPv4: </source>
<translation>IPv4</translation>
</message>
<message>
<location filename="../package/networkview.qml" line="225"/>
<source>None</source>
<translation></translation>
</message>
<message>
<location filename="../package/networkview.qml" line="226"/>
<source>IPv6: </source>
<translation>IPv6</translation>
</message>
<message>
<location filename="../package/networkview.qml" line="227"/>
<source>No network interface detected</source>
<translation></translation>
</message>
<message>
<location filename="../package/networkview.qml" line="303"/>
<source>Settings</source>
<translation></translation>
</message>
<message>
<location filename="../package/networkview.qml" line="313"/>
<source>Traffic Chart</source>
<translation></translation>
</message>
<message>
<location filename="../package/networkview.qml" line="324"/>
<source>About</source>
<translation></translation>
</message>
</context>
</TS>