diff --git a/AGENTS.md b/AGENTS.md index 856351d9..a36cbe84 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -103,6 +103,7 @@ Exceptions are allowed for migration and package-helper scripts where the helper - `default/region/china/pacman.sed` puts a `.omarchy.cn` twin ahead of every `.omarchy.org` server; `omarchy-refresh-pacman`, `install/post-install/pacman.sh` and `omarchy-region` apply it. Nothing in `omarchy update` or migrations may change the region or `/etc/locale.conf`, and a migration that edits an `.omarchy.org` `Server` line must edit its `.cn` twin the same way. - China-only menu rows carry `"when":"[[ \"$(omarchy-region)\" == \"China\" ]]"` on every row, not only the submenu, because search checks each row's own guard. - Chinese menu labels live in `default/omarchy/omarchy-menu.zh-cn.jsonc`, merged when `LANG` starts with `zh_CN`; give a new menu row with translatable words a label there. +- Shell text goes through `I18n.tr("English %1", arg)` from `qs.Commons`, with its Chinese in `shell/Commons/I18nZh.js` keyed by the English; notifications and OSD messages are translated where the shell shows them, so bash senders pass English. `test/shell.d/shell-i18n-test.sh` fails on a missing entry. - `rime-ice-installer` is built from `omarchy-pkgs` with `omarchy-offline.patch` until it ships in the Omarchy repo. # Config Structure diff --git a/agents/skills/shell-dev.md b/agents/skills/shell-dev.md index 9765af89..2c1137d0 100644 --- a/agents/skills/shell-dev.md +++ b/agents/skills/shell-dev.md @@ -24,6 +24,10 @@ Run `omarchy-restart-shell` after making changes to QML files. - Panel / overlay / menu plugins must expose `open(payloadJson)` and `close()` lifecycle methods for `shell summon` and `shell hide`. +## Text + +Wrap every user-visible string in `I18n.tr()` (`import qs.Commons`) and add its Chinese to `shell/Commons/I18nZh.js`; plain JS models that Node tests load stay English, and their text is wrapped where QML shows it. + ## IPC - `bin/omarchy-shell` is the canonical IPC entry point. It forwards to diff --git a/shell/Commons/I18n.qml b/shell/Commons/I18n.qml new file mode 100644 index 00000000..e1d38416 --- /dev/null +++ b/shell/Commons/I18n.qml @@ -0,0 +1,12 @@ +pragma Singleton +import QtQuick +import "Tr.js" as Tr + +// tr("English %1", arg) returns the Chinese entry under zh_CN; plain JS imports Tr.js for the same. +QtObject { + readonly property bool chinese: Tr.chinese + + function tr() { + return Tr.tr.apply(null, arguments) + } +} diff --git a/shell/Commons/I18nZh.js b/shell/Commons/I18nZh.js new file mode 100644 index 00000000..9fe36650 --- /dev/null +++ b/shell/Commons/I18nZh.js @@ -0,0 +1,455 @@ +// Chinese for the shell, keyed by the English text each I18n.tr() call passes; %1, %2 are arguments. +.pragma library + +var table = { + // Agents panel + "Agents": "AI 代理", + "Monthly": "每月", + "Weekly": "每周", + "Session": "会话", + "Limit": "额度", + "now": "现在", + "%1d %2h": "%1天 %2小时", + "%1h %2m": "%1小时 %2分", + "%1m": "%1分钟", + "%1 free reset": "%1 次免费重置", + "%1 free resets": "%1 次免费重置", + "%1 spent of %2 funded": "已用 %1,共充值 %2", + "estimated": "估算", + "%1 tokens this week": "本周 %1 tokens", + "%1 tokens today": "今天 %1 tokens", + "Mostly %1": "主要使用 %1", + "Busiest day: %1": "最忙的一天:%1", + "%1 prompt today": "今天 %1 条提示", + "%1 prompts today": "今天 %1 条提示", + "%1 session today": "今天 %1 个会话", + "%1 sessions today": "今天 %1 个会话", + "Merged from %1 device": "合并自 %1 台设备", + "Merged from %1 devices": "合并自 %1 台设备", + "Add an account": "添加账户", + "Add an %1 account": "添加 %1 账户", + "Add a %1 account": "添加 %1 账户", + "The sign-in didn't finish.": "登录没有完成。", + "Back to the limits": "返回额度", + "Add a subscription": "添加订阅", + "Start the default agent": "启动默认 AI 代理", + "MAKE SOMETHING COOL": "做点有意思的", + "Sign in to an AI coding subscription, and this panel keeps track of how much of it you have left.": "登录一个 AI 编程订阅,这个面板会记录剩余额度。", + "Name this account. It signs in through a private window, so your browser's current account isn't picked up.": "给这个账户起个名字。登录在隐私窗口中进行,不会用到浏览器当前登录的账户。", + "Work": "工作", + "Confirm this code in your browser": "在浏览器中确认这个代码", + "If the page shows a code instead of finishing, paste it here.": "如果页面显示代码而没有完成,把代码粘贴到这里。", + "Code": "代码", + "Open the sign-in page again": "重新打开登录页", + "Sign in to %1 again": "重新登录 %1", + "Balance": "余额", + "Start your default agent on a new %1": "用默认 AI 代理新建一个%1", + "Sign in to this account again": "重新登录这个账户", + "ACTIVE": "使用中", + "Autoswitch": "自动切换", + "Stop switching automatically": "停止自动切换", + "Switch here automatically at %1%": "用到 %1% 时自动切换到这里", + "Use": "使用", + "Usage unavailable": "用量不可用", + "Usage has not been updated yet.": "用量还没有更新。", + "%1: %2% used": "%1:已用 %2%", + "resets in %1": "%1后重置", + "Last updated %1 ago": "%1前更新", + "Last updated less than a minute ago": "不到一分钟前更新", + "Last updated time unavailable": "更新时间不可用", + "Theme": "主题", + "Plugin": "插件", + "App": "应用", + "Sunday": "星期日", + "Monday": "星期一", + "Tuesday": "星期二", + "Wednesday": "星期三", + "Thursday": "星期四", + "Friday": "星期五", + "Saturday": "星期六", + "%1: %2% of its %3 allowance": "%1:占%3额度的 %2%", + "Subscriptions": "订阅", + // Network panel + "Wiring bits": "接线中", + "Handling packets": "处理数据包", + "Sorting frames": "整理数据帧", + "Hauling bytes": "搬运字节", + "Routing crumbs": "转发碎片", + "Counting collisions": "清点冲突", + "Bending light": "弯折光线", + "Turn Wi-Fi off": "关闭 Wi-Fi", + "Turn Wi-Fi on": "打开 Wi-Fi", + "WI-FI BAND": "WI-FI 频段", + "WI-FI BAND: %1": "WI-FI 频段:%1", + "Auto": "自动", + "Let Wi-Fi pick the band": "由 Wi-Fi 自动选择频段", + "Stay on %1": "固定在 %1", + "Timeout": "超时", + "KNOWN NETWORKS": "已知网络", + "OTHER NETWORKS": "其他网络", + "Ethernet": "有线网络", + "Passphrase required": "需要密码", + "Wrong password": "密码错误", + "Network lost": "网络丢失", + "Disconnected": "已断开", + "Connection failed": "连接失败", + "Failed to connect": "无法连接", + "Timed out connecting": "连接超时", + "Timed out disconnecting": "断开超时", + "Timed out forgetting": "忘记网络超时", + "Sign in to this network": "登录这个网络", + "Limited internet access": "互联网访问受限", + "Show QR code": "显示二维码", + "Run a speed test": "测速", + "No connection": "无连接", + "SIGN-IN REQUIRED": "需要登录", + "LIMITED INTERNET ACCESS": "互联网访问受限", + "NOT CONNECTED": "未连接", + "Open Captive Portal": "打开认证页面", + "Sign in or accept this network’s terms to access the internet.": "登录或接受这个网络的条款后才能上网。", + "AUTOMATIC": "自动", + "DNS PROVIDER": "DNS 服务商", + "Use DNS from DHCP": "使用 DHCP 提供的 DNS", + "Set DNS to %1": "DNS 设为 %1", + "Set custom DNS servers": "设置自定义 DNS 服务器", + "Custom": "自定义", + "SCANNING WI-FI…": "正在扫描 WI-FI…", + "Connecting…": "正在连接…", + "Disconnecting…": "正在断开…", + "Forgetting…": "正在忘记…", + "Failed": "失败", + "Sign-in required": "需要登录", + "Connected": "已连接", + "Forget network": "忘记网络", + "Hidden": "隐藏网络", + "Identity (user@domain)": "身份(user@domain)", + "Passphrase": "密码", + "Connect": "连接", + "Copy to clipboard": "复制到剪贴板", + "Ping": "延迟", + "Packet Loss": "丢包", + "Receiving": "接收", + "Sending": "发送", + "Downloaded": "已下载", + "Uploaded": "已上传", + "IP Address": "IP 地址", + "Gateway": "网关", + "Copy IP": "复制 IP", + "Copy gateway": "复制网关", + "Connecting...": "正在连接…", + // Display panel + "Display": "显示", + "FIXED BRIGHTNESS": "亮度固定", + "BRIGHTNESS": "亮度", + "TEXT SIZE": "文字大小", + "SCALE": "缩放", + "DISPLAYS": "显示器", + "Sun blast": "烈日当空", + "Solar flare": "耀眼", + "Golden hour": "黄金时刻", + "Even day": "白昼", + "Soft glow": "柔光", + "Lamp light": "灯光", + "Candlelit": "烛光", + "Night owl": "夜猫子", + // Audio panel + "Audio": "音频", + "OUTPUT": "输出", + "INPUT": "输入", + "SOURCES": "音源", + "Mute": "静音", + "Unmute": "取消静音", + "Muted": "已静音", + "Silenced": "无声", + "Concert hall": "音乐厅", + "Party mode": "派对模式", + "Cranked up": "开大了", + "Steady groove": "稳稳的节奏", + "Easy listening": "轻松聆听", + "Murmur": "低语", + "Whisper": "耳语", + // Bluetooth panel + "Untangling wires": "理顺线缆", + "Streaming vikings": "维京人在传输", + "Pairing mysteries": "配对谜题", + "Herding headsets": "召集耳机", + "Taming radios": "驯服无线电", + "Summoning speakers": "召唤音箱", + "Wrangling codecs": "摆弄编解码器", + "Polishing packets": "打磨数据包", + "Bluetooth": "蓝牙", + "CONNECTED": "已连接", + "Forget": "忘记", + "No adapter": "没有适配器", + "Turned Off": "已关闭", + "Turn Bluetooth off": "关闭蓝牙", + "Turn Bluetooth on": "打开蓝牙", + "PAIRED": "已配对", + "AVAILABLE": "可用", + "No Bluetooth adapter": "没有蓝牙适配器", + "Turn Bluetooth on to scan": "打开蓝牙后扫描", + "Scanning for devices…": "正在扫描设备…", + "Disconnect": "断开", + "Pair": "配对", + "Device": "设备", + // Power panel + "Pumping power": "注入电力", + "Injecting electrons": "注入电子", + "Pouring juice": "倒入电量", + "Amassing watts": "积攒瓦特", + "Hoarding joules": "囤积焦耳", + "Sucking volts": "吸收伏特", + "Topping reserves": "补满储备", + "Soaking amps": "吸饱安培", + "Inhaling kilowatts": "吸入千瓦", + "Slurping power": "啜饮电力", + "Spending joules": "花费焦耳", + "Draining watts": "消耗瓦特", + "Burning electrons": "燃烧电子", + "Sipping juice": "小口用电", + "Spending coulombs": "花费库仑", + "Bleeding amps": "流失安培", + "Guzzling volts": "狂饮伏特", + "Munching reserves": "啃食储备", + "Battery": "电池", + "POWER PROFILE": "电源模式", + "Fully charged": "已充满", + "Threshold": "充电上限", + "On battery": "使用电池", + "Charging": "正在充电", + "Battery size": "电池容量", + "Charge cycles": "充电次数", + "Charge limit": "充电上限", + "Time left": "剩余时间", + "Time to full": "充满还需", + "Battery state": "电池状态", + "Discharging": "正在放电", + "Holding": "保持中", + // Tailscale panel + "Encrypting connections": "加密连接", + "Sending secrets": "传递秘密", + "Guarding wires": "守护线路", + "Braiding packets": "编织数据包", + "Polishing tunnels": "打磨隧道", + "Hiding routes": "隐藏路由", + "Sealing ports": "封好端口", + "Sorting tailnets": "整理 tailnet", + "Shuffling keys": "洗牌密钥", + "Watching machines": "照看机器", + "Tailscale CLI is not installed or not on PATH.": "没有安装 Tailscale 命令行,或它不在 PATH 中。", + "CONNECTIONS": "连接", + "EXIT NODES": "出口节点", + "Search regions": "搜索地区", + "No Mullvad regions found.": "没有找到 Mullvad 地区。", + "MACHINES": "机器", + "No machines found on this tailnet.": "这个 tailnet 上没有机器。", + "Authorize Tailscale operator": "授权 Tailscale 操作员", + "Allow this user to operate this Tailscale profile": "允许这个用户操作这个 Tailscale 配置", + "Send files": "发送文件", + "Turn Tailscale off": "关闭 Tailscale", + "Authorize this device": "授权这台设备", + "Turn Tailscale on": "打开 Tailscale", + "Choose Mullvad region": "选择 Mullvad 地区", + "Tailscale is disconnected": "Tailscale 已断开", + "Account": "账户", + "Starting Tailscale login…": "正在开始 Tailscale 登录…", + "Authorizing Tailscale operator...": "正在授权 Tailscale 操作员…", + "Tailscale login link not available yet": "Tailscale 登录链接还不可用", + "Authorize Tailscale operator to show connections": "授权 Tailscale 操作员后显示连接", + "Could not list Tailscale connections": "无法列出 Tailscale 连接", + "Tailscale command failed": "Tailscale 命令失败", + "Account switch failed": "切换账户失败", + "Exit node selection failed": "选择出口节点失败", + "Tailscale authorization failed": "Tailscale 授权失败", + "Tailscale operator authorized": "Tailscale 操作员已授权", + "Unknown": "未知", + // Dropbox panel + "Filing files": "归档文件", + "Distributing data": "分发数据", + "Shuffling folders": "整理文件夹", + "Boxing bytes": "装箱字节", + "Sorting stuff": "分拣东西", + "Syncing secrets": "同步秘密", + "Packing packets": "打包数据包", + "Moving memories": "搬运回忆", + "Wrangling revisions": "整理修订", + "Cataloging chaos": "编目混乱", + "RECENT FILES": "最近文件", + "No synced files found.": "没有找到同步的文件。", + "Pause syncing": "暂停同步", + "Resume syncing": "继续同步", + "Syncing paused": "同步已暂停", + "Stored": "已存储", + "Login to Dropbox": "登录 Dropbox", + "Dropbox CLI is not installed": "没有安装 Dropbox 命令行", + "Start the authentication flow": "开始认证", + "Install Dropbox from the service menu": "从服务菜单安装 Dropbox", + "Untitled": "未命名", + // Weather panel + "Search city": "搜索城市", + "FEELS": "体感", + "WIND": "风", + "HUMID": "湿度", + "Fetching forecast…": "正在获取天气预报…", + // Wi-Fi QR panel + "Generating QR code…": "正在生成二维码…", + "Scan to join this network": "扫码加入这个网络", + "Could not generate the Wi-Fi QR code": "无法生成 Wi-Fi 二维码", + "Could not read the Wi-Fi password": "无法读取 Wi-Fi 密码", + "Show password": "显示密码", + // Speed tests + "Speed test failed": "测速失败", + "DOWNLOAD": "下载", + "UPLOAD": "上传", + "Measure again via fast.com": "通过 fast.com 重新测速", + "Disk speed test failed": "磁盘测速失败", + "READ": "读取", + "WRITE": "写入", + "Measure again": "重新测速", + "Run Again": "再测一次", + // Lock screen + "Enter Password": "输入密码", + "Checking…": "正在验证…", + "Fingerprint reader unavailable": "指纹识别器不可用", + "Authentication failed (%1)": "认证失败(%1)", + // Authentication prompt + "Authentication is needed...": "需要认证…", + "Wrong": "错误", + "Checking...": "正在验证…", + "Enter password": "输入密码", + // Reminders + "Reminder message": "提醒内容", + "Remind in minutes": "几分钟后提醒", + // Notifications + "No recent notifications": "没有最近的通知", + // Bar + "Microphone muted": "麦克风已静音", + "Microphone in use": "麦克风使用中", + "Microphone live": "麦克风已开启", + "Pending Omarchy Updates": "有待安装的 Omarchy 更新", + "Tray icons": "托盘图标", + "Pinned icons stay visible. Hidden icons never show.": "固定的图标一直显示,隐藏的图标不再显示。", + "No tray items reporting.": "没有托盘项目。", + "Unpin": "取消固定", + "Pin": "固定", + "Show": "显示", + "Hide": "隐藏", + "Dictate": "语音输入", + "Allow Notifications": "允许通知", + "Silence Notifications": "静默通知", + "Day Light": "日间光线", + "Night Light": "夜间光线", + "Stop recording": "停止录屏", + "Screen Recording": "录屏", + "Allow Idle Lock & Screensaver": "允许空闲锁屏和屏保", + "Stay Awake": "保持唤醒", + // Shared controls + "Cancel": "取消", + "Confirm": "确认", + "Search...": "搜索…", + "No options": "没有选项", + "None selected": "未选择", + "Loading…": "正在加载…", + "No matches": "没有匹配项", + // Notifications and on-screen messages + "Learn Keybindings": "了解快捷键", + "Super + K for cheatsheet.\nSuper + Space for Omarchy Menu.": "Super + K 查看快捷键速查表。\nSuper + Space 打开 Omarchy 菜单。", + "Update System": "更新系统", + "Click to update the system.": "点击更新系统。", + "Setup Wi-Fi": "设置 Wi-Fi", + "Click to configure the wireless network.": "点击配置无线网络。", + "All reminders have been cleared": "所有提醒已清除", + "Animations disabled": "动画已关闭", + "Animations enabled": "动画已开启", + "Can't disable the only active display": "不能关闭唯一的显示器", + "Copied text from selection to clipboard": "已把选中文字复制到剪贴板", + "Could not complete shutdown. Please try again.": "无法完成关机,请重试。", + "Could not share": "无法分享", + "Crash capture disabled": "崩溃捕获已关闭", + "Crash capture enabled": "崩溃捕获已开启", + "Disable single-window square aspect ratio": "关闭单窗口方形比例", + "Enable single-window square aspect ratio": "开启单窗口方形比例", + "Download failed": "下载失败", + "Editing config file": "正在编辑配置文件", + "Extended mode restored": "已恢复扩展模式", + "FIDO2 authfile needs attention": "FIDO2 认证文件需要处理", + "Failed to start Windows VM": "无法启动 Windows 虚拟机", + "Herdr theme sync disabled": "Herdr 主题同步已关闭", + "Herdr theme sync enabled": "Herdr 主题同步已开启", + "Hold F9 to dictate (or toggle with Super + Ctrl + X).": "按住 F9 语音输入(或用 Super + Ctrl + X 切换)。", + "Laptop display disabled": "笔记本屏幕已关闭", + "Laptop display enabled": "笔记本屏幕已开启", + "Logging out": "正在注销", + "Microphone on": "麦克风已开启", + "No QR code found": "没有找到二维码", + "No RetroArch cores found": "没有找到 RetroArch 核心", + "No audio devices found": "没有找到音频设备", + "No background was found for theme": "这个主题没有壁纸", + "No external monitors found for mirror": "没有可镜像的外接显示器", + "No laptop display found": "没有找到笔记本屏幕", + "No laptop monitor found to mirror": "没有可镜像的笔记本屏幕", + "No outstanding reminders": "没有待办提醒", + "No video found for download": "没有可下载的视频", + "No webcam devices found": "没有找到摄像头", + "QR code copied to clipboard": "二维码已复制到剪贴板", + "Rebooting": "正在重启", + "Recording process had to be force-killed. Video may be corrupted.": "录屏进程被强制结束,视频可能已损坏。", + "Refusing unsafe monitor name": "拒绝不安全的显示器名称", + "Reminder": "提醒", + "Restore using Super + Home on this workspace.": "在这个工作区用 Super + Home 恢复。", + "Saved and copied to clipboard.": "已保存并复制到剪贴板。", + "Screen recording error": "录屏出错", + "Screensaver disabled": "屏保已关闭", + "Screensaver enabled": "屏保已开启", + "Screensaver only runs in Alacritty, Foot, Ghostty, or Kitty": "屏保只能在 Alacritty、Foot、Ghostty 或 Kitty 中运行", + "Select a region containing a QR code": "框选包含二维码的区域", + "Shutdown failed": "关机失败", + "Shutting down": "正在关机", + "Start it with Super + Space": "用 Super + Space 启动", + "Suspend now available in system menu": "系统菜单中已加入挂起", + "Suspend removed from system menu": "系统菜单中已移除挂起", + "TUI removed": "TUI 已移除", + "The file chooser did not open": "文件选择器没有打开", + "Theme removed": "主题已移除", + "Time to recharge!": "该充电了!", + "Transcoding video…": "正在转码视频…", + "URL copied to clipboard": "链接已复制到剪贴板", + "Upcoming reminders": "即将到来的提醒", + "Use Super + Alt + Home to save one for this workspace.": "用 Super + Alt + Home 为这个工作区保存一个。", + "Voxtype Dictation Ready": "Voxtype 语音输入就绪", + "Web app removed": "网页应用已移除", + "Windows VM": "Windows 虚拟机", + // Clock + "Right-click to toggle format": "右键切换格式", + "Back to today": "回到今天", + "BORN": "出生", + "LIVE TO": "活到", + "LIFE": "人生", + "Start weeks on %1": "每周从%1开始", + "Previous month": "上个月", + "Next month": "下个月", + // Menu + "Go": "前往", + "Input": "输入", + "Select": "选择", + "Do you want to uninstall %1?": "要卸载 %1 吗?", + "Uninstall": "卸载", + "No matches for “%1”": "没有匹配“%1”的结果", + "Nothing here yet": "这里还没有内容", + // World clock, emojis, clipboard + "Search cities…": "搜索城市…", + "World": "世界", + "Clock": "时钟", + "Remove": "移除", + "World clock": "世界时钟", + "Delete entire clipboard history?": "删除全部剪贴板历史?", + "Delete": "删除", + "Clipboard is empty": "剪贴板是空的", + "Sunrise": "日出", + "Sunset": "日落", + "It's %1 here": "这里现在是 %1", + "Jump to a city": "跳到一个城市", + // Media + "Pause": "暂停", + "Play": "播放" +} diff --git a/shell/Commons/Tr.js b/shell/Commons/Tr.js new file mode 100644 index 00000000..862dcbbb --- /dev/null +++ b/shell/Commons/Tr.js @@ -0,0 +1,11 @@ +.pragma library +.import "I18nZh.js" as Zh + +// Shell text in the session's language; QML reaches this through the I18n singleton, plain JS imports it. +var chinese = Qt.locale().name.indexOf("zh_CN") === 0 + +function tr(text) { + var out = chinese && Zh.table[text] !== undefined ? Zh.table[text] : text + for (var i = 1; i < arguments.length; i++) out = out.split("%" + i).join(String(arguments[i])) + return out +} diff --git a/shell/Commons/qmldir b/shell/Commons/qmldir index 1c9101c1..3f427d6b 100644 --- a/shell/Commons/qmldir +++ b/shell/Commons/qmldir @@ -1,6 +1,7 @@ module qs.Commons singleton Border 1.0 Border.qml singleton Color 1.0 Color.qml +singleton I18n 1.0 I18n.qml singleton IpcRegistry 1.0 IpcRegistry.qml singleton Style 1.0 Style.qml singleton Util 1.0 Util.qml diff --git a/shell/Ui/ConfirmDialog.qml b/shell/Ui/ConfirmDialog.qml index bc108d97..30c9871c 100644 --- a/shell/Ui/ConfirmDialog.qml +++ b/shell/Ui/ConfirmDialog.qml @@ -6,8 +6,8 @@ Item { property bool opened: false property string message: "" - property string cancelText: "Cancel" - property string confirmText: "Confirm" + property string cancelText: I18n.tr("Cancel") + property string confirmText: I18n.tr("Confirm") property int selectedIndex: 1 property color background: Color.background property color foreground: Color.foreground diff --git a/shell/Ui/MultiSelect.qml b/shell/Ui/MultiSelect.qml index ad2e2955..4da69119 100644 --- a/shell/Ui/MultiSelect.qml +++ b/shell/Ui/MultiSelect.qml @@ -27,9 +27,9 @@ Item { property var options: [] property var optionsCommand: [] property string optionsCommandCwd: "" - property string placeholderText: "Search..." - property string emptyText: "No options" - property string noSelectionText: "None selected" + property string placeholderText: I18n.tr("Search...") + property string emptyText: I18n.tr("No options") + property string noSelectionText: I18n.tr("None selected") property string triggerLabel: "" property bool showLabel: true @@ -492,7 +492,7 @@ Item { textFormat: Text.PlainText anchors.centerIn: parent visible: resultList.count === 0 - text: root.loadingOptions ? "Loading…" : (root.optionsError !== "" ? root.optionsError : root.emptyText) + text: root.loadingOptions ? I18n.tr("Loading…") : (root.optionsError !== "" ? root.optionsError : root.emptyText) color: Qt.darker(root.foreground, 1.6) font.family: root.fontFamily font.pixelSize: Style.font.body diff --git a/shell/Ui/SearchableDropdown.qml b/shell/Ui/SearchableDropdown.qml index 7728d86b..0f221ee2 100644 --- a/shell/Ui/SearchableDropdown.qml +++ b/shell/Ui/SearchableDropdown.qml @@ -21,8 +21,8 @@ Item { property string label: "" property string value: "" property var options: [] - property string placeholderText: "Search..." - property string emptyText: "No matches" + property string placeholderText: I18n.tr("Search...") + property string emptyText: I18n.tr("No matches") property string triggerLabel: "" property color foreground: Color.popups.text diff --git a/shell/Ui/SpeedTestOverlay.qml b/shell/Ui/SpeedTestOverlay.qml index f1beef92..7697212b 100644 --- a/shell/Ui/SpeedTestOverlay.qml +++ b/shell/Ui/SpeedTestOverlay.qml @@ -22,7 +22,7 @@ PanelWindow { property string unit: "Mbps" property string title: "" property string layerNamespace: "omarchy-speed-test" - property string runAgainTooltip: "Measure again" + property string runAgainTooltip: I18n.tr("Measure again") property real leftValue: 0 property real rightValue: 0 property bool leftLive: false @@ -164,7 +164,7 @@ PanelWindow { // Centered on the dial pair. Fades rather than unmounts while a run // is in flight, so the cluster never shifts. Button { - text: "Run Again" + text: I18n.tr("Run Again") tooltipText: root.runAgainTooltip bordered: true enabled: !root.running diff --git a/shell/plugins/agents/Panel.qml b/shell/plugins/agents/Panel.qml index 60677e91..82992fe4 100644 --- a/shell/plugins/agents/Panel.qml +++ b/shell/plugins/agents/Panel.qml @@ -338,9 +338,9 @@ Panel { // A few ways into making Omarchy your own, handed to the default agent. readonly property var starterPrompts: [ - { glyph: "󰏘", label: "Theme", prompt: "Make me a new Omarchy theme. Ask me what look or inspiration I have in mind, then build it following the Omarchy skill's theming guide and switch to it." }, - { glyph: "󰐱", label: "Plugin", prompt: "Make me a new Omarchy shell plugin. Ask me what I'd like it to do, then build it following the Omarchy skill's plugin guide and enable it." }, - { glyph: "󰣆", label: "App", prompt: "Make me a new app for my Omarchy desktop. Ask me what it should do, then build it following the omarchy-app skill and install it so it shows up in the app launcher." } + { glyph: "󰏘", label: I18n.tr("Theme"), prompt: "Make me a new Omarchy theme. Ask me what look or inspiration I have in mind, then build it following the Omarchy skill's theming guide and switch to it." }, + { glyph: "󰐱", label: I18n.tr("Plugin"), prompt: "Make me a new Omarchy shell plugin. Ask me what I'd like it to do, then build it following the Omarchy skill's plugin guide and enable it." }, + { glyph: "󰣆", label: I18n.tr("App"), prompt: "Make me a new app for my Omarchy desktop. Ask me what it should do, then build it following the omarchy-app skill and install it so it shows up in the app launcher." } ] function startPrompt(prompt) { @@ -406,7 +406,7 @@ Panel { var parts = [] if (String(account.plan || "") !== "") parts.push(account.plan) if (account.resetCredits && Number(account.resetCredits.available) > 0) - parts.push(account.resetCredits.available + " free reset" + (Number(account.resetCredits.available) === 1 ? "" : "s")) + parts.push(I18n.tr(Number(account.resetCredits.available) === 1 ? "%1 free reset" : "%1 free resets", account.resetCredits.available)) return parts.join(" · ") } @@ -491,11 +491,11 @@ Panel { function windowTitle(label) { var text = String(label || "").toLowerCase() - if (text.indexOf("month") >= 0) return "Monthly" - if (windowIsLong(text)) return "Weekly" - if (text.indexOf("session") >= 0 || windowSpanMs(label) > 0) return "Session" + if (text.indexOf("month") >= 0) return I18n.tr("Monthly") + if (windowIsLong(text)) return I18n.tr("Weekly") + if (text.indexOf("session") >= 0 || windowSpanMs(label) > 0) return I18n.tr("Session") var plain = String(label || "").replace(/\s*\(.*\)\s*/, "").trim() - return plain === "" ? "Limit" : plain + return plain === "" ? I18n.tr("Limit") : plain } // A collector that already knows which window a limit belongs to says so, @@ -575,13 +575,13 @@ Panel { } function formatDuration(ms) { - if (!(ms > 0)) return "now" + if (!(ms > 0)) return I18n.tr("now") var minutes = Math.floor(ms / 60000) var hours = Math.floor(minutes / 60) var days = Math.floor(hours / 24) - if (days > 0) return days + "d " + (hours % 24) + "h" - if (hours > 0) return hours + "h " + (minutes % 60) + "m" - return Math.max(1, minutes) + "m" + if (days > 0) return I18n.tr("%1d %2h", days, hours % 24) + if (hours > 0) return I18n.tr("%1h %2m", hours, minutes % 60) + return I18n.tr("%1m", Math.max(1, minutes)) } // ---------------------------------------------------------------- balance @@ -611,8 +611,8 @@ Panel { function balanceDetailText(b) { if (!b || !(b.funded > 0)) return "" - var text = formatMoney(b.spent, b.currency) + " spent of " + formatMoney(b.funded, b.currency) + " funded" - if (b.estimated) text += " · estimated" + var text = I18n.tr("%1 spent of %2 funded", formatMoney(b.spent, b.currency), formatMoney(b.funded, b.currency)) + if (b.estimated) text += " · " + I18n.tr("estimated") return text } @@ -653,16 +653,16 @@ Panel { } var phrases = [] - if (week > 0) phrases.push(usage.formatTokenCount(week) + " tokens this week") - if (today > 0) phrases.push(usage.formatTokenCount(today) + " tokens today") + if (week > 0) phrases.push(I18n.tr("%1 tokens this week", usage.formatTokenCount(week))) + if (today > 0) phrases.push(I18n.tr("%1 tokens today", usage.formatTokenCount(today))) var topModel = "" for (var model in byModel) if (topModel === "" || byModel[model] > byModel[topModel]) topModel = model - if (topModel !== "" && byModel[topModel] > 0) phrases.push("Mostly " + topModel) + if (topModel !== "" && byModel[topModel] > 0) phrases.push(I18n.tr("Mostly %1", topModel)) var busiest = "" for (var date in byDay) if (busiest === "" || byDay[date] > byDay[busiest]) busiest = date - if (busiest !== "" && byDay[busiest] > 0) phrases.push("Busiest day: " + dayName(busiest)) - if (prompts > 0) phrases.push(prompts + " prompt" + (prompts === 1 ? "" : "s") + " today") - if (sessions > 0) phrases.push(sessions + " session" + (sessions === 1 ? "" : "s") + " today") + if (busiest !== "" && byDay[busiest] > 0) phrases.push(I18n.tr("Busiest day: %1", dayName(busiest))) + if (prompts > 0) phrases.push(I18n.tr(prompts === 1 ? "%1 prompt today" : "%1 prompts today", prompts)) + if (sessions > 0) phrases.push(I18n.tr(sessions === 1 ? "%1 session today" : "%1 sessions today", sessions)) return phrases } property int phraseIndex: 0 @@ -672,7 +672,7 @@ Panel { property string shownPhrase: "" readonly property string heroPhrase: addStage !== "" || blankSlate ? addHeading - : (shownPhrase !== "" ? shownPhrase : "Subscriptions") + : (shownPhrase !== "" ? shownPhrase : I18n.tr("Subscriptions")) function showPhrase() { var n = summaryPhrases.length @@ -684,7 +684,7 @@ Panel { function dayName(date) { var parsed = new Date(String(date || "") + "T00:00:00") if (isNaN(parsed.getTime())) return String(date || "") - return ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][parsed.getDay()] + return I18n.tr(["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][parsed.getDay()]) } // Only speaks up when the numbers cover more than this machine. @@ -693,7 +693,7 @@ Panel { for (var i = 0; i < providers.length; i++) { var count = Number(providers[i].syncDeviceCount || 0) if (providers[i].syncEnabled && count > 0) - return "Merged from " + count + " device" + (count === 1 ? "" : "s") + return I18n.tr(count === 1 ? "Merged from %1 device" : "Merged from %1 devices", count) } return "" } @@ -743,8 +743,8 @@ Panel { // Adding takes over the hero's line, and the cursor starts over. readonly property string addHeading: picking || addProvider === "" - ? "Add an account" - : "Add " + (/^[AEIOU]/.test(addProviderName(addProvider)) ? "an " : "a ") + addProviderName(addProvider) + " account" + ? I18n.tr("Add an account") + : I18n.tr(/^[AEIOU]/.test(addProviderName(addProvider)) ? "Add an %1 account" : "Add a %1 account", addProviderName(addProvider)) onAddStageChanged: { phraseSwap.stop() hero.metaOpacity = 1.0 @@ -803,7 +803,7 @@ Panel { stdout: SplitParser { onRead: function(line) { root.handleAddLine(line) } } onExited: { if (root.addStage === "running") { - root.addResult = "The sign-in didn't finish." + root.addResult = I18n.tr("The sign-in didn't finish.") root.addStage = "error" } } @@ -938,7 +938,7 @@ Panel { PanelHero { id: hero width: parent.width - title: "Agents" + title: I18n.tr("Agents") meta: root.heroPhrase foreground: root.foreground fontFamily: root.fontFamily @@ -964,7 +964,7 @@ Panel { onHovered: root.pointAt("add") readonly property bool adding: root.addStage !== "" glyph: adding ? "󰅖" : "󰐕" - tooltip: adding ? "Back to the limits" : "Add a subscription" + tooltip: adding ? I18n.tr("Back to the limits") : I18n.tr("Add a subscription") onClicked: adding ? root.cancelAdd() : root.addAccount() } @@ -973,7 +973,7 @@ Panel { hasCursor: root.hasKey("launch") onHovered: root.pointAt("launch") glyph: "󰞷" - tooltip: "Start the default agent" + tooltip: I18n.tr("Start the default agent") onClicked: root.launchAgent() } } @@ -1010,7 +1010,7 @@ Panel { spacing: Style.space(12) PanelSectionHeader { - text: "MAKE SOMETHING COOL" + text: I18n.tr("MAKE SOMETHING COOL") foreground: root.foreground fontFamily: root.fontFamily } @@ -1064,7 +1064,7 @@ Panel { Text { visible: root.blankSlate && root.picking width: parent.width - text: "Sign in to an AI coding subscription, and this panel keeps track of how much of it you have left." + text: I18n.tr("Sign in to an AI coding subscription, and this panel keeps track of how much of it you have left.") color: root.dim font.family: root.fontFamily font.pixelSize: Style.font.caption @@ -1136,7 +1136,7 @@ Panel { Text { width: parent.width - text: "Name this account. It signs in through a private window, so your browser's current account isn't picked up." + text: I18n.tr("Name this account. It signs in through a private window, so your browser's current account isn't picked up.") color: root.dim font.family: root.fontFamily font.pixelSize: Style.font.caption @@ -1146,7 +1146,7 @@ Panel { TextField { id: addNameField width: parent.width - placeholderText: "Work" + placeholderText: I18n.tr("Work") foreground: root.foreground font.family: root.fontFamily font.pixelSize: Style.font.body @@ -1178,7 +1178,7 @@ Panel { spacing: Style.space(4) Text { - text: "Confirm this code in your browser" + text: I18n.tr("Confirm this code in your browser") color: root.dim font.family: root.fontFamily font.pixelSize: Style.font.caption @@ -1202,7 +1202,7 @@ Panel { Text { width: parent.width - text: "If the page shows a code instead of finishing, paste it here." + text: I18n.tr("If the page shows a code instead of finishing, paste it here.") color: root.dim font.family: root.fontFamily font.pixelSize: Style.font.caption @@ -1212,7 +1212,7 @@ Panel { TextField { id: pasteField width: parent.width - placeholderText: "Code" + placeholderText: I18n.tr("Code") foreground: root.foreground font.family: root.fontFamily font.pixelSize: Style.font.body @@ -1223,7 +1223,7 @@ Panel { TextLink { visible: root.addUrl !== "" - text: "Open the sign-in page again" + text: I18n.tr("Open the sign-in page again") onClicked: root.reopenSignIn() } } @@ -1329,9 +1329,9 @@ Panel { TextLink { visible: !section.multi && root.needsSignIn(section.provider) picked: root.hasKey("providerSignin", section.providerIndex) - text: "Sign-in required" + text: I18n.tr("Sign-in required") idleColor: root.urgent - tooltip: "Sign in to " + (section.provider ? section.provider.providerName : "") + " again" + tooltip: I18n.tr("Sign in to %1 again", section.provider ? section.provider.providerName : "") onClicked: root.signInAgain(section.provider, null) } @@ -1397,7 +1397,7 @@ Panel { textFormat: Text.PlainText width: parent.width * 0.3 anchors.verticalCenter: parent.verticalCenter - text: "Balance" + text: I18n.tr("Balance") color: root.foreground font.family: root.fontFamily font.pixelSize: Style.font.caption @@ -1567,7 +1567,7 @@ Panel { PanelToolTip { visible: tileMouse.containsMouse - text: "Start your default agent on a new " + tile.title.toLowerCase() + text: I18n.tr("Start your default agent on a new %1", tile.title.toLowerCase()) } } @@ -1771,9 +1771,9 @@ Panel { id: signInLink visible: root.needsSignIn(head.account) picked: head.pickedKind === "signin" - text: "Sign-in required" + text: I18n.tr("Sign-in required") idleColor: root.urgent - tooltip: "Sign in to this account again" + tooltip: I18n.tr("Sign in to this account again") onClicked: root.signInAgain(head.owner, head.account) } } @@ -1809,7 +1809,7 @@ Panel { id: headActive visible: head.isActive anchors.right: parent.right - text: "ACTIVE" + text: I18n.tr("ACTIVE") color: Color.accent font.family: root.fontFamily font.pixelSize: Style.font.caption @@ -1828,19 +1828,19 @@ Panel { TextLink { visible: head.autoOn || useHover.hovered || head.picked - text: head.pickedKind === "autoswitch" ? "Autoswitch ⏎" : "Autoswitch" + text: I18n.tr("Autoswitch") + (head.pickedKind === "autoswitch" ? " ⏎" : "") picked: head.pickedKind === "autoswitch" current: head.autoOn tooltip: head.autoOn - ? "Stop switching automatically" - : "Switch here automatically at " + root.switchThreshold(head.owner) + "%" + ? I18n.tr("Stop switching automatically") + : I18n.tr("Switch here automatically at %1%", root.switchThreshold(head.owner)) onClicked: root.setSwitchMode(head.owner, head.autoOn ? "manual" : "auto") } // With Autoswitch on, Use waits until the line is hovered or picked. TextLink { visible: !head.autoOn || useHover.hovered || head.picked - text: head.pickedKind === "account" ? "Use ⏎" : "Use" + text: I18n.tr("Use") + (head.pickedKind === "account" ? " ⏎" : "") picked: head.pickedKind === "account" onClicked: root.useAccount(head.owner, head.account) } @@ -1857,7 +1857,7 @@ Panel { Text { id: unavailableLabel - text: "Usage unavailable" + text: I18n.tr("Usage unavailable") color: root.dim font.family: root.fontFamily font.pixelSize: Style.font.bodySmall @@ -1867,7 +1867,7 @@ Panel { PanelToolTip { visible: unavailableHover.hovered && unavailable.visible - text: unavailable.record ? String(unavailable.record.authHelpText || "Usage has not been updated yet.") : "" + text: unavailable.record ? String(unavailable.record.authHelpText || I18n.tr("Usage has not been updated yet.")) : "" } } @@ -1892,16 +1892,16 @@ Panel { text: { var lines = [] if (compact.window) { - lines.push(compact.window.title + ": " + Math.round(compact.window.percent * 100) + "% used" - + (compact.resetMs > 0 ? " · resets in " + root.formatDuration(compact.resetMs) : "")) + lines.push(I18n.tr("%1: %2% used", compact.window.title, Math.round(compact.window.percent * 100)) + + (compact.resetMs > 0 ? " · " + I18n.tr("resets in %1", root.formatDuration(compact.resetMs)) : "")) } if (compact.stale) lines.push(compact.fetchedAt > 0 && root.nowMs - compact.fetchedAt > 60000 - ? "Last updated " + root.formatDuration(root.nowMs - compact.fetchedAt) + " ago" - : compact.fetchedAt > 0 ? "Last updated less than a minute ago" : "Last updated time unavailable") + ? I18n.tr("Last updated %1 ago", root.formatDuration(root.nowMs - compact.fetchedAt)) + : compact.fetchedAt > 0 ? I18n.tr("Last updated less than a minute ago") : I18n.tr("Last updated time unavailable")) for (var i = 0; i < compact.scoped.length; i++) - lines.push(compact.scoped[i].title + ": " + Math.round(compact.scoped[i].percent * 100) + "% of its " - + String(compact.window ? compact.window.title : "").toLowerCase() + " allowance") + lines.push(I18n.tr("%1: %2% of its %3 allowance", compact.scoped[i].title, Math.round(compact.scoped[i].percent * 100), + String(compact.window ? compact.window.title : "").toLowerCase())) return lines.join("\n") } } diff --git a/shell/plugins/bar/indicators/Dictation.qml b/shell/plugins/bar/indicators/Dictation.qml index 57f3c319..1d001aab 100644 --- a/shell/plugins/bar/indicators/Dictation.qml +++ b/shell/plugins/bar/indicators/Dictation.qml @@ -1,5 +1,6 @@ import QtQuick import Quickshell.Io +import qs.Commons import qs.Ui BarIndicator { @@ -12,7 +13,7 @@ BarIndicator { activeText: icon inactiveText: "󰍬" activeTooltipText: state - inactiveTooltipText: "Dictate" + inactiveTooltipText: I18n.tr("Dictate") function update(raw) { var data = extractData(raw) diff --git a/shell/plugins/bar/indicators/Dnd.qml b/shell/plugins/bar/indicators/Dnd.qml index 052469bf..063608a4 100644 --- a/shell/plugins/bar/indicators/Dnd.qml +++ b/shell/plugins/bar/indicators/Dnd.qml @@ -11,8 +11,8 @@ BarIndicator { active: dnd activeText: "󰂛" inactiveText: "󰂛" - activeTooltipText: "Allow Notifications" - inactiveTooltipText: "Silence Notifications" + activeTooltipText: I18n.tr("Allow Notifications") + inactiveTooltipText: I18n.tr("Silence Notifications") onPressed: function() { if (root.notificationService) { diff --git a/shell/plugins/bar/indicators/NightLight.qml b/shell/plugins/bar/indicators/NightLight.qml index b15744c7..e338cf07 100644 --- a/shell/plugins/bar/indicators/NightLight.qml +++ b/shell/plugins/bar/indicators/NightLight.qml @@ -1,4 +1,5 @@ import QtQuick +import qs.Commons import qs.Ui BarIndicator { @@ -9,8 +10,8 @@ BarIndicator { active: nightlightService ? nightlightService.enabled : false activeText: "󰔎" inactiveText: "󰔎" - activeTooltipText: "Day Light" - inactiveTooltipText: "Night Light" + activeTooltipText: I18n.tr("Day Light") + inactiveTooltipText: I18n.tr("Night Light") function toggle() { if (root.nightlightService) root.nightlightService.setNightlight(!root.active) diff --git a/shell/plugins/bar/indicators/ScreenRecording.qml b/shell/plugins/bar/indicators/ScreenRecording.qml index 271e1c39..fcebfe24 100644 --- a/shell/plugins/bar/indicators/ScreenRecording.qml +++ b/shell/plugins/bar/indicators/ScreenRecording.qml @@ -1,5 +1,6 @@ import QtQuick import Quickshell.Io +import qs.Commons import qs.Ui BarIndicator { @@ -10,8 +11,8 @@ BarIndicator { active: recording activeText: "󰻂" inactiveText: "󰻂" - activeTooltipText: "Stop recording" - inactiveTooltipText: "Screen Recording" + activeTooltipText: I18n.tr("Stop recording") + inactiveTooltipText: I18n.tr("Screen Recording") function refresh() { if (!root.bar || statusProc.running) return diff --git a/shell/plugins/bar/indicators/StayAwake.qml b/shell/plugins/bar/indicators/StayAwake.qml index 70a7829e..605022e9 100644 --- a/shell/plugins/bar/indicators/StayAwake.qml +++ b/shell/plugins/bar/indicators/StayAwake.qml @@ -1,4 +1,5 @@ import QtQuick +import qs.Commons import qs.Ui BarIndicator { @@ -9,8 +10,8 @@ BarIndicator { active: idleService ? idleService.stayAwake : false activeText: "󰅶" inactiveText: "󰅶" - activeTooltipText: "Allow Idle Lock & Screensaver" - inactiveTooltipText: "Stay Awake" + activeTooltipText: I18n.tr("Allow Idle Lock & Screensaver") + inactiveTooltipText: I18n.tr("Stay Awake") function toggle() { if (root.idleService) root.idleService.setIdleEnabled(root.active) diff --git a/shell/plugins/bar/widgets/Microphone.qml b/shell/plugins/bar/widgets/Microphone.qml index 4f1bf211..8504708b 100644 --- a/shell/plugins/bar/widgets/Microphone.qml +++ b/shell/plugins/bar/widgets/Microphone.qml @@ -1,6 +1,7 @@ import QtQuick import Quickshell import Quickshell.Services.Pipewire +import qs.Commons import qs.Ui BarWidget { @@ -40,7 +41,7 @@ BarWidget { bar: root.bar text: root.muted ? "󰍭" : "󰍬" active: root.inUse - tooltipText: root.muted ? "Microphone muted" : (root.inUse ? "Microphone in use" : "Microphone live") + tooltipText: root.muted ? I18n.tr("Microphone muted") : (root.inUse ? I18n.tr("Microphone in use") : I18n.tr("Microphone live")) onPressed: function(b) { if (b === Qt.MiddleButton) root.bar.run("omarchy-shell shell toggle omarchy.audio") else root.toggleMute() diff --git a/shell/plugins/bar/widgets/SystemUpdate.qml b/shell/plugins/bar/widgets/SystemUpdate.qml index 2baf6b0d..d5a63b19 100644 --- a/shell/plugins/bar/widgets/SystemUpdate.qml +++ b/shell/plugins/bar/widgets/SystemUpdate.qml @@ -59,7 +59,7 @@ BarWidget { text: "\uf021" slotSize: Style.bar.statusSlot fontSize: Style.font.caption - tooltipText: "Pending Omarchy Updates" + tooltipText: I18n.tr("Pending Omarchy Updates") onPressed: root.runUpdate() } } diff --git a/shell/plugins/bar/widgets/Tray.qml b/shell/plugins/bar/widgets/Tray.qml index dfe11ad7..f6b5c012 100644 --- a/shell/plugins/bar/widgets/Tray.qml +++ b/shell/plugins/bar/widgets/Tray.qml @@ -410,7 +410,7 @@ BarWidget { spacing: Style.space(8) Text { - text: "Tray icons" + text: I18n.tr("Tray icons") color: root.foreground font.family: root.fontFamily font.pixelSize: Style.font.body @@ -418,7 +418,7 @@ BarWidget { } Text { - text: "Pinned icons stay visible. Hidden icons never show." + text: I18n.tr("Pinned icons stay visible. Hidden icons never show.") color: Qt.darker(root.foreground, 1.4) font.family: root.fontFamily font.pixelSize: Style.font.caption @@ -428,7 +428,7 @@ BarWidget { Text { visible: root.allItems.length === 0 - text: "No tray items reporting." + text: I18n.tr("No tray items reporting.") color: Qt.darker(root.foreground, 1.5) font.family: root.fontFamily font.pixelSize: Style.font.bodySmall @@ -485,7 +485,7 @@ BarWidget { anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right iconText: "\uf08d" - text: rowRoot.isPinned ? "Unpin" : "Pin" + text: rowRoot.isPinned ? I18n.tr("Unpin") : I18n.tr("Pin") foreground: root.foreground horizontalPadding: 8 verticalPadding: 3 @@ -500,7 +500,7 @@ BarWidget { anchors.right: rowPinBtn.left anchors.rightMargin: Style.space(6) iconText: "\uf06e" - text: rowRoot.isHidden ? "Show" : "Hide" + text: rowRoot.isHidden ? I18n.tr("Show") : I18n.tr("Hide") foreground: root.foreground horizontalPadding: 8 verticalPadding: 3 diff --git a/shell/plugins/clipboard/Clipboard.qml b/shell/plugins/clipboard/Clipboard.qml index a8feb23a..edad2498 100644 --- a/shell/plugins/clipboard/Clipboard.qml +++ b/shell/plugins/clipboard/Clipboard.qml @@ -398,8 +398,8 @@ Item { anchors.fill: parent opened: root.clearConfirmOpen z: 10 - message: "Delete entire clipboard history?" - confirmText: "Delete" + message: I18n.tr("Delete entire clipboard history?") + confirmText: I18n.tr("Delete") background: root.background foreground: root.foreground scrim: root.scrim @@ -592,7 +592,7 @@ Item { Text { textFormat: Text.PlainText - text: root.history.length === 0 ? "Clipboard is empty" : "No matches for “" + root.filterText + "”" + text: root.history.length === 0 ? I18n.tr("Clipboard is empty") : I18n.tr("No matches for “%1”", root.filterText) color: root.foreground opacity: 0.7 font.family: root.fontFamily diff --git a/shell/plugins/emojis/Emojis.qml b/shell/plugins/emojis/Emojis.qml index b1bcdec0..2a900b2a 100644 --- a/shell/plugins/emojis/Emojis.qml +++ b/shell/plugins/emojis/Emojis.qml @@ -324,7 +324,7 @@ Item { Text { textFormat: Text.PlainText - text: "No matches for “" + root.filterText + "”" + text: I18n.tr("No matches for “%1”", root.filterText) color: root.foreground opacity: 0.7 font.family: root.fontFamily diff --git a/shell/plugins/image-picker/ImagePicker.qml b/shell/plugins/image-picker/ImagePicker.qml index bb448692..ceb1a696 100644 --- a/shell/plugins/image-picker/ImagePicker.qml +++ b/shell/plugins/image-picker/ImagePicker.qml @@ -91,7 +91,7 @@ Item { function currentLabel() { var path = currentPath() - if (!path) return filterText ? "No matches" : "" + if (!path) return filterText ? I18n.tr("No matches") : "" return labelForPath(path) } diff --git a/shell/plugins/lock/LockView.qml b/shell/plugins/lock/LockView.qml index e00ef6e9..49663701 100644 --- a/shell/plugins/lock/LockView.qml +++ b/shell/plugins/lock/LockView.qml @@ -24,7 +24,7 @@ Item { property string passwordText: "" property bool syncingPasswordText: false - readonly property string placeholderText: "Enter Password" + readonly property string placeholderText: I18n.tr("Enter Password") readonly property int fieldWidth: 381 readonly property int fieldHeight: 67 readonly property int outlineThickness: 3 @@ -222,7 +222,7 @@ Item { Text { textFormat: Text.PlainText anchors.fill: passwordInput - text: root.authenticatingPassword ? "Checking…" : (root.failureMessage.length > 0 ? root.failureMessage : root.placeholderText) + text: root.authenticatingPassword ? I18n.tr("Checking…") : (root.failureMessage.length > 0 ? root.failureMessage : root.placeholderText) visible: passwordInput.text.length === 0 color: root.authenticatingPassword ? Color.lock.text : (root.failureMessage.length > 0 ? Color.lock.textError : Color.lock.placeholder) font.family: Style.font.family @@ -265,7 +265,7 @@ Item { anchors.topMargin: 18 anchors.horizontalCenter: inputField.horizontalCenter visible: root.fingerprintConfigured && root.fingerprintUnavailable - text: "Fingerprint reader unavailable" + text: I18n.tr("Fingerprint reader unavailable") color: Color.lock.textError font.family: Style.font.family font.pixelSize: Style.font.heading diff --git a/shell/plugins/lock/Service.qml b/shell/plugins/lock/Service.qml index 41336479..f1c5304a 100644 --- a/shell/plugins/lock/Service.qml +++ b/shell/plugins/lock/Service.qml @@ -350,7 +350,7 @@ Item { enteredPassword = "" pendingPassword = "" failedAttempts += 1 - failureMessage = "Authentication failed (" + failedAttempts + ")" + failureMessage = I18n.tr("Authentication failed (%1)", failedAttempts) runWake() } diff --git a/shell/plugins/menu/Menu.qml b/shell/plugins/menu/Menu.qml index 5f614c64..63cc99c8 100644 --- a/shell/plugins/menu/Menu.qml +++ b/shell/plugins/menu/Menu.qml @@ -50,7 +50,7 @@ Item { // path doesn't have to shell out to bash + jq on every open. property string defaultMenuPath: omarchyPath + "/default/omarchy/omarchy-menu.jsonc" property string userMenuPath: Quickshell.env("HOME") + "/.config/omarchy/extensions/omarchy-menu.jsonc" - property string localeMenuPath: (Quickshell.env("LANG") || "").startsWith("zh_CN") ? omarchyPath + "/default/omarchy/omarchy-menu.zh-cn.jsonc" : "" + property string localeMenuPath: I18n.chinese ? omarchyPath + "/default/omarchy/omarchy-menu.zh-cn.jsonc" : "" property var defaultMenuItems: [] property var localeMenuItems: [] property var userMenuItems: [] @@ -867,7 +867,7 @@ Item { function openDmenu(payload) { requestSerial += 1 mode = payload.mode === "input" ? "input" : "select" - dmenuPrompt = String(payload.prompt || (mode === "input" ? "Input" : "Select")) + dmenuPrompt = String(payload.prompt || (mode === "input" ? I18n.tr("Input") : I18n.tr("Select"))) dmenuOptions = Array.isArray(payload.options) ? payload.options : [] selectionFile = String(payload.selectionFile || "") doneFile = String(payload.doneFile || "") @@ -1182,8 +1182,8 @@ Item { anchors.fill: parent opened: root.deleteConfirmOpen z: 10 - message: "Do you want to uninstall " + ((root.deleteTarget && root.deleteTarget.label) || "") + "?" - confirmText: "Uninstall" + message: I18n.tr("Do you want to uninstall %1?", (root.deleteTarget && root.deleteTarget.label) || "") + confirmText: I18n.tr("Uninstall") background: root.background foreground: root.foreground scrim: root.scrim @@ -1215,7 +1215,7 @@ Item { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - text: root.filterText || (root.dmenuActive ? (root.dmenuPrompt + "…") : ((root.item(root.activeMenu) ? (root.item(root.activeMenu).title || root.item(root.activeMenu).label) : "Go") + "…")) + text: root.filterText || (root.dmenuActive ? (root.dmenuPrompt + "…") : ((root.item(root.activeMenu) ? I18n.tr(root.item(root.activeMenu).title || root.item(root.activeMenu).label) : I18n.tr("Go")) + "…")) color: root.foreground opacity: root.filterText ? 1 : 0.58 font.family: root.fontFamily @@ -1471,7 +1471,7 @@ Item { Text { textFormat: Text.PlainText - text: root.filterText ? "No matches for “" + root.filterText + "”" : "Nothing here yet" + text: root.filterText ? I18n.tr("No matches for “%1”", root.filterText) : I18n.tr("Nothing here yet") color: root.foreground opacity: 0.7 font.family: root.fontFamily diff --git a/shell/plugins/notifications/Service.qml b/shell/plugins/notifications/Service.qml index cba13cbe..448680d0 100644 --- a/shell/plugins/notifications/Service.qml +++ b/shell/plugins/notifications/Service.qml @@ -721,7 +721,7 @@ Item { originalId: -1, app: "omarchy-action", appIcon: "", - summary: "No recent notifications", + summary: I18n.tr("No recent notifications"), body: "", image: "", glyph: "󰂚", diff --git a/shell/plugins/notifications/components/NotificationCard.qml b/shell/plugins/notifications/components/NotificationCard.qml index d7df8d83..4c0fcd90 100644 --- a/shell/plugins/notifications/components/NotificationCard.qml +++ b/shell/plugins/notifications/components/NotificationCard.qml @@ -44,7 +44,7 @@ BorderSurface { readonly property bool singleLineToast: sanitizedBody.length === 0 readonly property bool collapseRedundantIcon: singleLineToast && !hasGlyph && summaryStartsWithGlyph readonly property string sanitizedBody: sanitizeBody(body) - readonly property string styledBody: NotificationLogic.styledBody(body, app, appIcon) + readonly property string styledBody: NotificationLogic.styledBody(I18n.tr(body), app, appIcon) readonly property color dimColor: Qt.darker(Color.notifications.text, 1.4) readonly property color bodyColor: Qt.darker(Color.notifications.text, 1.15) @@ -168,7 +168,7 @@ BorderSurface { textFormat: Text.PlainText Layout.fillWidth: true visible: root.summary.length > 0 - text: root.summary + text: I18n.tr(root.summary) font.family: "Liberation Sans" color: Color.notifications.text font.pixelSize: Style.font.title diff --git a/shell/plugins/osd/Osd.qml b/shell/plugins/osd/Osd.qml index 69ddc25b..6b36f00f 100644 --- a/shell/plugins/osd/Osd.qml +++ b/shell/plugins/osd/Osd.qml @@ -90,7 +90,7 @@ Item { font.family: Style.font.family font.bold: true font.pixelSize: Style.font.title - text: root.message + text: I18n.tr(root.message) } TextMetrics { @@ -190,7 +190,7 @@ Item { // a hole in the padding; the slack lands in the gap after the bar. horizontalAlignment: root.hasProgress ? Text.AlignRight : Text.AlignLeft anchors.verticalCenter: parent.verticalCenter - text: root.message + text: I18n.tr(root.message) font: messageMetrics.font color: Color.popups.text elide: Text.ElideRight diff --git a/shell/plugins/panels/audio/Panel.qml b/shell/plugins/panels/audio/Panel.qml index 548c409b..3b78f54c 100644 --- a/shell/plugins/panels/audio/Panel.qml +++ b/shell/plugins/panels/audio/Panel.qml @@ -176,7 +176,7 @@ Panel { readonly property bool hasOutput: !!(volumeSink && volumeSink.audio) readonly property bool hasInput: !!(source && source.audio) readonly property bool anyAudible: (hasOutput && !outputMuted) || (hasInput && !inputMuted) - readonly property string toggleHint: anyAudible ? "Mute" : "Unmute" + readonly property string toggleHint: anyAudible ? I18n.tr("Mute") : I18n.tr("Unmute") readonly property color hoverFill: bar ? Style.hoverFillFor(bar.foreground, Color.accent) @@ -434,7 +434,7 @@ Panel { // panel's brightnessName ladder; bands are wide enough that small // tweaks don't rename the room you're in. function outputVolumeName(volume, muted) { - return Model.outputVolumeName(volume, muted) + return I18n.tr(Model.outputVolumeName(volume, muted)) } function setOutputVolume(v) { @@ -768,7 +768,7 @@ Panel { spacing: Style.space(2) Text { - text: "Audio" + text: I18n.tr("Audio") color: root.bar.foreground font.family: root.bar.fontFamily font.pixelSize: Style.font.title @@ -810,7 +810,7 @@ Panel { PanelSectionHeader { id: outputHeader - text: "OUTPUT" + text: I18n.tr("OUTPUT") foreground: root.bar.foreground fontFamily: root.bar.fontFamily anchors.left: parent.left @@ -897,7 +897,7 @@ Panel { PanelSectionHeader { id: microphoneHeader - text: "INPUT" + text: I18n.tr("INPUT") foreground: root.bar.foreground fontFamily: root.bar.fontFamily anchors.left: parent.left @@ -1000,7 +1000,7 @@ Panel { visible: root.displayAudioStreams.length > 0 PanelSectionHeader { - text: "SOURCES" + text: I18n.tr("SOURCES") foreground: root.bar.foreground fontFamily: root.bar.fontFamily } diff --git a/shell/plugins/panels/bluetooth/Panel.qml b/shell/plugins/panels/bluetooth/Panel.qml index 11b0ce0c..696c664c 100644 --- a/shell/plugins/panels/bluetooth/Panel.qml +++ b/shell/plugins/panels/bluetooth/Panel.qml @@ -64,19 +64,19 @@ Panel { property int phraseIndex: 0 readonly property var activePhrases: [ - "Untangling wires", - "Streaming vikings", - "Pairing mysteries", - "Herding headsets", - "Taming radios", - "Summoning speakers", - "Wrangling codecs", - "Polishing packets" + I18n.tr("Untangling wires"), + I18n.tr("Streaming vikings"), + I18n.tr("Pairing mysteries"), + I18n.tr("Herding headsets"), + I18n.tr("Taming radios"), + I18n.tr("Summoning speakers"), + I18n.tr("Wrangling codecs"), + I18n.tr("Polishing packets") ] readonly property bool rotatingPhrases: adapter && adapter.enabled readonly property string heroStatusText: { - if (!adapter) return "No adapter" - if (!adapter.enabled) return "Turned Off" + if (!adapter) return I18n.tr("No adapter") + if (!adapter.enabled) return I18n.tr("Turned Off") return activePhrases[phraseIndex % activePhrases.length] } @@ -101,7 +101,7 @@ Panel { // sits above the device sections so the adapter can be toggled by keyboard // even when it is off and no device rows exist. readonly property bool headerHasCursor: cursorActive && focusSection === "header" - readonly property string toggleHint: root.adapter && root.adapter.enabled ? "Turn Bluetooth off" : "Turn Bluetooth on" + readonly property string toggleHint: root.adapter && root.adapter.enabled ? I18n.tr("Turn Bluetooth off") : I18n.tr("Turn Bluetooth on") readonly property color hoverFill: bar ? Style.hoverFillFor(bar.foreground, Color.accent) @@ -183,7 +183,7 @@ Panel { var rows = scrollRows if (index < 0 || index >= rows.length) return "" if (index > 0 && rows[index - 1].section === rows[index].section) return "" - return rows[index].section === "known" ? "PAIRED" : "AVAILABLE" + return rows[index].section === "known" ? I18n.tr("PAIRED") : I18n.tr("AVAILABLE") } function audioSinks() { @@ -738,7 +738,7 @@ Panel { spacing: Style.space(2) Text { - text: "Bluetooth" + text: I18n.tr("Bluetooth") color: root.bar.foreground font.family: root.bar.fontFamily font.pixelSize: Style.font.title @@ -775,7 +775,7 @@ Panel { spacing: Style.space(10) PanelSectionHeader { - text: "CONNECTED" + text: I18n.tr("CONNECTED") foreground: root.bar.foreground fontFamily: root.bar.fontFamily } @@ -867,9 +867,9 @@ Panel { Text { textFormat: Text.PlainText visible: root.connectedDevices.length === 0 && root.scrollRows.length === 0 - text: !root.adapter ? "No Bluetooth adapter" - : !root.adapter.enabled ? "Turn Bluetooth on to scan" - : "Scanning for devices…" + text: !root.adapter ? I18n.tr("No Bluetooth adapter") + : !root.adapter.enabled ? I18n.tr("Turn Bluetooth on to scan") + : I18n.tr("Scanning for devices…") color: Qt.darker(root.bar.foreground, 1.5) font.family: root.bar.fontFamily font.pixelSize: Style.font.bodySmall @@ -894,9 +894,9 @@ Panel { readonly property string action: root.pendingAction(dev ? dev.address : "") readonly property string actionTooltip: { if (!dev) return "" - if (isConnected) return "Disconnect" - if (isDiscovered) return "Pair" - return "Connect" + if (isConnected) return I18n.tr("Disconnect") + if (isDiscovered) return I18n.tr("Pair") + return I18n.tr("Connect") } readonly property bool rowSelected: root.cursorActive && root.focusSection === sectionName && root.selectedIndex === rowIndex @@ -911,13 +911,13 @@ Panel { readonly property string statusText: { if (!dev) return "" - if (action === "forgetting") return "Forgetting…" - if (action === "disconnecting" || devState === 2) return "Disconnecting…" + if (action === "forgetting") return I18n.tr("Forgetting…") + if (action === "disconnecting" || devState === 2) return I18n.tr("Disconnecting…") if (isConnected) { if (dev.batteryAvailable) return Math.round(dev.battery * 100) + "%" - return sectionName === "connected" ? "" : "Connected" + return sectionName === "connected" ? "" : I18n.tr("Connected") } - if (action === "connecting" || devState === 3 || dev.pairing === true) return "Connecting…" + if (action === "connecting" || devState === 3 || dev.pairing === true) return I18n.tr("Connecting…") if (isDiscovered) return "" return "" } @@ -994,7 +994,7 @@ Panel { Text { textFormat: Text.PlainText - text: root.deviceLabel(row.dev) || "Device" + text: root.deviceLabel(row.dev) || I18n.tr("Device") color: root.bar.foreground font.family: root.bar.fontFamily font.pixelSize: Style.font.body @@ -1019,7 +1019,7 @@ Panel { anchors.verticalCenter: parent.verticalCenter visible: row.showForgetButton iconText: "󰅙" - tooltipText: "Forget" + tooltipText: I18n.tr("Forget") foreground: root.bar.foreground hoverColor: root.bar.foreground fontFamily: root.bar.fontFamily diff --git a/shell/plugins/panels/clock/BarWidget.qml b/shell/plugins/panels/clock/BarWidget.qml index 3235a1cc..81f7b038 100644 --- a/shell/plugins/panels/clock/BarWidget.qml +++ b/shell/plugins/panels/clock/BarWidget.qml @@ -21,7 +21,7 @@ BarWidget { : setting("format", "dddd HH:mm") readonly property string configuredAltFormat: vertical ? setting("verticalFormatAlt", "dd\nMMM\n'W'ww\n''yy") - : setting("formatAlt", "d MMMM 'W'ww yyyy") + : setting("formatAlt", I18n.chinese ? "yyyy年M月d日 'W'ww" : "d MMMM 'W'ww yyyy") readonly property var formatRing: Model.clockFormatRing(configuredFormat, configuredAltFormat, Model.clockFormats(vertical)) @@ -56,8 +56,10 @@ BarWidget { root.bar.shell.updateEntryInline(root.moduleName, entry) } + // Qt.formatDateTime is always English; a Chinese session takes day and month names from its locale. function formatted(date) { - return Qt.formatDateTime(date, activeFormat.replace(/ww/g, Model.isoWeekLiteral(date.getFullYear(), date.getMonth(), date.getDate()))) + var format = activeFormat.replace(/ww/g, Model.isoWeekLiteral(date.getFullYear(), date.getMonth(), date.getDate())) + return I18n.chinese ? Qt.locale().toString(date, format) : Qt.formatDateTime(date, format) } // ---- Calendar popup. Shape contract for shell.summon/hide/toggle @@ -153,7 +155,7 @@ BarWidget { fixedHeight: root.vertical ? root.verticalLines.length * Style.bar.iconSlot : -1 horizontalMargin: 8.75 verticalPadding: 8.75 - tooltipText: "Right-click to toggle format" + tooltipText: I18n.tr("Right-click to toggle format") onPressed: function(b) { if (b === Qt.RightButton) root.cycleFormat() diff --git a/shell/plugins/panels/clock/Panel.qml b/shell/plugins/panels/clock/Panel.qml index ec55afd3..227bcf59 100644 --- a/shell/plugins/panels/clock/Panel.qml +++ b/shell/plugins/panels/clock/Panel.qml @@ -64,10 +64,8 @@ Panel { // convention. Clicking the grid's "W" heading writes the choice back to // shell.json. readonly property int weekStart: Model.normalizedWeekStart(setting("weekStartDay", null), Qt.locale().firstDayOfWeek) - // The interface is English throughout, so day names are not taken from the - // system locale. Where the week starts still is: that is a regional - // convention rather than a translation, and it stays overridable above. - readonly property var labelLocale: Qt.locale("en_US") + // Day names follow the interface language, not the system locale; the week start above is a regional convention. + readonly property var labelLocale: I18n.chinese ? Qt.locale("zh_CN") : Qt.locale("en_US") readonly property string nextWeekStartLabel: labelLocale.dayName(Model.toggledWeekStart(weekStart), Locale.LongFormat) readonly property var weekdays: Model.weekdayOrder(weekStart) readonly property var weeks: Model.monthGrid(viewYear, viewMonth, weekStart, todayKey) @@ -315,7 +313,7 @@ Panel { id: heroDate textFormat: Text.PlainText anchors.verticalCenter: parent.verticalCenter - text: Qt.formatDate(root.today, "MMMM d") + text: I18n.chinese ? root.labelLocale.toString(root.today, "M月d日") : Qt.formatDate(root.today, "MMMM d") color: heroMouse.containsMouse ? Style.hoverStateColor(root.contentForeground, Color.accent) : root.contentForeground @@ -338,7 +336,7 @@ Panel { PanelToolTip { visible: heroMouse.containsMouse - text: "Back to today" + text: I18n.tr("Back to today") fontFamily: root.contentFontFamily } } @@ -371,7 +369,7 @@ Panel { Text { anchors.verticalCenter: parent.verticalCenter - text: "BORN" + text: I18n.tr("BORN") color: Qt.darker(root.contentForeground, 1.5) font.family: root.contentFontFamily font.pixelSize: Style.font.bodySmall @@ -394,7 +392,7 @@ Panel { anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: 0 leftPadding: Style.space(6) - text: "LIVE TO" + text: I18n.tr("LIVE TO") color: Qt.darker(root.contentForeground, 1.5) font.family: root.contentFontFamily font.pixelSize: Style.font.bodySmall @@ -481,7 +479,7 @@ Panel { id: lifeLabel anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter - text: "LIFE" + text: I18n.tr("LIFE") color: Qt.darker(root.contentForeground, 1.5) font.family: root.contentFontFamily font.pixelSize: Style.font.bodySmall @@ -600,7 +598,7 @@ Panel { PanelToolTip { visible: weekStartMouse.containsMouse - text: "Start weeks on " + root.nextWeekStartLabel + text: I18n.tr("Start weeks on %1", root.nextWeekStartLabel) fontFamily: root.contentFontFamily } } @@ -723,7 +721,7 @@ Panel { // "MAY 2026" and a "SEPTEMBER 2026". width: Style.space(130) horizontalAlignment: Text.AlignHCenter - text: Qt.formatDate(root.viewDate, "MMMM yyyy").toUpperCase() + text: I18n.chinese ? root.labelLocale.toString(root.viewDate, "yyyy年M月") : Qt.formatDate(root.viewDate, "MMMM yyyy").toUpperCase() color: Qt.darker(root.contentForeground, 1.4) font.family: root.contentFontFamily font.pixelSize: Style.font.body @@ -737,7 +735,7 @@ Panel { anchors.leftMargin: -Style.space(8) anchors.verticalCenter: parent.verticalCenter iconText: "󰅁" - tooltipText: "Previous month" + tooltipText: I18n.tr("Previous month") foreground: root.contentForeground fontFamily: root.contentFontFamily onClicked: root.moveMonth(-1) @@ -748,7 +746,7 @@ Panel { anchors.rightMargin: -Style.space(8) anchors.verticalCenter: parent.verticalCenter iconText: "󰅂" - tooltipText: "Next month" + tooltipText: I18n.tr("Next month") foreground: root.contentForeground fontFamily: root.contentFontFamily onClicked: root.moveMonth(1) diff --git a/shell/plugins/panels/disk-speedtest/Panel.qml b/shell/plugins/panels/disk-speedtest/Panel.qml index 3b86f5be..256699eb 100644 --- a/shell/plugins/panels/disk-speedtest/Panel.qml +++ b/shell/plugins/panels/disk-speedtest/Panel.qml @@ -117,7 +117,7 @@ Item { } if (!root.expectedStop && exitCode !== 0) { - root.error = root.stderrText || "Disk speed test failed" + root.error = root.stderrText || I18n.tr("Disk speed test failed") root.phase = "" root.running = false return @@ -133,10 +133,10 @@ Item { fontFamily: Style.font.family layerNamespace: "omarchy-disk-speedtest" title: root.diskName - leftLabel: "READ" - rightLabel: "WRITE" + leftLabel: I18n.tr("READ") + rightLabel: I18n.tr("WRITE") unit: "MB/s" - runAgainTooltip: "Measure again" + runAgainTooltip: I18n.tr("Measure again") running: root.running leftValue: root.toRate(root.readMBps) rightValue: root.toRate(root.writeMBps) diff --git a/shell/plugins/panels/dropbox/Panel.qml b/shell/plugins/panels/dropbox/Panel.qml index 1a415846..a050ce27 100644 --- a/shell/plugins/panels/dropbox/Panel.qml +++ b/shell/plugins/panels/dropbox/Panel.qml @@ -20,16 +20,16 @@ Panel { property int phraseIndex: 0 readonly property var activePhrases: [ - "Filing files", - "Distributing data", - "Shuffling folders", - "Boxing bytes", - "Sorting stuff", - "Syncing secrets", - "Packing packets", - "Moving memories", - "Wrangling revisions", - "Cataloging chaos" + I18n.tr("Filing files"), + I18n.tr("Distributing data"), + I18n.tr("Shuffling folders"), + I18n.tr("Boxing bytes"), + I18n.tr("Sorting stuff"), + I18n.tr("Syncing secrets"), + I18n.tr("Packing packets"), + I18n.tr("Moving memories"), + I18n.tr("Wrangling revisions"), + I18n.tr("Cataloging chaos") ] readonly property string heroPhraseText: activePhrases[phraseIndex % activePhrases.length] readonly property color foreground: bar ? bar.foreground : Color.foreground @@ -37,7 +37,7 @@ Panel { readonly property color dim: Qt.darker(foreground, 1.55) readonly property string fontFamily: bar ? bar.fontFamily : Style.font.family readonly property color iconColor: dropbox.authenticated && dropbox.active ? foreground : dim - readonly property string toggleHint: dropbox.active ? "Pause syncing" : "Resume syncing" + readonly property string toggleHint: dropbox.active ? I18n.tr("Pause syncing") : I18n.tr("Resume syncing") readonly property color barIconColor: dropbox.authenticated && dropbox.active ? barForeground : Qt.darker(barForeground, 1.55) // Only claim the header cursor when the switch is actually on screen — // "header" stays navigable, but an absent CLI leaves nothing to highlight. @@ -244,7 +244,7 @@ Panel { id: hero width: parent.width title: "Dropbox" - meta: dropbox.active ? root.heroPhraseText : "Syncing paused" + meta: dropbox.active ? root.heroPhraseText : I18n.tr("Syncing paused") foreground: root.foreground fontFamily: root.fontFamily iconOpacity: dropbox.active ? 1.0 : 0.5 @@ -304,7 +304,7 @@ Panel { Column { width: parent.width spacing: Style.spacing.labelGap - InfoPair { label: "Stored"; value: Model.usageText(dropbox.usedBytes, dropbox.quotaBytes, dropbox.quotaKnown) } + InfoPair { label: I18n.tr("Stored"); value: Model.usageText(dropbox.usedBytes, dropbox.quotaBytes, dropbox.quotaKnown) } } } @@ -319,7 +319,7 @@ Panel { spacing: Style.space(10) PanelSectionHeader { - text: "RECENT FILES" + text: I18n.tr("RECENT FILES") foreground: root.foreground fontFamily: root.fontFamily } @@ -327,7 +327,7 @@ Panel { Text { visible: dropbox.files.length === 0 width: parent.width - text: "No synced files found." + text: I18n.tr("No synced files found.") color: root.dim font.family: root.fontFamily font.pixelSize: Style.font.body @@ -424,7 +424,7 @@ Panel { Text { textFormat: Text.PlainText Layout.fillWidth: true - text: dropbox.installed ? "Login to Dropbox" : "Dropbox CLI is not installed" + text: dropbox.installed ? I18n.tr("Login to Dropbox") : I18n.tr("Dropbox CLI is not installed") color: root.foreground font.family: root.fontFamily font.pixelSize: Style.font.body @@ -434,7 +434,7 @@ Panel { Text { textFormat: Text.PlainText Layout.fillWidth: true - text: dropbox.installed ? "Start the authentication flow" : "Install Dropbox from the service menu" + text: dropbox.installed ? I18n.tr("Start the authentication flow") : I18n.tr("Install Dropbox from the service menu") color: root.dim font.family: root.fontFamily font.pixelSize: Style.font.caption @@ -457,7 +457,7 @@ Panel { id: fileRow property var file: null property int rowIndex: 0 - readonly property string fileName: file ? String(file.name || "Untitled") : "Untitled" + readonly property string fileName: file ? String(file.name || I18n.tr("Untitled")) : I18n.tr("Untitled") hasCursor: root.cursorActive && root.focusSection === "files" && root.fileIndex === rowIndex foreground: root.foreground diff --git a/shell/plugins/panels/elsewhen/CityMatches.qml b/shell/plugins/panels/elsewhen/CityMatches.qml index bb80f789..8e1df8b6 100644 --- a/shell/plugins/panels/elsewhen/CityMatches.qml +++ b/shell/plugins/panels/elsewhen/CityMatches.qml @@ -73,7 +73,7 @@ Column { width: parent.width horizontalAlignment: Text.AlignHCenter topPadding: Style.spacing.md - text: list.citySearch.loading ? list.citySearch.loadingText : "No matches" + text: list.citySearch.loading ? list.citySearch.loadingText : I18n.tr("No matches") color: list.citySearch.fainter } diff --git a/shell/plugins/panels/elsewhen/CityRow.qml b/shell/plugins/panels/elsewhen/CityRow.qml index 6222c014..6d05e2d4 100644 --- a/shell/plugins/panels/elsewhen/CityRow.qml +++ b/shell/plugins/panels/elsewhen/CityRow.qml @@ -300,7 +300,7 @@ Rectangle { color: Model.mix(Color.popups.background, row.foreground, _hot ? 0.24 : 0.14) borderSpec: Border.flat(Model.mix(Color.popups.background, row.foreground, _hot ? 0.65 : 0.35), Style.space(1)) iconText: "\u00d7" - tooltipText: "Remove" + tooltipText: I18n.tr("Remove") foreground: row.dim hoverColor: row.foreground fontFamily: row.fontFamily diff --git a/shell/plugins/panels/elsewhen/CitySearch.qml b/shell/plugins/panels/elsewhen/CitySearch.qml index e91cae2c..f0c1c89a 100644 --- a/shell/plugins/panels/elsewhen/CitySearch.qml +++ b/shell/plugins/panels/elsewhen/CitySearch.qml @@ -17,7 +17,7 @@ Column { property var offsetLabel: function(zoneId) { return "" } property bool loading: false property string buttonText: "+ Add a city" - property string placeholderText: "Search cities\u2026" + property string placeholderText: I18n.tr("Search cities…") property string loadingText: "Loading zones\u2026" property real fontSize: Style.font.bodySmall property color foreground: Color.foreground diff --git a/shell/plugins/panels/elsewhen/DaylightStrip.qml b/shell/plugins/panels/elsewhen/DaylightStrip.qml index 4c171c98..f899a1c2 100644 --- a/shell/plugins/panels/elsewhen/DaylightStrip.qml +++ b/shell/plugins/panels/elsewhen/DaylightStrip.qml @@ -65,8 +65,8 @@ Item { var out = [] var up = Sun.eventMark(sun.riseMinutes) var down = Sun.eventMark(sun.setMinutes) - if (up !== null) out.push({ x: up, minutes: sun.riseMinutes, rising: true, name: "Sunrise" }) - if (down !== null) out.push({ x: down, minutes: sun.setMinutes, rising: false, name: "Sunset" }) + if (up !== null) out.push({ x: up, minutes: sun.riseMinutes, rising: true, name: I18n.tr("Sunrise") }) + if (down !== null) out.push({ x: down, minutes: sun.setMinutes, rising: false, name: I18n.tr("Sunset") }) return out } diff --git a/shell/plugins/panels/elsewhen/Globe.qml b/shell/plugins/panels/elsewhen/Globe.qml index 335312ec..4769cdc0 100644 --- a/shell/plugins/panels/elsewhen/Globe.qml +++ b/shell/plugins/panels/elsewhen/Globe.qml @@ -584,7 +584,7 @@ Item { limit: 5 inlineResults: false loading: root.jumpOptions.length === 0 - buttonText: "Jump to a city" + buttonText: I18n.tr("Jump to a city") loadingText: "Loading cities\u2026" offsetLabel: function(zoneId) { return Model.utcOffsetLabel(root.offsets[zoneId]) } foreground: root.foreground diff --git a/shell/plugins/panels/elsewhen/HeroTitle.qml b/shell/plugins/panels/elsewhen/HeroTitle.qml index 9629408f..238ea63c 100644 --- a/shell/plugins/panels/elsewhen/HeroTitle.qml +++ b/shell/plugins/panels/elsewhen/HeroTitle.qml @@ -72,7 +72,7 @@ Column { anchors.horizontalCenter: parent.horizontalCenter spacing: Style.space(9) - TitleWord { text: "World" } + TitleWord { text: I18n.tr("World") } Item { id: heroIcon @@ -150,7 +150,7 @@ Column { } } - TitleWord { text: "Clock" } + TitleWord { text: I18n.tr("Clock") } } component TitleWord: Text { diff --git a/shell/plugins/panels/elsewhen/Panel.qml b/shell/plugins/panels/elsewhen/Panel.qml index cfd6722f..eeda5551 100644 --- a/shell/plugins/panels/elsewhen/Panel.qml +++ b/shell/plugins/panels/elsewhen/Panel.qml @@ -334,7 +334,7 @@ Panel { } // No full stop: a period hanging off the arc reads as a speck. - readonly property string hereLine: "It's " + localTime + " here" + readonly property string hereLine: I18n.tr("It's %1 here", localTime) + (homeCity !== "" ? " in " + homeCity : "") + (scrubLabel !== "" ? " " + scrubLabel : "") @@ -714,7 +714,7 @@ Panel { bar: root.bar slotSize: Style.bar.statusSlot text: "󰇧" - tooltipText: "World clock" + tooltipText: I18n.tr("World clock") onPressed: function(buttonCode) { if (buttonCode === Qt.RightButton || buttonCode === Qt.MiddleButton) root.refresh() else root.toggle() diff --git a/shell/plugins/panels/monitor/Panel.qml b/shell/plugins/panels/monitor/Panel.qml index 11708d3c..b703b719 100644 --- a/shell/plugins/panels/monitor/Panel.qml +++ b/shell/plugins/panels/monitor/Panel.qml @@ -555,7 +555,7 @@ Panel { spacing: Style.space(2) Text { - text: "Display" + text: I18n.tr("Display") color: root.bar.foreground font.family: root.bar.fontFamily font.pixelSize: Style.font.title @@ -569,9 +569,9 @@ Panel { textFormat: Text.PlainText text: { if (root.brightnessAvailable) { - return root.brightnessName(brightnessSlider.dragging ? brightnessSlider.liveValue : root.brightnessPercent).toUpperCase() + return I18n.tr(root.brightnessName(brightnessSlider.dragging ? brightnessSlider.liveValue : root.brightnessPercent)).toUpperCase() } - return "FIXED BRIGHTNESS" + return I18n.tr("FIXED BRIGHTNESS") } color: Qt.darker(root.bar.foreground, 1.4) font.family: root.bar.fontFamily @@ -601,7 +601,7 @@ Panel { PanelSectionHeader { id: brightnessHeader - text: "BRIGHTNESS" + text: I18n.tr("BRIGHTNESS") foreground: root.bar.foreground fontFamily: root.bar.fontFamily anchors.left: parent.left @@ -674,7 +674,7 @@ Panel { PanelSectionHeader { id: textSizeHeader - text: "TEXT SIZE" + text: I18n.tr("TEXT SIZE") foreground: root.bar.foreground fontFamily: root.bar.fontFamily anchors.left: parent.left @@ -746,7 +746,7 @@ Panel { PanelSectionHeader { id: scaleHeader - text: "SCALE" + text: I18n.tr("SCALE") foreground: root.bar.foreground fontFamily: root.bar.fontFamily anchors.left: parent.left @@ -808,7 +808,7 @@ Panel { visible: root.displays.length > 1 PanelSectionHeader { - text: "DISPLAYS" + text: I18n.tr("DISPLAYS") foreground: root.bar.foreground fontFamily: root.bar.fontFamily } diff --git a/shell/plugins/panels/network/Panel.qml b/shell/plugins/panels/network/Panel.qml index a5ea65ee..cdcbab01 100644 --- a/shell/plugins/panels/network/Panel.qml +++ b/shell/plugins/panels/network/Panel.qml @@ -56,13 +56,13 @@ Panel { readonly property bool hasTransferStats: info.rx_bytes !== undefined property int connectionPhraseIndex: 0 readonly property var connectionPhrases: [ - "Wiring bits", - "Handling packets", - "Sorting frames", - "Hauling bytes", - "Routing crumbs", - "Counting collisions", - "Bending light", + I18n.tr("Wiring bits"), + I18n.tr("Handling packets"), + I18n.tr("Sorting frames"), + I18n.tr("Hauling bytes"), + I18n.tr("Routing crumbs"), + I18n.tr("Counting collisions"), + I18n.tr("Bending light"), ] readonly property string connectionPhrase: connectionPhrases[connectionPhraseIndex % connectionPhrases.length] readonly property bool networkManagerAvailable: Networking.backend === NetworkBackendType.NetworkManager @@ -137,7 +137,7 @@ Panel { readonly property bool qrHeaderHasCursor: cursorActive && focusSection === "header" && headerIndex === qrHeaderIndex readonly property bool speedHeaderHasCursor: cursorActive && focusSection === "header" && headerIndex === speedHeaderIndex readonly property bool toggleHeaderHasCursor: cursorActive && focusSection === "header" && headerIndex === toggleHeaderIndex - readonly property string toggleHint: Networking.wifiEnabled ? "Turn Wi-Fi off" : "Turn Wi-Fi on" + readonly property string toggleHint: Networking.wifiEnabled ? I18n.tr("Turn Wi-Fi off") : I18n.tr("Turn Wi-Fi on") readonly property var dnsProviders: ["DHCP", "Cloudflare", "Google", "Custom"] property int dnsIndex: 0 // ["2.4", "5", ...], or empty when there is nothing to choose between. @@ -159,7 +159,10 @@ Panel { // Under Automatic there is nothing to pick, so the pills collapse away and // the header states the live band instead. readonly property bool bandPillsVisible: canSelectBand && bandPinned - readonly property string bandSectionTitle: Model.bandSectionTitle(bandEffective, bandCurrent) + readonly property string bandSectionTitle: { + var title = Model.bandSectionTitle(bandEffective, bandCurrent) + return title.indexOf("WI-FI BAND: ") === 0 ? I18n.tr("WI-FI BAND: %1", title.slice(12)) : I18n.tr(title) + } readonly property bool bandBusy: pendingBand !== "" // The speed test needs an interface to test, so its hero action only // appears once there is one. @@ -279,11 +282,12 @@ Panel { } function bandLabel(band) { - return Model.bandLabel(band) + return I18n.tr(Model.bandLabel(band)) } function bandTooltip(band) { - return Model.bandTooltip(band) + var tip = Model.bandTooltip(band) + return tip.indexOf("Stay on ") === 0 ? I18n.tr("Stay on %1", bandLabel(band)) : I18n.tr(tip) } // Single cursor model: exactly one highlighted spot across the whole @@ -662,7 +666,7 @@ Panel { } function formatPingLatency(ms) { - return Model.formatPingLatency(ms, hasInternetPing) + return I18n.tr(Model.formatPingLatency(ms, hasInternetPing)) } function formatPacketLoss(percent) { @@ -709,7 +713,7 @@ Panel { } function wifiSectionTitle(index) { - return Model.wifiSectionTitle(wifiNetworks, index) + return I18n.tr(Model.wifiSectionTitle(wifiNetworks, index)) } function wifiIconFor(strength) { @@ -754,7 +758,7 @@ Panel { cancelPasswordPrompt() var connection = "" if (info.type === "wifi") connection = info.ssid || "Wi-Fi" - else if (info.type === "ethernet") connection = "Ethernet" + else if (info.type === "ethernet") connection = I18n.tr("Ethernet") bar.shell.summon("omarchy.speedtest", connection ? JSON.stringify({ connection: connection }) : "{}") } @@ -835,7 +839,7 @@ Panel { if (!network || actionKind === "" || actionSsid !== (network.name || "")) return actionTimeout.stop() failureSsid = actionSsid - failureReason = networkFailureReason(reason, requiresCredentials(network.security)) + failureReason = I18n.tr(networkFailureReason(reason, requiresCredentials(network.security))) actionSsid = "" actionKind = "" refresh() @@ -1062,9 +1066,9 @@ Panel { onTriggered: { if (!root.actionKind) return var reason - if (root.actionKind === "connect") reason = "Timed out connecting" - else if (root.actionKind === "disconnect") reason = "Timed out disconnecting" - else reason = "Timed out forgetting" + if (root.actionKind === "connect") reason = I18n.tr("Timed out connecting") + else if (root.actionKind === "disconnect") reason = I18n.tr("Timed out disconnecting") + else reason = I18n.tr("Timed out forgetting") root.failureSsid = root.actionSsid root.failureReason = reason root.actionSsid = "" @@ -1079,8 +1083,8 @@ Panel { bar: root.bar text: root.icon active: root.restricted - tooltipText: root.hasCaptivePortal ? "Sign in to this network" - : (root.restricted ? "Limited internet access" : "") + tooltipText: root.hasCaptivePortal ? I18n.tr("Sign in to this network") + : (root.restricted ? I18n.tr("Limited internet access") : "") onPressed: function(b) { if (root.opened) root.close() @@ -1249,7 +1253,7 @@ Panel { id: qrAction visible: root.canShareWifi iconText: "󰐲" - tooltipText: "Show QR code" + tooltipText: I18n.tr("Show QR code") foreground: root.bar.foreground fontFamily: root.bar.fontFamily iconSize: Style.font.subtitle * 1.5 @@ -1265,7 +1269,7 @@ Panel { id: speedAction visible: root.canRunSpeedTest iconText: "󰓅" - tooltipText: "Run a speed test" + tooltipText: I18n.tr("Run a speed test") foreground: root.bar.foreground fontFamily: root.bar.fontFamily iconSize: Style.font.subtitle * 1.5 @@ -1316,8 +1320,8 @@ Panel { // SSID even before route/details polling has returned anything. if (root.kind === "wifi" && root.connectedWifiNetwork) return root.connectedWifiNetwork.name || "Wi-Fi" if (root.info.type === "wifi") return root.info.ssid || "Wi-Fi" - if (root.info.type === "ethernet") return "Ethernet" - return root.info.iface || (root.kind === "disconnected" ? "Disconnected" : "No connection") + if (root.info.type === "ethernet") return I18n.tr("Ethernet") + return root.info.iface || (root.kind === "disconnected" ? I18n.tr("Disconnected") : I18n.tr("No connection")) } readonly property string detail: root.headerDetail() @@ -1334,15 +1338,15 @@ Panel { textFormat: Text.PlainText width: parent.width text: { - if (root.hasCaptivePortal) return "SIGN-IN REQUIRED" - if (root.restricted) return "LIMITED INTERNET ACCESS" + if (root.hasCaptivePortal) return I18n.tr("SIGN-IN REQUIRED") + if (root.restricted) return I18n.tr("LIMITED INTERNET ACCESS") if (root.info.type === "wifi") { if (root.canDisconnect) return root.connectionPhrase.toUpperCase() - if (root.kind === "disconnected") return "NOT CONNECTED" + if (root.kind === "disconnected") return I18n.tr("NOT CONNECTED") return "" } if (root.info.type === "ethernet") return root.connectionPhrase.toUpperCase() - if (root.kind === "disconnected") return "NOT CONNECTED" + if (root.kind === "disconnected") return I18n.tr("NOT CONNECTED") return "" } visible: text !== "" @@ -1365,7 +1369,7 @@ Panel { Button { id: portalAction width: parent.width - text: "Open Captive Portal" + text: I18n.tr("Open Captive Portal") iconText: "󰏌" foreground: root.bar.urgent accent: root.bar.urgent @@ -1384,7 +1388,7 @@ Panel { Text { width: parent.width - text: "Sign in or accept this network’s terms to access the internet." + text: I18n.tr("Sign in or accept this network’s terms to access the internet.") textFormat: Text.PlainText wrapMode: Text.WordWrap color: root.bar.foreground @@ -1410,38 +1414,38 @@ Panel { // opened, once the first probe returned, shoving everything below // them down. They now hold their place and read "--" until there is // a sample. - InfoLabel { text: "Ping" } + InfoLabel { text: I18n.tr("Ping") } DetailValue { text: root.formatPingLatency(root.internetPingLatency) color: root.internetPingPacketLoss > 0 ? root.bar.urgent : root.bar.foreground } - InfoLabel { text: "Packet Loss" } + InfoLabel { text: I18n.tr("Packet Loss") } DetailValue { text: root.formatPacketLoss(root.internetPingPacketLoss) color: root.internetPingPacketLoss > 0 ? root.bar.urgent : root.bar.foreground } - InfoLabel { text: "Receiving" } + InfoLabel { text: I18n.tr("Receiving") } DetailValue { text: root.hasTransferStats ? root.formatRate(root.downloadRate) : "--" } - InfoLabel { text: "Sending" } + InfoLabel { text: I18n.tr("Sending") } DetailValue { text: root.hasTransferStats ? root.formatRate(root.uploadRate) : "--" } - InfoLabel { text: "Downloaded" } + InfoLabel { text: I18n.tr("Downloaded") } DetailValue { text: root.hasTransferStats ? root.formatBytes(parseFloat(root.info.rx_bytes || "0")) : "--" } - InfoLabel { text: "Uploaded" } + InfoLabel { text: I18n.tr("Uploaded") } DetailValue { text: root.hasTransferStats ? root.formatBytes(parseFloat(root.info.tx_bytes || "0")) : "--" } - InfoLabel { text: "IP Address" } + InfoLabel { text: I18n.tr("IP Address") } DetailValue { text: root.info.ip || "--" copyable: !!root.info.ip - tooltipText: "Copy IP" + tooltipText: I18n.tr("Copy IP") } - InfoLabel { text: "Gateway" } + InfoLabel { text: I18n.tr("Gateway") } DetailValue { text: root.info.gateway || "--" copyable: !!root.info.gateway - tooltipText: "Copy gateway" + tooltipText: I18n.tr("Copy gateway") } } } @@ -1482,7 +1486,7 @@ Panel { PanelSectionHeader { id: bandAutoLabel - text: "AUTOMATIC" + text: I18n.tr("AUTOMATIC") foreground: root.bar.foreground fontFamily: root.bar.fontFamily anchors.verticalCenter: parent.verticalCenter @@ -1515,8 +1519,8 @@ Panel { PanelToolTip { visible: bandAutoSwitch.containsMouse text: root.bandPinned - ? "Let Wi-Fi pick the band" - : "Stay on " + root.bandLabel(root.bandCurrent) + ? I18n.tr("Let Wi-Fi pick the band") + : I18n.tr("Stay on %1", root.bandLabel(root.bandCurrent)) fontFamily: root.bar.fontFamily } } @@ -1587,7 +1591,7 @@ Panel { spacing: Style.space(10) PanelSectionHeader { - text: "DNS PROVIDER" + text: I18n.tr("DNS PROVIDER") foreground: root.bar.foreground fontFamily: root.bar.fontFamily } @@ -1603,7 +1607,7 @@ Panel { DnsProviderPill { provider: "DHCP" index: 0 - tooltipText: "Use DNS from DHCP" + tooltipText: I18n.tr("Use DNS from DHCP") width: dnsRow.cellWidth onClicked: root.setDns(provider) } @@ -1611,7 +1615,7 @@ Panel { DnsProviderPill { provider: "Cloudflare" index: 1 - tooltipText: "Set DNS to Cloudflare" + tooltipText: I18n.tr("Set DNS to %1", "Cloudflare") width: dnsRow.cellWidth onClicked: root.setDns(provider) } @@ -1619,7 +1623,7 @@ Panel { DnsProviderPill { provider: "Google" index: 2 - tooltipText: "Set DNS to Google" + tooltipText: I18n.tr("Set DNS to %1", "Google") width: dnsRow.cellWidth onClicked: root.setDns(provider) } @@ -1627,7 +1631,7 @@ Panel { DnsProviderPill { provider: "Custom" index: 3 - tooltipText: "Set custom DNS servers" + tooltipText: I18n.tr("Set custom DNS servers") width: dnsRow.cellWidth onClicked: root.setDns(provider) } @@ -1643,7 +1647,7 @@ Panel { PanelSectionHeader { visible: root.wifiStationAvailable && root.scanning - text: "SCANNING WI-FI…" + text: I18n.tr("SCANNING WI-FI…") foreground: root.bar.foreground fontFamily: root.bar.fontFamily } @@ -1746,7 +1750,7 @@ Panel { required property string provider required property int index - text: provider + text: I18n.tr(provider) fontSize: Style.font.bodySmall foreground: root.bar.foreground fontFamily: root.bar.fontFamily @@ -1829,12 +1833,12 @@ Panel { readonly property string statusText: { if (!net) return "" if (isPasswordOpen) return "" - if (isBusy && root.actionKind === "connect") return "Connecting…" - if (isBusy && root.actionKind === "disconnect") return "Disconnecting…" - if (isBusy && root.actionKind === "forget") return "Forgetting…" - if (isFailed) return root.failureReason || "Failed" - if (isConnected && root.kind === "wifi" && root.hasCaptivePortal) return "Sign-in required" - if (isConnected) return "Connected" + if (isBusy && root.actionKind === "connect") return I18n.tr("Connecting…") + if (isBusy && root.actionKind === "disconnect") return I18n.tr("Disconnecting…") + if (isBusy && root.actionKind === "forget") return I18n.tr("Forgetting…") + if (isFailed) return root.failureReason || I18n.tr("Failed") + if (isConnected && root.kind === "wifi" && root.hasCaptivePortal) return I18n.tr("Sign-in required") + if (isConnected) return I18n.tr("Connected") return "" } @@ -1951,7 +1955,7 @@ Panel { PanelToolTip { visible: rightMouse.containsMouse || row.forgetFocused - text: "Forget network" + text: I18n.tr("Forget network") fontFamily: root.bar.fontFamily } } @@ -1967,7 +1971,7 @@ Panel { Text { textFormat: Text.PlainText - text: row.net ? (row.net.ssid || "Hidden") : "" + text: row.net ? (row.net.ssid || I18n.tr("Hidden")) : "" color: root.bar.foreground font.family: root.bar.fontFamily font.pixelSize: Style.font.body @@ -2027,7 +2031,7 @@ Panel { anchors.right: connectPwBtn.left anchors.top: parent.top anchors.rightMargin: Style.space(6) - placeholderText: "Identity (user@domain)" + placeholderText: I18n.tr("Identity (user@domain)") font.family: Style.font.family font.pixelSize: Style.font.body foreground: root.bar.foreground @@ -2053,7 +2057,7 @@ Panel { anchors.bottomMargin: Style.spacing.rowGap / 2 anchors.rightMargin: Style.space(6) password: true - placeholderText: "Passphrase" + placeholderText: I18n.tr("Passphrase") font.family: Style.font.family font.pixelSize: Style.font.body foreground: root.bar.foreground @@ -2086,7 +2090,7 @@ Panel { anchors.fill: parent horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter - text: row.isFailed ? "Wrong password" : "Connecting..." + text: row.isFailed ? I18n.tr("Wrong password") : I18n.tr("Connecting...") color: row.isFailed ? root.bar.urgent : root.bar.foreground font.family: root.bar.fontFamily font.pixelSize: Style.font.bodySmall @@ -2103,7 +2107,7 @@ Panel { anchors.verticalCenter: parent.verticalCenter enabled: row.net && pwField.text.length > 0 && (!row.isEnterprise || idField.text.length > 0) iconText: "󰄬" - tooltipText: "Connect" + tooltipText: I18n.tr("Connect") foreground: root.bar.foreground fontFamily: root.bar.fontFamily onClicked: row.submitCredentials() @@ -2114,7 +2118,7 @@ Panel { component DetailValue: InfoValue { property bool copyable: false - property string tooltipText: "Copy to clipboard" + property string tooltipText: I18n.tr("Copy to clipboard") Layout.fillWidth: true horizontalAlignment: Text.AlignRight diff --git a/shell/plugins/panels/power/Panel.qml b/shell/plugins/panels/power/Panel.qml index 80b8ff67..c20b5bce 100644 --- a/shell/plugins/panels/power/Panel.qml +++ b/shell/plugins/panels/power/Panel.qml @@ -54,7 +54,7 @@ Panel { function modeLabel() { var device = UPower.displayDevice - return Model.modeLabel(device, root.discharging, upowerStates()) + return I18n.tr(Model.modeLabel(device, root.discharging, upowerStates())) } function profileIcon(name) { @@ -94,26 +94,26 @@ Panel { // Cute agent-flavored phrases shown in the hero status line, rotated on a // timer so the panel feels alive when current is flowing (either direction). readonly property var chargingPhrases: [ - "Pumping power", - "Injecting electrons", - "Pouring juice", - "Amassing watts", - "Hoarding joules", - "Sucking volts", - "Topping reserves", - "Soaking amps", - "Inhaling kilowatts" + I18n.tr("Pumping power"), + I18n.tr("Injecting electrons"), + I18n.tr("Pouring juice"), + I18n.tr("Amassing watts"), + I18n.tr("Hoarding joules"), + I18n.tr("Sucking volts"), + I18n.tr("Topping reserves"), + I18n.tr("Soaking amps"), + I18n.tr("Inhaling kilowatts") ] readonly property var onBatteryPhrases: [ - "Slurping power", - "Spending joules", - "Draining watts", - "Burning electrons", - "Sipping juice", - "Spending coulombs", - "Bleeding amps", - "Guzzling volts", - "Munching reserves" + I18n.tr("Slurping power"), + I18n.tr("Spending joules"), + I18n.tr("Draining watts"), + I18n.tr("Burning electrons"), + I18n.tr("Sipping juice"), + I18n.tr("Spending coulombs"), + I18n.tr("Bleeding amps"), + I18n.tr("Guzzling volts"), + I18n.tr("Munching reserves") ] property int phraseIndex: 0 @@ -127,7 +127,7 @@ Panel { readonly property bool rotatingPhrases: activePhrases.length > 0 readonly property string heroStatusText: { - if (fullyCharged) return "Fully charged" + if (fullyCharged) return I18n.tr("Fully charged") if (rotatingPhrases) return activePhrases[phraseIndex % activePhrases.length] return modeLabel() } @@ -346,7 +346,7 @@ Panel { spacing: Style.space(2) Text { - text: "Battery" + text: I18n.tr("Battery") color: root.bar.foreground font.family: root.bar.fontFamily font.pixelSize: Style.font.title @@ -433,20 +433,20 @@ Panel { Column { width: (parent.width - parent.spacing) / 2 spacing: Style.spacing.labelGap - InfoPair { label: "Battery size"; value: root.batteryInfo.size || "" } - InfoPair { label: "Charge cycles"; value: root.batteryInfo.cycles || "—" } + InfoPair { label: I18n.tr("Battery size"); value: root.batteryInfo.size || "" } + InfoPair { label: I18n.tr("Charge cycles"); value: root.batteryInfo.cycles || "—" } } Column { width: (parent.width - parent.spacing) / 2 spacing: Style.spacing.labelGap InfoPair { - label: root.chargeThresholdActive ? "Charge limit" : (root.discharging ? "Time left" : "Time to full") + label: root.chargeThresholdActive ? I18n.tr("Charge limit") : (root.discharging ? I18n.tr("Time left") : I18n.tr("Time to full")) value: root.chargeThresholdActive ? (root.batteryInfo.threshold || "-") : (root.batteryFlowIdle ? "-" : (root.batteryInfo.time || "—")) } InfoPair { - label: root.chargeThresholdActive ? "Battery state" : (root.discharging ? "Discharging" : "Charging") - value: root.chargeThresholdActive ? "Holding" : (root.batteryFull ? "-" : (root.batteryInfo.rate || "")) + label: root.chargeThresholdActive ? I18n.tr("Battery state") : (root.discharging ? I18n.tr("Discharging") : I18n.tr("Charging")) + value: root.chargeThresholdActive ? I18n.tr("Holding") : (root.batteryFull ? "-" : (root.batteryInfo.rate || "")) } } } @@ -461,7 +461,7 @@ Panel { spacing: Style.space(10) PanelSectionHeader { - text: "POWER PROFILE" + text: I18n.tr("POWER PROFILE") foreground: root.bar.foreground fontFamily: root.bar.fontFamily } diff --git a/shell/plugins/panels/speedtest/Panel.qml b/shell/plugins/panels/speedtest/Panel.qml index 6805aa05..d41fbd54 100644 --- a/shell/plugins/panels/speedtest/Panel.qml +++ b/shell/plugins/panels/speedtest/Panel.qml @@ -149,7 +149,7 @@ Item { } if (!root.expectedStop && exitCode !== 0) { - root.error = root.stderrText || "Speed test failed" + root.error = root.stderrText || I18n.tr("Speed test failed") root.phase = "" root.running = false return @@ -177,7 +177,7 @@ Item { onStreamFinished: { var fields = String(text || "").trim().split("\t") if (fields[0] === "wifi") root.connectionName = fields[1] || "Wi-Fi" - else if (fields[0] === "ethernet") root.connectionName = "Ethernet" + else if (fields[0] === "ethernet") root.connectionName = I18n.tr("Ethernet") } } } @@ -186,9 +186,9 @@ Item { fontFamily: Style.font.family layerNamespace: "omarchy-network-speedtest" title: root.connectionName - leftLabel: "DOWNLOAD" - rightLabel: "UPLOAD" - runAgainTooltip: "Measure again via fast.com" + leftLabel: I18n.tr("DOWNLOAD") + rightLabel: I18n.tr("UPLOAD") + runAgainTooltip: I18n.tr("Measure again via fast.com") running: root.running leftValue: root.downloadValue rightValue: root.uploadValue diff --git a/shell/plugins/panels/tailscale/Panel.qml b/shell/plugins/panels/tailscale/Panel.qml index 8d0a84cb..99941acd 100644 --- a/shell/plugins/panels/tailscale/Panel.qml +++ b/shell/plugins/panels/tailscale/Panel.qml @@ -24,16 +24,16 @@ Panel { property string mullvadQuery: "" property int phraseIndex: 0 readonly property var activePhrases: [ - "Encrypting connections", - "Sending secrets", - "Guarding wires", - "Braiding packets", - "Polishing tunnels", - "Hiding routes", - "Sealing ports", - "Sorting tailnets", - "Shuffling keys", - "Watching machines" + I18n.tr("Encrypting connections"), + I18n.tr("Sending secrets"), + I18n.tr("Guarding wires"), + I18n.tr("Braiding packets"), + I18n.tr("Polishing tunnels"), + I18n.tr("Hiding routes"), + I18n.tr("Sealing ports"), + I18n.tr("Sorting tailnets"), + I18n.tr("Shuffling keys"), + I18n.tr("Watching machines") ] readonly property string heroPhraseText: activePhrases[phraseIndex % activePhrases.length] @@ -52,7 +52,7 @@ Panel { // "header" stays navigable, but an absent CLI leaves nothing to highlight. readonly property bool headerHasCursor: cursorActive && focusSection === "header" && tailscale.installed readonly property color iconColor: tailscale.active ? foreground : dim - readonly property string toggleHint: tailscale.active ? "Turn Tailscale off" : (tailscale.needsLogin ? "Authorize this device" : "Turn Tailscale on") + readonly property string toggleHint: tailscale.active ? I18n.tr("Turn Tailscale off") : (tailscale.needsLogin ? I18n.tr("Authorize this device") : I18n.tr("Turn Tailscale on")) readonly property color barIconColor: tailscale.active ? barForeground : Qt.darker(barForeground, 1.55) readonly property color hoverFill: bar ? Style.hoverFillFor(bar.foreground, Color.accent) : "transparent" readonly property color selectedFill: bar ? Style.selectedFillFor(bar.foreground, Color.accent) : "transparent" @@ -76,7 +76,7 @@ Panel { var nodes = [] for (var i = 0; i < tailscale.tailnetExitNodes.length; i++) nodes.push(tailscale.tailnetExitNodes[i]) for (var j = 0; j < recentMullvadExitNodes.length; j++) nodes.push(recentMullvadExitNodes[j]) - if (tailscale.mullvadRegions.length > 0) nodes.push({ id: "mullvad:add", AddMullvad: true, DisplayName: "Choose Mullvad region" }) + if (tailscale.mullvadRegions.length > 0) nodes.push({ id: "mullvad:add", AddMullvad: true, DisplayName: I18n.tr("Choose Mullvad region") }) return nodes } @@ -132,7 +132,7 @@ Panel { } function mullvadRegionTitle(peer) { - if (!peer) return "Unknown" + if (!peer) return I18n.tr("Unknown") var city = String(peer.City || "").trim() var country = String(peer.Country || "").trim() if (city === "" || city === "Any") return country || String(peer.DisplayName || "Unknown") @@ -458,7 +458,7 @@ Panel { id: hero width: parent.width title: tailscale.installed ? (tailscale.selfName || "Tailscale") : "Tailscale" - meta: tailscale.active ? root.heroPhraseText : "Tailscale is disconnected" + meta: tailscale.active ? root.heroPhraseText : I18n.tr("Tailscale is disconnected") foreground: root.foreground fontFamily: root.fontFamily iconOpacity: tailscale.active ? 1.0 : 0.5 @@ -501,7 +501,7 @@ Panel { textFormat: Text.PlainText visible: tailscale.actionStatus !== "" || tailscale.lastError !== "" width: parent.width - text: tailscale.actionStatus !== "" ? tailscale.actionStatus : tailscale.lastError + text: I18n.tr(tailscale.actionStatus !== "" ? tailscale.actionStatus : tailscale.lastError) color: tailscale.lastError !== "" && tailscale.actionStatus === "" ? root.urgent : root.dim font.family: root.fontFamily font.pixelSize: Style.font.bodySmall @@ -520,7 +520,7 @@ Panel { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Style.space(12) - text: "Tailscale CLI is not installed or not on PATH." + text: I18n.tr("Tailscale CLI is not installed or not on PATH.") color: root.dim font.family: root.fontFamily font.pixelSize: Style.font.body @@ -539,7 +539,7 @@ Panel { spacing: Style.space(10) PanelSectionHeader { - text: "CONNECTIONS" + text: I18n.tr("CONNECTIONS") foreground: root.foreground fontFamily: root.fontFamily } @@ -572,7 +572,7 @@ Panel { spacing: Style.space(10) PanelSectionHeader { - text: "EXIT NODES" + text: I18n.tr("EXIT NODES") foreground: root.foreground fontFamily: root.fontFamily } @@ -602,7 +602,7 @@ Panel { id: mullvadSearch width: parent.width foreground: root.foreground - placeholderText: "Search regions" + placeholderText: I18n.tr("Search regions") text: root.mullvadQuery onTextChanged: { root.mullvadQuery = text @@ -638,7 +638,7 @@ Panel { Text { visible: root.filteredMullvadRegions.length === 0 width: parent.width - text: "No Mullvad regions found." + text: I18n.tr("No Mullvad regions found.") color: root.dim font.family: root.fontFamily font.pixelSize: Style.font.bodySmall @@ -676,7 +676,7 @@ Panel { spacing: Style.space(10) PanelSectionHeader { - text: "MACHINES" + text: I18n.tr("MACHINES") foreground: root.foreground fontFamily: root.fontFamily } @@ -684,7 +684,7 @@ Panel { Text { visible: tailscale.installed && tailscale.active && tailscale.peers.length === 0 width: parent.width - text: "No machines found on this tailnet." + text: I18n.tr("No machines found on this tailnet.") color: root.dim font.family: root.fontFamily font.pixelSize: Style.font.body @@ -777,7 +777,7 @@ Panel { Text { Layout.fillWidth: true - text: "Authorize Tailscale operator" + text: I18n.tr("Authorize Tailscale operator") color: root.foreground font.family: root.fontFamily font.pixelSize: Style.font.body @@ -786,7 +786,7 @@ Panel { Text { Layout.fillWidth: true - text: "Allow this user to operate this Tailscale profile" + text: I18n.tr("Allow this user to operate this Tailscale profile") color: root.dim font.family: root.fontFamily font.pixelSize: Style.font.caption @@ -803,7 +803,7 @@ Panel { property int rowIndex: 0 readonly property bool selectedAccount: account && account.selected === true readonly property bool switchingAccount: account && tailscale.switchingAccountId === String(account.id || "") - readonly property string accountText: account ? tailscale.accountLabel(account) : "Account" + readonly property string accountText: account ? tailscale.accountLabel(account) : I18n.tr("Account") hasCursor: root.cursorActive && root.focusSection === "accounts" && root.accountIndex === rowIndex current: selectedAccount @@ -867,7 +867,7 @@ Panel { id: peerRow property var peer: null property int rowIndex: 0 - readonly property string peerName: peer ? String(peer.DisplayName || peer.HostName || "Unknown") : "Unknown" + readonly property string peerName: peer ? String(peer.DisplayName || peer.HostName || I18n.tr("Unknown")) : I18n.tr("Unknown") readonly property string peerIp: peer && peer.TailscaleIPs && peer.TailscaleIPs.length > 0 ? String(peer.TailscaleIPs[0]) : "" readonly property string peerIpv6: { if (!peer || !peer.TailscaleIPv6 || peer.TailscaleIPv6.length === 0) return "" @@ -978,7 +978,7 @@ Panel { id: sendButton visible: tailscale.canSendFiles(peerRow.peer) iconText: "󰒊" - tooltipText: "Send files" + tooltipText: I18n.tr("Send files") foreground: root.foreground fontFamily: root.fontFamily Layout.alignment: Qt.AlignVCenter @@ -1118,8 +1118,8 @@ Panel { readonly property bool addMullvad: peer && peer.AddMullvad === true readonly property bool activeExitNode: peer && peer.ExitNode === true readonly property bool settingExitNode: peer && tailscale.settingExitNodeId === String(peer.id || "") - readonly property string peerName: peer ? String(peer.DisplayName || peer.HostName || "Unknown") : "Unknown" - readonly property string actionTooltip: addMullvad ? "" : (activeExitNode ? "Disconnect" : "Connect") + readonly property string peerName: peer ? String(peer.DisplayName || peer.HostName || I18n.tr("Unknown")) : I18n.tr("Unknown") + readonly property string actionTooltip: addMullvad ? "" : (activeExitNode ? I18n.tr("Disconnect") : I18n.tr("Connect")) hasCursor: root.cursorActive && root.focusSection === "exitNodes" && root.exitNodeIndex === rowIndex current: activeExitNode || settingExitNode || (addMullvad && root.mullvadPickerOpen) @@ -1199,7 +1199,7 @@ Panel { readonly property bool activeExitNode: peer && peer.ExitNode === true readonly property bool settingExitNode: peer && tailscale.settingExitNodeId === String(peer.id || "") readonly property bool selectedRegion: root.mullvadPickerOpen && root.mullvadRegionIndex === rowIndex - readonly property string actionTooltip: activeExitNode ? "Disconnect" : "Connect" + readonly property string actionTooltip: activeExitNode ? I18n.tr("Disconnect") : I18n.tr("Connect") foreground: root.foreground fill: root.hoverFill diff --git a/shell/plugins/panels/weather/Panel.qml b/shell/plugins/panels/weather/Panel.qml index e9fa5c31..cc75ab8c 100644 --- a/shell/plugins/panels/weather/Panel.qml +++ b/shell/plugins/panels/weather/Panel.qml @@ -616,7 +616,7 @@ Panel { id: locationField width: Style.space(190) enabled: !root.savingLocation - placeholderText: "Search city" + placeholderText: I18n.tr("Search city") foreground: root.bar.foreground font.family: root.bar.fontFamily @@ -683,7 +683,7 @@ Panel { Column { spacing: Style.space(5) Text { - text: "FEELS" + text: I18n.tr("FEELS") color: Qt.darker(root.bar.foreground, 1.5) font.family: root.bar.fontFamily font.pixelSize: Style.font.bodySmall @@ -701,7 +701,7 @@ Panel { Column { spacing: Style.space(5) Text { - text: "WIND" + text: I18n.tr("WIND") color: Qt.darker(root.bar.foreground, 1.5) font.family: root.bar.fontFamily font.pixelSize: Style.font.bodySmall @@ -719,7 +719,7 @@ Panel { Column { spacing: Style.space(5) Text { - text: "HUMID" + text: I18n.tr("HUMID") color: Qt.darker(root.bar.foreground, 1.5) font.family: root.bar.fontFamily font.pixelSize: Style.font.bodySmall @@ -792,7 +792,7 @@ Panel { Text { visible: !root.current - text: "Fetching forecast…" + text: I18n.tr("Fetching forecast…") color: Qt.darker(root.bar.foreground, 1.5) font.family: root.bar.fontFamily font.pixelSize: Style.font.bodySmall diff --git a/shell/plugins/panels/wifiqr/Panel.qml b/shell/plugins/panels/wifiqr/Panel.qml index 1fdf4dec..af05a1f7 100644 --- a/shell/plugins/panels/wifiqr/Panel.qml +++ b/shell/plugins/panels/wifiqr/Panel.qml @@ -183,7 +183,7 @@ Item { if (exitCode !== 0 || root.qrSize === 0) { root.qrSize = 0 root.qrRows = [] - if (root.error === "") root.error = "Could not generate the Wi-Fi QR code" + if (root.error === "") root.error = I18n.tr("Could not generate the Wi-Fi QR code") } } } @@ -206,7 +206,7 @@ Item { if (root.pwExpectedStop) return if (!root.opened) return if (exitCode === 0 && root.password !== "") root.passwordVisible = true - else root.passwordError = "Could not read the Wi-Fi password" + else root.passwordError = I18n.tr("Could not read the Wi-Fi password") } } @@ -305,7 +305,7 @@ Item { Text { visible: root.loading - text: "Generating QR code…" + text: I18n.tr("Generating QR code…") color: root.onScrimDim font.family: root.fontFamily font.pixelSize: Style.font.bodySmall @@ -328,7 +328,7 @@ Item { Text { visible: root.showingQr - text: "Scan to join this network" + text: I18n.tr("Scan to join this network") color: root.onScrimDim font.family: root.fontFamily font.pixelSize: Style.font.bodySmall @@ -341,7 +341,7 @@ Item { visible: root.showingQr && root.secured text: root.passwordError !== "" ? root.passwordError : root.passwordVisible ? root.password - : "Show password" + : I18n.tr("Show password") color: root.passwordError !== "" ? root.onScrimUrgent : root.onScrim opacity: root.passwordVisible || root.passwordError !== "" ? 1 : 0.6 font.family: root.fontFamily diff --git a/shell/plugins/polkit/PolkitAgent.qml b/shell/plugins/polkit/PolkitAgent.qml index 98d6a0ac..ce64b01b 100644 --- a/shell/plugins/polkit/PolkitAgent.qml +++ b/shell/plugins/polkit/PolkitAgent.qml @@ -78,7 +78,7 @@ Item { var flow = polkitAgent.flow if (!flow) return - currentMessage = String(flow.message || "Authentication is needed...") + currentMessage = String(flow.message || I18n.tr("Authentication is needed...")) currentPrompt = String(flow.inputPrompt || "") currentSupplementary = String(flow.supplementaryMessage || "") responseRequired = !!flow.isResponseRequired @@ -336,7 +336,7 @@ Item { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - text: root.errorFlash ? "Wrong" : (root.submitted ? "Checking..." : "Enter password") + text: root.errorFlash ? I18n.tr("Wrong") : (root.submitted ? I18n.tr("Checking...") : I18n.tr("Enter password")) color: root.errorFlash ? Color.polkit.textError : root.foreground opacity: root.errorFlash ? 1 : 0.36 font.family: root.fontFamily diff --git a/shell/plugins/reminders/ReminderFlow.qml b/shell/plugins/reminders/ReminderFlow.qml index efdff65c..031dce48 100644 --- a/shell/plugins/reminders/ReminderFlow.qml +++ b/shell/plugins/reminders/ReminderFlow.qml @@ -28,7 +28,7 @@ Item { property int headerHeight: Math.max(Style.space(34), Style.font.title + Style.spacing.controlPaddingY * 2) property int cardWidth: Math.min(Style.space(300), panel.width - Style.gapsOut * 2) property int cardHeight: Math.min(contentMargin * 2 + headerHeight, panel.height - Style.gapsOut * 2) - readonly property string promptText: root.step === "message" ? "Reminder message" : "Remind in minutes" + readonly property string promptText: root.step === "message" ? I18n.tr("Reminder message") : I18n.tr("Remind in minutes") function open(payloadJson) { var payload = ({}) diff --git a/shell/plugins/services/media/Service.qml b/shell/plugins/services/media/Service.qml index 0653627c..f3fe7ea3 100644 --- a/shell/plugins/services/media/Service.qml +++ b/shell/plugins/services/media/Service.qml @@ -411,7 +411,7 @@ Item { handled = true } } else if (action === "playPause") { - actionLabel = player && player.isPlaying ? "Pause" : "Play" + actionLabel = player && player.isPlaying ? I18n.tr("Pause") : I18n.tr("Play") iconName = player && player.isPlaying ? "media-pause" : "media-play" if (player && player.isPlaying && player.canPause) { player.pause() diff --git a/test/shell.d/clock-test.sh b/test/shell.d/clock-test.sh index d2bde8a1..6261ff40 100755 --- a/test/shell.d/clock-test.sh +++ b/test/shell.d/clock-test.sh @@ -225,7 +225,7 @@ assert(/readonly property int lifeExpectancy: Model\.parseLifeExpectancy\(settin assert(/id: expectancyField/.test(panelSource) && /id: bornField/.test(panelSource), 'calendar offers both inputs') assert(/visible: root\.editingLife\s*\n\s*anchors\.horizontalCenter: parent\.horizontalCenter/.test(panelSource), 'calendar centers the inputs over the bar they replace') assert(/visible: root\.birthYear > 0/.test(panelSource), 'calendar hides the life bar until a birth year is known') -assert(/text: "LIFE"/.test(panelSource) && /root\.lifeDone/.test(panelSource), 'calendar shows the life bar') +assert(/text: I18n\.tr\("LIFE"\)/.test(panelSource) && /root\.lifeDone/.test(panelSource), 'calendar shows the life bar') assert(/text: "Memento Mori"/.test(panelSource), 'calendar names the life bar on hover') assert(/onDoubleTapped: root\.clearLife\(\)/.test(panelSource), 'calendar puts the life bar away when it is double-tapped') assert(/persistSettings\(\{ birthYear: 0 \}\)/.test(panelSource), 'calendar clears the birth year to hide the life bar') diff --git a/test/shell.d/menu-locale-test.sh b/test/shell.d/menu-locale-test.sh index a5cbc653..935c3a67 100755 --- a/test/shell.d/menu-locale-test.sh +++ b/test/shell.d/menu-locale-test.sh @@ -39,6 +39,7 @@ console.log('ok - Chinese labels replace only the label, and user extensions sti JS grep -qF 'root.localeMenuItems.concat(root.userMenuItems)' "$ROOT/shell/plugins/menu/Menu.qml" && - grep -qF 'startsWith("zh_CN") ? omarchyPath + "/default/omarchy/omarchy-menu.zh-cn.jsonc"' "$ROOT/shell/plugins/menu/Menu.qml" || + grep -qF 'I18n.chinese ? omarchyPath + "/default/omarchy/omarchy-menu.zh-cn.jsonc"' "$ROOT/shell/plugins/menu/Menu.qml" && + grep -qF 'var chinese = Qt.locale().name.indexOf("zh_CN") === 0' "$ROOT/shell/Commons/Tr.js" || fail "the menu loads the Chinese overlay under zh_CN, beneath the user extension" pass "the menu loads the Chinese overlay under zh_CN, beneath the user extension" diff --git a/test/shell.d/notifications-test.sh b/test/shell.d/notifications-test.sh index 48414ed4..a3484514 100644 --- a/test/shell.d/notifications-test.sh +++ b/test/shell.d/notifications-test.sh @@ -137,7 +137,7 @@ assertEqual( // belongs in the logic module, where the strip runs after it. const cardQml = fs.readFileSync(path.join(root, 'shell/plugins/notifications/components/NotificationCard.qml'), 'utf8') assert( - /readonly property string styledBody: NotificationLogic\.styledBody\(body, app, appIcon\)/.test(cardQml), + /readonly property string styledBody: NotificationLogic\.styledBody\(I18n\.tr\(body\), app, appIcon\)/.test(cardQml), 'the notification card renders the body that was stripped after the newline rewrite' ) assert( diff --git a/test/shell.d/shell-i18n-test.sh b/test/shell.d/shell-i18n-test.sh new file mode 100644 index 00000000..700600a6 --- /dev/null +++ b/test/shell.d/shell-i18n-test.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +run_node_test <<'JS' +const fs = require('fs') +const vm = require('vm') + +const source = fs.readFileSync(path.join(root, 'shell/Commons/I18nZh.js'), 'utf8').replace('.pragma library', '') +const context = {} +vm.runInNewContext(source + '\nthis.table = table', context) +const table = context.table + +const files = [] +const walk = dir => { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name) + if (entry.isDirectory()) walk(full) + else if (/\.(qml|js)$/.test(entry.name)) files.push(full) + } +} +walk(path.join(root, 'shell')) + +const missing = [] +let calls = 0 +for (const file of files) { + const text = fs.readFileSync(file, 'utf8') + for (const match of text.matchAll(/I18n\.tr\(\s*"((?:[^"\\]|\\.)*)"/g)) { + calls++ + const key = JSON.parse('"' + match[1] + '"') + if (!(key in table)) missing.push(`${path.relative(root, file)}: ${key}`) + } +} +assert(calls > 300, 'the shell routes its text through I18n.tr', `found ${calls} calls`) +assert(missing.length === 0, 'every I18n.tr text has a Chinese entry', missing.join('\n')) + +const placeholders = s => (s.match(/%\d/g) || []).sort().join(',') +const mismatched = Object.keys(table).filter(key => placeholders(key) !== placeholders(table[key])) +assert(mismatched.length === 0, 'every Chinese entry keeps its English placeholders', mismatched.join('\n')) +JS