diff --git a/AGENTS.md b/AGENTS.md index a36cbe84..9dff669f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -103,7 +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. +- Shell text goes through `I18n.tr("English %1", arg)` from `qs.Commons`, with its Chinese in `shell/Commons/I18nZh.js` keyed by the English. The shell translates fixed notification and OSD text by exact match; a sender whose message carries a name or number words it with `omarchy-text "English %1" "$arg"` (Python: `text()`), which reads the same table. `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/bin/omarchy-agent-account-add b/bin/omarchy-agent-account-add index 4885eaed..20b52229 100755 --- a/bin/omarchy-agent-account-add +++ b/bin/omarchy-agent-account-add @@ -85,7 +85,7 @@ finish() { fail() { if [[ $events == "true" ]]; then printf '@@omarchy error %s\n' "$1" - omarchy-notification-send -g 󰚩 -u critical "Couldn't add the $name account" "$1" + omarchy-notification-send -g 󰚩 -u critical "$(omarchy-text "Couldn't add the %1 account" "$name")" "$1" else echo "$1" >&2 fi @@ -113,9 +113,9 @@ interruptible() { } if omarchy-cmd-missing "$provider"; then - say "Installing the $name CLI…" + say "$(omarchy-text "Installing the %1 CLI…" "$name")" if [[ $events == "true" ]]; then - mise use -g "$package" >/dev/null 2>&1 || fail "Installing the $name CLI didn't finish." + mise use -g "$package" >/dev/null 2>&1 || fail "$(omarchy-text "Installing the %1 CLI didn't finish." "$name")" else mise use -g "$package" || exit 1 fi @@ -150,26 +150,26 @@ if [[ -n $reauth ]]; then else account_home=$(omarchy-agent-account-state homes "$provider" | awk -F'\t' -v id="$reauth" '$1 == id { print $2 }') fi - [[ -n $account_home ]] || fail "No $name account named $reauth." + [[ -n $account_home ]] || fail "$(omarchy-text "No %1 account named %2." "$name" "$reauth")" if [[ $account_home == "$HOME/.$provider" ]]; then - say "Sign in to $name again in the browser window that opens." - login || fail "The $name sign-in didn't finish." + say "$(omarchy-text "Sign in to %1 again in the browser window that opens." "$name")" + login || fail "$(omarchy-text "The %1 sign-in didn't finish." "$name")" else - say "Sign in again as this account in the private window that opens." + say "$(omarchy-text "Sign in again as this account in the private window that opens.")" private_browser - interruptible env BROWSER="$browser" "$home_env=$account_home" "${login_command[@]}" || fail "The $name sign-in didn't finish." + interruptible env BROWSER="$browser" "$home_env=$account_home" "${login_command[@]}" || fail "$(omarchy-text "The %1 sign-in didn't finish." "$name")" fi omarchy-agent-usage-update --force "$provider" >/dev/null 2>&1 & - finish "Signed in to $name again." + finish "$(omarchy-text "Signed in to %1 again." "$name")" exit 0 fi # The first subscription signs in where the CLI always looks, through your # normal browser: nothing is signed in there yet that it could be mistaken for. if ! primary_signed_in "$provider"; then - say "Sign in to $name in the browser window that opens." + say "$(omarchy-text "Sign in to %1 in the browser window that opens." "$name")" if login && primary_signed_in "$provider"; then # The first agent signed in on a machine with none chosen becomes the # default, so `omarchy agent` and the panel's starter prompts have one to @@ -179,9 +179,9 @@ if ! primary_signed_in "$provider"; then printf '%s\n' "$provider" >"$HOME/.config/omarchy/defaults/agent" fi omarchy-agent-usage-update --force "$provider" >/dev/null 2>&1 & - finish "Signed in to $name." + finish "$(omarchy-text "Signed in to %1." "$name")" else - fail "The $name sign-in didn't finish." + fail "$(omarchy-text "The %1 sign-in didn't finish." "$name")" fi else if [[ -z $label && $events == "false" && -t 0 ]]; then @@ -189,7 +189,7 @@ else fi if [[ $events == "true" ]]; then - say "Sign in as the account you're adding in the private window that opens." + say "$(omarchy-text "Sign in as the account you're adding in the private window that opens.")" else cat </dev/null 2>&1 & if [[ $events == "true" ]]; then - finish "Added $added." + finish "$(omarchy-text "Added %1." "$added")" else - finish "Added $added." + finish "$(omarchy-text "Added %1." "$added")" echo "Switch to it with: omarchy agent account use $provider $(jq -r .id <<<"$account")" fi else diff --git a/bin/omarchy-agent-account-state b/bin/omarchy-agent-account-state index 0578b6cc..a5490789 100755 --- a/bin/omarchy-agent-account-state +++ b/bin/omarchy-agent-account-state @@ -73,6 +73,11 @@ class AccountError(Exception): pass +def text(english: str, *args: object) -> str: + """English with %1.. filled, in the session's language, from the shell's table.""" + return subprocess.run(["omarchy-text", english, *map(str, args)], capture_output=True, text=True, check=True).stdout.rstrip("\n") + + # ------------------------------------------------------------------- paths @@ -335,12 +340,12 @@ def begin(provider: str) -> Path: def register(provider: str, label: str, pending: Path) -> dict[str, Any]: pending = pending.resolve() if pending.parent != (accounts_root() / provider / ".pending").resolve(): - raise AccountError(f"{pending} is not a pending {PROVIDERS[provider]['name']} login") + raise AccountError(text("%1 is not a pending %2 login", pending, PROVIDERS[provider]['name'])) found = identity(provider, pending) if not found["accountId"]: shutil.rmtree(pending, ignore_errors=True) - raise AccountError("The login didn't finish, so no account was added.") + raise AccountError(text("The login didn't finish, so no account was added.")) with Locked(provider) as registry: # Each home is asked who it is now, not trusted from when it was added: a @@ -350,10 +355,10 @@ def register(provider: str, label: str, pending: Path) -> dict[str, Any]: for account in registry["accounts"]: if account.get("accountId") == found["accountId"]: shutil.rmtree(pending, ignore_errors=True) - raise AccountError( - f"That's {account['label']} ({found['email'] or 'already added'}). " - "Try again, and sign in as the other account in the private window." - ) + raise AccountError(text( + "That's %1 (%2). Try again, and sign in as the other account in the private window.", + account["label"], found["email"] or text("already added"), + )) label = label.strip() or found["email"].split("@")[0] or "Account" account_id = unique_id(provider, registry, label) @@ -395,11 +400,11 @@ def unique_id(provider: str, registry: dict[str, Any], label: str, keep: dict[st def rename(provider: str, target: str, label: str) -> dict[str, Any]: label = label.strip() if not label: - raise AccountError("Give the account a name") + raise AccountError(text("Give the account a name")) with Locked(provider) as registry: account = find(registry, target) if not account: - raise AccountError(f"No {PROVIDERS[provider]['name']} account named {target}") + raise AccountError(text("No %1 account named %2", PROVIDERS[provider]['name'], target)) new_id = unique_id(provider, registry, label, keep=account) if not account.get("primary") and not account.get("home"): account["home"] = str(account_home(provider, account)) @@ -428,7 +433,7 @@ def use(provider: str, target: str) -> tuple[dict[str, Any], dict[str, Any]]: target = ids[(ids.index(previous["id"]) + 1) % len(ids)] account = find(registry, target) if not account: - raise AccountError(f"No {PROVIDERS[provider]['name']} account named {target}") + raise AccountError(text("No %1 account named %2", PROVIDERS[provider]['name'], target)) registry["active"] = account["id"] registry["alert"] = "" if not account.get("primary"): @@ -456,12 +461,12 @@ def remove(provider: str, target: str) -> dict[str, Any]: with Locked(provider) as registry: account = find(registry, target) if not account: - raise AccountError(f"No {PROVIDERS[provider]['name']} account named {target}") + raise AccountError(text("No %1 account named %2", PROVIDERS[provider]['name'], target)) if account.get("primary"): - raise AccountError(f"{account['label']} is the login in {primary_home(provider)}; it can't be removed here.") + raise AccountError(text("%1 is the login in %2; it can't be removed here.", account["label"], primary_home(provider))) home = account_home(provider, account) if home_in_use(provider, home): - raise AccountError(f"A running {PROVIDERS[provider]['name']} session uses {account['label']}. Quit it first, then remove the account.") + raise AccountError(text("A running %1 session uses %2. Quit it first, then remove the account.", PROVIDERS[provider]['name'], account["label"])) # Only the account's own directory goes. Its links point into the primary # home, and rmtree removes a link without following it. if home.resolve().parent == (accounts_root() / provider).resolve(): @@ -474,14 +479,14 @@ def remove(provider: str, target: str) -> dict[str, Any]: def set_mode(provider: str, mode: str, threshold: str | None) -> dict[str, Any]: if mode not in SWITCH_MODES: - raise AccountError("Switch mode is manual or auto") + raise AccountError(text("Switch mode is manual or auto")) with Locked(provider) as registry: registry["switch"] = mode if threshold is not None: try: registry["threshold"] = max(50, min(100, int(threshold))) except ValueError: - raise AccountError("The threshold is a percentage between 50 and 100") + raise AccountError(text("The threshold is a percentage between 50 and 100")) registry["alert"] = "" return dict(registry) @@ -556,10 +561,10 @@ def duration(delta: dt.timedelta) -> str: hours, minutes = divmod(minutes, 60) days, hours = divmod(hours, 24) if days: - return f"{days}d {hours}h" + return text("%1d %2h", days, hours) if hours: - return f"{hours}h {minutes}m" - return f"{minutes}m" + return text("%1h %2m", hours, minutes) + return text("%1m", minutes) # The whole switching policy, with no I/O: given the registry and the per @@ -659,21 +664,23 @@ def autoswitch(provider: str, now: dt.datetime | None = None) -> bool: threshold = registry["threshold"] if decision["action"] == "exhausted": soonest = decision["soonest"] - when = f" {soonest[1]['label']} resets in {duration(soonest[0] - now)}." if soonest else "" - notify(f"All {name} accounts are over {threshold}%", f"Staying on {source['label']}.{when}") + staying = text("Staying on %1.", source["label"]) + if soonest: + staying = text("Staying on %1. %2 resets in %3.", source["label"], soonest[1]["label"], duration(soonest[0] - now)) + notify(text("All %1 accounts are over %2%", name, threshold), staying) return False target = decision["to"] - headline = f"{source['label']} is at {round(decision['fromPeak'])}% of a {name} limit" + headline = text("%1 is at %2% of a %3 limit", source["label"], round(decision["fromPeak"]), name) if decision["action"] == "switch": registry["active"] = target["id"] if not target.get("primary"): link_shared(provider, account_home(provider, target)) - notify(headline, f"New {name} sessions now use {target['label']} ({round(decision['toPeak'])}%). Running sessions stay on {source['label']}.") + notify(headline, text("New %1 sessions now use %2 (%3%). Running sessions stay on %4.", name, target["label"], round(decision["toPeak"]), source["label"])) return True notify( headline, - f"{target['label']} is at {round(decision['toPeak'])}%. Click to switch new sessions to it.", + text("%1 is at %2%. Click to switch new sessions to it.", target["label"], round(decision["toPeak"])), ["omarchy-agent-account-use", provider, target["id"]], ) return False diff --git a/bin/omarchy-agent-account-use b/bin/omarchy-agent-account-use index 3103e953..d50041c4 100755 --- a/bin/omarchy-agent-account-use +++ b/bin/omarchy-agent-account-use @@ -31,9 +31,9 @@ active=$(jq -r '.active.label + (if (.active.plan // "") != "" then " (" + .acti if [[ $(jq -r .previous.id <<<"$result") == "$(jq -r .active.id <<<"$result")" ]]; then echo "New $name sessions already use $active." else - message="Running sessions stay on $previous." + message=$(omarchy-text "Running sessions stay on %1." "$previous") echo "New $name sessions now use $active. $message" - omarchy-notification-send -g 󰚩 "New $name sessions now use $active" "$message" + omarchy-notification-send -g 󰚩 "$(omarchy-text "New %1 sessions now use %2" "$name" "$active")" "$message" # The usage record's own limits describe the active account. omarchy-agent-usage-update --limits-only "$1" >/dev/null 2>&1 & fi diff --git a/bin/omarchy-agent-usage-claude b/bin/omarchy-agent-usage-claude index e615c583..fcef1b4c 100755 --- a/bin/omarchy-agent-usage-claude +++ b/bin/omarchy-agent-usage-claude @@ -22,6 +22,7 @@ import json import os import re import sqlite3 +import subprocess import sys import tempfile import time @@ -32,6 +33,11 @@ from typing import Any AGENT_ID = "claude" AGENT_NAME = "Claude Code" +def text(english, *args): + """English with %1.. filled, in the session's language, from the shell's table.""" + return subprocess.run(["omarchy-text", english, *map(str, args)], capture_output=True, text=True, check=True).stdout.rstrip("\n") + + AUTH_HELP = "Run `claude auth login` to restore authoritative usage." USAGE_ENDPOINT = "https://api.anthropic.com/api/oauth/usage" PROBE_MIN_INTERVAL_SECONDS = 15 @@ -989,11 +995,12 @@ def probe_limits(access_token: str) -> dict[str, Any]: except urllib.error.HTTPError as error: retry_after = error.headers.get("retry-after", "") if error.headers else "" if error.code == 429: - help_text = "Anthropic's usage endpoint is rate limiting checks right now" + ( - f" (retry after {retry_after}s)" if retry_after else "" - ) + ". Local Claude Code stats are still shown." + if retry_after: + help_text = text("Anthropic's usage endpoint is rate limiting checks right now (retry after %1s). Local Claude Code stats are still shown.", retry_after) + else: + help_text = text("Anthropic's usage endpoint is rate limiting checks right now. Local Claude Code stats are still shown.") else: - help_text = f"Anthropic's usage endpoint returned status {error.code}. Local Claude Code stats are still shown." + help_text = text("Anthropic's usage endpoint returned status %1. Local Claude Code stats are still shown.", error.code) return {"ok": False, "helpText": help_text} except Exception: # A transport failure reached no server at all — no route, no DNS. Any diff --git a/bin/omarchy-battery-low b/bin/omarchy-battery-low index d107d4f8..e45eac84 100755 --- a/bin/omarchy-battery-low +++ b/bin/omarchy-battery-low @@ -13,5 +13,5 @@ fi level=$1 -omarchy-notification-send -g 󱐋 -u critical "Time to recharge!" "Battery is down to ${level}%" -i battery-caution -t 30000 +omarchy-notification-send -g 󱐋 -u critical "Time to recharge!" "$(omarchy-text "Battery is down to %1%" "$level")" -i battery-caution -t 30000 omarchy-hook battery-low "$level" diff --git a/bin/omarchy-battery-status b/bin/omarchy-battery-status index cc0a6024..f8e17d1c 100755 --- a/bin/omarchy-battery-status +++ b/bin/omarchy-battery-status @@ -129,9 +129,9 @@ if [[ $charge_holding == "true" ]]; then threshold_label="${threshold_end}%" fi - echo "Battery ${percentage}% · Holding at ${threshold_label} · ${power_rate}W / ${capacity}Wh" + omarchy-text "Battery %1% · Holding at %2 · %3W / %4Wh" "$percentage" "$threshold_label" "$power_rate" "$capacity" elif [[ $state == "charging" ]]; then - echo "Battery ${percentage}% · ${time_remaining} to full ·  ${power_rate}W / ${capacity}Wh" + omarchy-text "Battery %1% · %2 to full ·  %3W / %4Wh" "$percentage" "$time_remaining" "$power_rate" "$capacity" else - echo "Battery ${percentage}% · ${time_remaining} left ·  ${power_rate}W / ${capacity}Wh" + omarchy-text "Battery %1% · %2 left ·  %3W / %4Wh" "$percentage" "$time_remaining" "$power_rate" "$capacity" fi diff --git a/bin/omarchy-capture-screenrecording b/bin/omarchy-capture-screenrecording index 0c4c5494..3d4f5b22 100755 --- a/bin/omarchy-capture-screenrecording +++ b/bin/omarchy-capture-screenrecording @@ -23,7 +23,7 @@ OUTPUT_DIR="${OMARCHY_SCREENRECORD_DIR:-${XDG_VIDEOS_DIR:-$HOME/Videos}}" if [[ ! -d $OUTPUT_DIR ]]; then - omarchy-notification-send -u critical -t 3000 "Screen recording directory does not exist: $OUTPUT_DIR" + omarchy-notification-send -u critical -t 3000 "$(omarchy-text "Screen recording directory does not exist: %1" "$OUTPUT_DIR")" exit 1 fi diff --git a/bin/omarchy-crash-watch b/bin/omarchy-crash-watch index d3e78d16..65fefe55 100755 --- a/bin/omarchy-crash-watch +++ b/bin/omarchy-crash-watch @@ -40,7 +40,7 @@ announce() { omarchy-notification-send \ --urgency critical \ --glyph "$CRASH_GLYPH" \ - "Process crashed: $comm" \ + "$(omarchy-text "Process crashed: %1" "$comm")" \ "Click to diagnose with AI" \ --exec omarchy-agent-crash "$pid" "$comm" "$exe" "$signal" } diff --git a/bin/omarchy-default-browser b/bin/omarchy-default-browser index 3e54bd3b..9feeb4d9 100755 --- a/bin/omarchy-default-browser +++ b/bin/omarchy-default-browser @@ -48,4 +48,4 @@ fi env -u BROWSER xdg-settings set default-web-browser "$desktop_id" || exit 1 -omarchy-notification-send -g $glyph "$name is now the default browser" +omarchy-notification-send -g $glyph "$(omarchy-text "%1 is now the default browser" "$name")" diff --git a/bin/omarchy-default-editor b/bin/omarchy-default-editor index 74964fd6..a24c6b80 100755 --- a/bin/omarchy-default-editor +++ b/bin/omarchy-default-editor @@ -59,4 +59,4 @@ fi mkdir -p "$(dirname "$editor_file")" printf '%s\n' "$editor" >"$editor_file" -omarchy-notification-send -g $glyph "$name is now the default editor" +omarchy-notification-send -g $glyph "$(omarchy-text "%1 is now the default editor" "$name")" diff --git a/bin/omarchy-default-terminal b/bin/omarchy-default-terminal index ff0e5df2..177c2d41 100755 --- a/bin/omarchy-default-terminal +++ b/bin/omarchy-default-terminal @@ -48,4 +48,4 @@ cat >~/.config/xdg-terminals.list <"$TOGGLE_FLAG" - omarchy-notification-send -g 󰍹 "Mirroring enabled ($EXTERNAL)" + omarchy-notification-send -g 󰍹 "$(omarchy-text "Mirroring enabled (%1)" "$EXTERNAL")" hyprctl reload fi } diff --git a/bin/omarchy-hyprland-window-width b/bin/omarchy-hyprland-window-width index 78bf5d89..eb8e3700 100755 --- a/bin/omarchy-hyprland-window-width +++ b/bin/omarchy-hyprland-window-width @@ -40,14 +40,14 @@ notify_missing_width() { local key="$1" local workspace="$2" - omarchy-notification-send -g  "No saved width found for $key on workspace $workspace" "Use Super + Alt + Home to save one for this workspace." + omarchy-notification-send -g  "$(omarchy-text "No saved width found for %1 on workspace %2" "$key" "$workspace")" "Use Super + Alt + Home to save one for this workspace." } notify_saved_width() { local key="$1" local workspace="$2" - omarchy-notification-send -g  "Saved width for $key on workspace $workspace" "Restore using Super + Home on this workspace." + omarchy-notification-send -g  "$(omarchy-text "Saved width for %1 on workspace %2" "$key" "$workspace")" "Restore using Super + Home on this workspace." } hypr_dispatch() { diff --git a/bin/omarchy-hyprland-workspace-layout-toggle b/bin/omarchy-hyprland-workspace-layout-toggle index 29b0c10e..66832858 100755 --- a/bin/omarchy-hyprland-workspace-layout-toggle +++ b/bin/omarchy-hyprland-workspace-layout-toggle @@ -18,4 +18,4 @@ printf 'hl.workspace_rule({ workspace = "%s", layout = "%s" })\n' "$ACTIVE_WORKS hyprctl eval "hl.workspace_rule({ workspace = \"$ACTIVE_WORKSPACE\", layout = \"$NEW_LAYOUT\" })" >/dev/null 2>&1 || \ hyprctl keyword workspace "$ACTIVE_WORKSPACE, layout:$NEW_LAYOUT" -omarchy-notification-send -g 󱂬 "Workspace layout set to $NEW_LAYOUT" +omarchy-notification-send -g 󱂬 "$(omarchy-text "Workspace layout set to %1" "$NEW_LAYOUT")" diff --git a/bin/omarchy-menu-plugin b/bin/omarchy-menu-plugin index 35ce5a36..8566bed5 100755 --- a/bin/omarchy-menu-plugin +++ b/bin/omarchy-menu-plugin @@ -28,7 +28,7 @@ rows=$(jq -r --arg icon "$PLUGIN_ICON" \ ". as \$plugins | .[] | select($filter) | \$icon + \"\\t\" + .name + \"\\t\" + .id" <<<"$plugins") -[[ -n $rows ]] || { omarchy-notification-send "No plugin to ${1}"; exit 0; } +[[ -n $rows ]] || { omarchy-notification-send "$(omarchy-text "No plugin to $1")"; exit 0; } selection=$(omarchy-menu-select "${1^} plugin" <<<"$rows") || exit 0 [[ -n $selection ]] || exit 0 diff --git a/bin/omarchy-menu-timezone b/bin/omarchy-menu-timezone index 1c95a1ba..0e88493d 100755 --- a/bin/omarchy-menu-timezone +++ b/bin/omarchy-menu-timezone @@ -9,4 +9,4 @@ set -e timezone=$(timedatectl list-timezones | omarchy-menu-select "Set timezone" -- --width 520 --maxheight 520) || exit 1 sudo timedatectl set-timezone "$timezone" omarchy-shell -q omarchy.clock refresh -omarchy-notification-send "Timezone is now set to $timezone" +omarchy-notification-send "$(omarchy-text "Timezone is now set to %1" "$timezone")" diff --git a/bin/omarchy-migrate-notify b/bin/omarchy-migrate-notify index 00cce3d6..ae0ef9a1 100755 --- a/bin/omarchy-migrate-notify +++ b/bin/omarchy-migrate-notify @@ -21,9 +21,9 @@ pending_migrations=$(omarchy-migrate --pending 2>/dev/null) || exit 0 pending_count=$(printf '%s\n' "$pending_migrations" | sed '/^[[:space:]]*$/d' | wc -l) if (( pending_count == 1 )); then - message="Click to run 1 pending migration." + message=$(omarchy-text "Click to run 1 pending migration.") else - message="Click to run $pending_count pending migrations." + message=$(omarchy-text "Click to run %1 pending migrations." "$pending_count") fi # This runs from omarchy-migrate-notify.service after graphical-session.target, diff --git a/bin/omarchy-notification-time b/bin/omarchy-notification-time index 6963f373..99adb274 100755 --- a/bin/omarchy-notification-time +++ b/bin/omarchy-notification-time @@ -2,4 +2,4 @@ # omarchy:summary=Show the current time and date notification -omarchy-notification-send -g  -u low "$(date +"%A %H:%M · %d %B %Y · Week %V")" +omarchy-notification-send -g  -u low "$(date +"%A %H:%M · %d %B %Y") · $(omarchy-text "Week %1" "$(date +%V)")" diff --git a/bin/omarchy-reminder b/bin/omarchy-reminder index d1287101..e6df57d1 100755 --- a/bin/omarchy-reminder +++ b/bin/omarchy-reminder @@ -59,7 +59,7 @@ show_reminders() { if [[ -n $reminder_message ]]; then body+="$reminder_message in $(format_remaining $remaining) ($(date -d "@$next" +%-H:%M))"$'\n' else - body+="${reminder_minutes}-min reminder in $(format_remaining $remaining) ($(date -d "@$next" +%-H:%M))"$'\n' + body+="$(omarchy-text "%1-min reminder in %2 (%3)" "$reminder_minutes" "$(format_remaining $remaining)" "$(date -d "@$next" +%-H:%M)")"$'\n' fi done < <(active_reminder_timers "$now") @@ -91,7 +91,7 @@ show_json() { if [[ -n $reminder_message ]]; then label=$reminder_message else - label="${reminder_minutes}-min reminder" + label=$(omarchy-text "%1-min reminder" "$reminder_minutes") fi item_json=$(jq -cn \ @@ -176,7 +176,7 @@ if [[ -z $minutes ]] || [[ ! $minutes =~ ^[0-9]+$ ]] || ((minutes == 0)); then fi if [[ -z $message ]]; then - message="Your ${minutes} minutes are up" + message=$(omarchy-text "Your %1 minutes are up" "$minutes") fi set_at=$(date +%s) @@ -184,14 +184,14 @@ remind_at=$(date -d "+${minutes} minutes" +%H:%M) unit="omarchy-reminder-${minutes}m-$set_at" reminder_dir="${XDG_RUNTIME_DIR:-/tmp}/omarchy-reminders" message_file="$reminder_dir/$unit.message" -confirmation="You'll be reminded at $remind_at" -confirmation_title="Reminder set for ${minutes} minutes" +confirmation=$(omarchy-text "You'll be reminded at %1" "$remind_at") +confirmation_title=$(omarchy-text "Reminder set for %1 minutes" "$minutes") mkdir -p "$reminder_dir" if [[ -n $custom_message ]]; then printf "%s" "$custom_message" >"$message_file" - confirmation_title="$custom_message in ${minutes} minutes" + confirmation_title=$(omarchy-text "%1 in %2 minutes" "$custom_message" "$minutes") fi systemd-run --user --quiet --collect --on-active="${minutes}m" --unit="$unit" \ diff --git a/bin/omarchy-system-sleep-lock b/bin/omarchy-system-sleep-lock index d471cea0..1e1ccfe6 100755 --- a/bin/omarchy-system-sleep-lock +++ b/bin/omarchy-system-sleep-lock @@ -99,7 +99,7 @@ report_unsecured() { omarchy-notification-send -u critical -g 󰌾 \ "Screen did not lock before suspend" \ - "The session was left unlocked ($1)." >/dev/null 2>&1 || true + "$(omarchy-text "The session was left unlocked (%1)." "$1")" >/dev/null 2>&1 || true exit 1 } diff --git a/bin/omarchy-tailscale-receive b/bin/omarchy-tailscale-receive index 3f27641c..fd3e0eca 100755 --- a/bin/omarchy-tailscale-receive +++ b/bin/omarchy-tailscale-receive @@ -59,7 +59,7 @@ claim_path() { announce() { local path="$1" local name="${path##*/}" - local args=("Received $name" "Saved to ${dir/#$HOME/~}" -u critical) + local args=("$(omarchy-text "Received %1" "$name")" "$(omarchy-text "Saved to %1" "${dir/#$HOME/~}")" -u critical) case "${name,,}" in *.png | *.jpg | *.jpeg | *.gif | *.webp | *.avif | *.bmp | *.tif | *.tiff) diff --git a/bin/omarchy-tailscale-send b/bin/omarchy-tailscale-send index 84093cc8..f8d736b2 100755 --- a/bin/omarchy-tailscale-send +++ b/bin/omarchy-tailscale-send @@ -27,7 +27,7 @@ if ((${#files[@]} == 0)); then picked=$(omarchy-file-select --title "Send to $name" --multiple) || status=$? if ((${status:-0} > 1)); then - omarchy-notification-send -g "󰒊" -u critical "Could not send to $name" \ + omarchy-notification-send -g "󰒊" -u critical "$(omarchy-text "Could not send to %1" "$name")" \ "The file chooser did not open" exit 1 fi @@ -43,8 +43,8 @@ else fi if error=$(tailscale file cp --update-interval=0 -- "${files[@]}" "$machine:" 2>&1); then - omarchy-notification-send -g "󰒊" "Sent to $name" "$what" + omarchy-notification-send -g "󰒊" "$(omarchy-text "Sent to %1" "$name")" "$what" else - omarchy-notification-send -g "󰒊" -u critical "Could not send to $name" "${error:-Taildrop transfer failed}" + omarchy-notification-send -g "󰒊" -u critical "$(omarchy-text "Could not send to %1" "$name")" "${error:-$(omarchy-text "Taildrop transfer failed")}" exit 1 fi diff --git a/bin/omarchy-text b/bin/omarchy-text new file mode 100755 index 00000000..17768a9c --- /dev/null +++ b/bin/omarchy-text @@ -0,0 +1,38 @@ +#!/bin/bash + +# omarchy:summary=Print an Omarchy message in the session's language, with %1.. filled from the arguments +# omarchy:args= [args...] +# omarchy:examples=omarchy-text "Sent to %1" phone +# omarchy:hidden=true + +set -euo pipefail + +if (($# < 1)); then + echo "Usage: omarchy-text [args...]" >&2 + exit 1 +fi + +text=$1 +shift + +# The shell's table is the one source: one JSON-encoded "english": "chinese" pair per line. +if [[ ${LANG:-} == zh_CN* ]]; then + translated=$(grep -E '^ ".*": ".*",?$' "$OMARCHY_PATH/shell/Commons/I18nZh.js" | sed 's/,$//' | paste -sd, | + sed 's/^/{/; s/$/}/' | jq -r --arg key "$text" '.[$key] // empty') + [[ -z $translated ]] || text=$translated +fi + +# One pass over the template, so an argument that holds %2 is printed as given. +out="" +rest=$text +while [[ $rest =~ ^([^%]*)%(.)(.*)$ ]]; do + out+=${BASH_REMATCH[1]} + mark=${BASH_REMATCH[2]} + rest=${BASH_REMATCH[3]} + if [[ $mark == [1-9] ]] && ((mark <= $#)); then + out+=${!mark} + else + out+="%$mark" + fi +done +printf '%s\n' "$out$rest" diff --git a/bin/omarchy-transcode b/bin/omarchy-transcode index 27337d06..5f0bcd6b 100755 --- a/bin/omarchy-transcode +++ b/bin/omarchy-transcode @@ -193,14 +193,14 @@ main() { output=$(output_path "$input" "$format" "$resolution") if [[ $type == "video" ]]; then - omarchy-notification-send -g  "Transcoding video…" "$(basename -- "$input") to $format ($resolution)" + omarchy-notification-send -g  "Transcoding video…" "$(omarchy-text "%1 to %2 (%3)" "$(basename -- "$input")" "$format" "$resolution")" transcode_video "$input" "$format" "$resolution" "$output" copy_to_clipboard "$output" - omarchy-notification-send -g  "Transcoded to $resolution $format" "Saved and copied to clipboard." + omarchy-notification-send -g  "$(omarchy-text "Transcoded to %1 %2" "$resolution" "$format")" "Saved and copied to clipboard." else transcode_picture "$input" "$format" "$resolution" "$output" copy_to_clipboard "$output" - omarchy-notification-send -g  "Transcoded to $resolution $format" "Saved and copied to clipboard." + omarchy-notification-send -g  "$(omarchy-text "Transcoded to %1 %2" "$resolution" "$format")" "Saved and copied to clipboard." fi } diff --git a/shell/Commons/I18n.qml b/shell/Commons/I18n.qml index 731bad2d..857b21d4 100644 --- a/shell/Commons/I18n.qml +++ b/shell/Commons/I18n.qml @@ -13,8 +13,4 @@ QtObject { function trMatch(text, templates) { return Tr.trMatch(text, templates) } - - function trMessage(text) { - return Tr.trMessage(text) - } } diff --git a/shell/Commons/I18nZh.js b/shell/Commons/I18nZh.js index 6771e9eb..96b13372 100644 --- a/shell/Commons/I18nZh.js +++ b/shell/Commons/I18nZh.js @@ -480,7 +480,6 @@ var table = { "Couldn't add the %1 account": "无法添加 %1 账户", "Mirroring enabled (%1)": "已开启镜像(%1)", "New %1 sessions now use %2": "新的 %1 会话现在使用 %2", - "No plugin to %1": "没有插件可以%1", "No saved width found for %1 on workspace %2": "工作区 %2 没有为 %1 保存的宽度", "Saved width for %1 on workspace %2": "已为工作区 %2 的 %1 保存宽度", "Screen recording directory does not exist: %1": "录屏目录不存在:%1", @@ -528,9 +527,29 @@ var table = { "The session was left unlocked (%1).": "会话没有锁定(%1)。", "%1 to %2 (%3)": "%1 转为 %2(%3)", "Battery %1% · Holding at %2 · %3W / %4Wh": "电池 %1% · 保持在 %2 · %3W / %4Wh", - "Battery %1% · %2 to full · %3W / %4Wh": "电池 %1% · %2 后充满 · %3W / %4Wh", - "Battery %1% · %2 left · %3W / %4Wh": "电池 %1% · 剩余 %2 · %3W / %4Wh", - "%1 · %2 · Week %3": "%1 · %2 · 第 %3 周", + "Battery %1% · %2 to full ·  %3W / %4Wh": "电池 %1% · %2 后充满 ·  %3W / %4Wh", + "Battery %1% · %2 left ·  %3W / %4Wh": "电池 %1% · 剩余 %2 ·  %3W / %4Wh", + "Week %1": "第 %1 周", + "Installing the %1 CLI didn't finish.": "%1 命令行没有装完。", + "No %1 account named %2.": "没有名为 %2 的 %1 账户。", + "No %1 account named %2": "没有名为 %2 的 %1 账户", + "Couldn't set up a home for the new account.": "无法为新账户准备目录。", + "%1 is not a pending %2 login": "%1 不是待完成的 %2 登录", + "The login didn't finish, so no account was added.": "登录没有完成,没有添加账户。", + "That's %1 (%2). Try again, and sign in as the other account in the private window.": "这是 %1(%2)。请重试,并在隐私窗口中登录另一个账户。", + "already added": "已添加", + "Give the account a name": "给账户起个名字", + "%1 is the login in %2; it can't be removed here.": "%1 是 %2 中的登录,不能在这里移除。", + "A running %1 session uses %2. Quit it first, then remove the account.": "有一个正在运行的 %1 会话使用 %2。先退出它,再移除账户。", + "Switch mode is manual or auto": "切换模式为手动或自动", + "The threshold is a percentage between 50 and 100": "阈值是 50 到 100 之间的百分比", + "%1-min reminder in %2 (%3)": "%1 分钟提醒,还有 %2(%3)", + "%1-min reminder": "%1 分钟提醒", + "Your %1 minutes are up": "%1 分钟到了", + "No plugin to enable": "没有可启用的插件", + "No plugin to disable": "没有可停用的插件", + "No plugin to clone": "没有可克隆的插件", + "No plugin to remove": "没有可移除的插件", // Clock "Right-click to toggle format": "右键切换格式", "Back to today": "回到今天", @@ -578,53 +597,3 @@ var table = { "Next": "下一首", "Previous": "上一首" } - -// Parameterized messages that arrive finished; trMessage matches them, so each needs a table entry above. -var templates = [ - "Anthropic's usage endpoint is rate limiting checks right now (retry after %1s). Local Claude Code stats are still shown.", - "Anthropic's usage endpoint returned status %1. Local Claude Code stats are still shown.", - "New %1 sessions now use %2 (%3%). Running sessions stay on %4.", - "Sign in to %1 again in the browser window that opens.", - "%1 is at %2%. Click to switch new sessions to it.", - "Sign in to %1 in the browser window that opens.", - "Screen recording directory does not exist: %1", - "Battery %1% · Holding at %2 · %3W / %4Wh", - "No saved width found for %1 on workspace %2", - "Battery %1% · %2 to full · %3W / %4Wh", - "Battery %1% · %2 left · %3W / %4Wh", - "Click to run %1 pending migrations.", - "The session was left unlocked (%1).", - "Saved width for %1 on workspace %2", - "Staying on %1. %2 resets in %3.", - "%1 is now the default terminal", - "%1 is now the default browser", - "The %1 sign-in didn't finish.", - "%1 is now the default editor", - "All %1 accounts are over %2%", - "Running sessions stay on %1.", - "Couldn't add the %1 account", - "Reminder set for %1 minutes", - "New %1 sessions now use %2", - "Workspace layout set to %1", - "%1 is at %2% of a %3 limit", - "Timezone is now set to %1", - "You'll be reminded at %1", - "Battery is down to %1%", - "Mirroring enabled (%1)", - "Installing the %1 CLI…", - "Signed in to %1 again.", - "%1 · %2 · Week %3", - "Could not send to %1", - "Transcoded to %1 %2", - "Process crashed: %1", - "Signed in to %1.", - "%1 in %2 minutes", - "No plugin to %1", - "Staying on %1.", - "%1 to %2 (%3)", - "%1 installed", - "Received %1", - "Saved to %1", - "Sent to %1", - "Added %1." -] diff --git a/shell/Commons/Tr.js b/shell/Commons/Tr.js index ef8e6ad4..b4be1108 100644 --- a/shell/Commons/Tr.js +++ b/shell/Commons/Tr.js @@ -13,7 +13,7 @@ function tr(text) { }) } -// Text built elsewhere around a name: the first English template that matches is translated with what %1.. captured. +// Text a panel's own model builds in known formats: the first matching template is translated with what %1.. captured. function trMatch(text, templates) { for (var i = 0; i < templates.length; i++) { var pattern = "^" + templates[i].replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/%\d/g, "(.+)") + "$" @@ -22,8 +22,3 @@ function trMatch(text, templates) { } return tr(text) } - -// Finished messages from other programs (notifications, collectors) that carry a name or number. -function trMessage(text) { - return trMatch(text, Zh.templates) -} diff --git a/shell/plugins/agents/Panel.qml b/shell/plugins/agents/Panel.qml index 1b51d224..f488b783 100644 --- a/shell/plugins/agents/Panel.qml +++ b/shell/plugins/agents/Panel.qml @@ -1173,7 +1173,7 @@ Panel { Text { width: parent.width textFormat: Text.PlainText - text: I18n.trMessage(root.addStatus) + text: I18n.tr(root.addStatus) color: root.foreground font.family: root.fontFamily font.pixelSize: Style.font.bodySmall @@ -1243,7 +1243,7 @@ Panel { visible: root.addStage === "done" || root.addStage === "error" width: parent.width textFormat: Text.PlainText - text: I18n.trMessage(root.addResult) + text: I18n.tr(root.addResult) color: root.addStage === "error" ? root.urgent : root.foreground font.family: root.fontFamily font.pixelSize: Style.font.bodySmall @@ -1350,7 +1350,7 @@ Panel { width: parent.width textFormat: Text.PlainText // Shown for the status, so a record with no help to offer says the status rather than nothing. - text: section.provider ? I18n.trMessage(String(section.provider.authHelpText || section.provider.usageStatusText || "")) : "" + text: section.provider ? I18n.tr(String(section.provider.authHelpText || section.provider.usageStatusText || "")) : "" color: root.urgent font.family: root.fontFamily font.pixelSize: Style.font.caption @@ -1879,7 +1879,7 @@ Panel { PanelToolTip { visible: unavailableHover.hovered && unavailable.visible - text: unavailable.record ? I18n.trMessage(String(unavailable.record.authHelpText || "Usage has not been updated yet.")) : "" + text: unavailable.record ? I18n.tr(String(unavailable.record.authHelpText || "Usage has not been updated yet.")) : "" } } diff --git a/shell/plugins/notifications/components/NotificationCard.qml b/shell/plugins/notifications/components/NotificationCard.qml index 97a6eb34..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(I18n.trMessage(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: I18n.trMessage(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 abb43f14..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: I18n.trMessage(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: I18n.trMessage(root.message) + text: I18n.tr(root.message) font: messageMetrics.font color: Color.popups.text elide: Text.ElideRight diff --git a/shell/plugins/panels/dropbox/Panel.qml b/shell/plugins/panels/dropbox/Panel.qml index b16df749..4774cfbf 100644 --- a/shell/plugins/panels/dropbox/Panel.qml +++ b/shell/plugins/panels/dropbox/Panel.qml @@ -291,7 +291,7 @@ Panel { textFormat: Text.PlainText visible: dropbox.actionStatus !== "" || dropbox.lastError !== "" width: parent.width - text: I18n.trMessage(dropbox.actionStatus !== "" ? dropbox.actionStatus : dropbox.lastError) + text: I18n.tr(dropbox.actionStatus !== "" ? dropbox.actionStatus : dropbox.lastError) color: dropbox.lastError !== "" && dropbox.actionStatus === "" ? root.urgent : root.dim font.family: root.fontFamily font.pixelSize: Style.font.bodySmall diff --git a/test/shell.d/agent-usage-claude-limits-test.sh b/test/shell.d/agent-usage-claude-limits-test.sh index 70d2b379..5428efe9 100644 --- a/test/shell.d/agent-usage-claude-limits-test.sh +++ b/test/shell.d/agent-usage-claude-limits-test.sh @@ -5,6 +5,12 @@ source "$(dirname "$0")/base-test.sh" require_command jq require_command python3 +# The collector words its diagnostics through omarchy-text. +text_bin=$(mktemp -d) +trap 'rm -rf "$text_bin"' EXIT +ln -s "$ROOT/bin/omarchy-text" "$text_bin/omarchy-text" +export PATH="$text_bin:$PATH" + # probe_limits reaches Anthropic, so the reader that interprets its answer is # exercised on its own: the collector loads as a module, and a recorded payload # stands in for the response. @@ -112,7 +118,7 @@ pass "Claude collector keeps Max multipliers and plain plan names" # is still good. Drive collect_limits over a planted cache with the network # unreachable, so nothing but the credential state decides the answer. CACHE_HOME=$(mktemp -d) -trap 'rm -rf "$CACHE_HOME"' EXIT +trap 'rm -rf "$CACHE_HOME" "$text_bin"' EXIT collect_limits() { COLLECTOR="$ROOT/bin/omarchy-agent-usage-claude" TOKEN="$1" EXPIRES_AT="$2" \ diff --git a/test/shell.d/agents-panel-test.sh b/test/shell.d/agents-panel-test.sh index c5981b42..5449aaff 100755 --- a/test/shell.d/agents-panel-test.sh +++ b/test/shell.d/agents-panel-test.sh @@ -31,7 +31,7 @@ assert(!/opacity: stale \?/.test(panelSource) && /I18n\.tr\("Last updated %1 ago assert(/onPickingChanged: resetKeys\(\)/.test(panelSource), 'the cursor starts over when the agent list comes or goes') assert(!/t === "a" \|\| t === "A"/.test(panelSource), 'adding an account has no hotkey; the + is the way in') assert(/if \(!accounts\[a\]\.active\) \{/.test(panelSource) && /if \(row\.length > 0\) rows\.push\(row\)/.test(panelSource), 'the active account with nothing to fix is not a keyboard stop') -assert(/text: section\.provider \? I18n\.trMessage\(String\(section\.provider\.authHelpText \|\| section\.provider\.usageStatusText \|\| ""\)\) : ""/.test(panelSource), 'the trouble line falls back to the usage status when a record has no auth help') +assert(/text: section\.provider \? I18n\.tr\(String\(section\.provider\.authHelpText \|\| section\.provider\.usageStatusText \|\| ""\)\) : ""/.test(panelSource), 'the trouble line falls back to the usage status when a record has no auth help') const mainSource = fs.readFileSync(root + '/shell/plugins/agents/Main.qml', 'utf8') assert(/var from = Math\.min\(0\.8, \(threshold - 15\) \/ 100\)/.test(mainSource), 'faster checks start 15 points below the switch threshold') assert(/rows\.push\(\[\{ kind: "provider", index: p \}\]\)/.test(panelSource), "every agent's header is a keyboard stop") diff --git a/test/shell.d/menu-plugin-test.sh b/test/shell.d/menu-plugin-test.sh index d29403cb..0e3348ea 100755 --- a/test/shell.d/menu-plugin-test.sh +++ b/test/shell.d/menu-plugin-test.sh @@ -11,6 +11,7 @@ trap 'rm -rf "$TMPDIR"' EXIT STUB_DIR="$TMPDIR/stub" mkdir -p "$STUB_DIR" +ln -s "$ROOT/bin/omarchy-text" "$STUB_DIR/omarchy-text" # The picker reads the plugin list from omarchy-plugin-list and hands what it # decided to a verb-specific command, so stubbing both ends shows which plugin diff --git a/test/shell.d/notifications-test.sh b/test/shell.d/notifications-test.sh index bf9890d5..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\(I18n\.trMessage\(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 index a0e6a227..6f7ca5d3 100644 --- a/test/shell.d/shell-i18n-test.sh +++ b/test/shell.d/shell-i18n-test.sh @@ -10,7 +10,7 @@ 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; this.templates = templates', context) +vm.runInNewContext(source + '\nthis.table = table', context) const table = context.table const files = [] @@ -40,11 +40,33 @@ for (const file of files) { } } assert(calls > 300, 'the shell routes its text through I18n.tr', `found ${calls} calls`) -const missingTemplates = Array.from(context.templates).filter(t => !(t in table)) -assert(missingTemplates.length === 0, 'every message template has a Chinese entry', missingTemplates.join('\n')) + +// Senders translate messages that carry a name or number with omarchy-text, or text() in Python. +const senders = [] +for (const entry of fs.readdirSync(path.join(root, 'bin'), { withFileTypes: true })) { + if (!entry.isFile()) continue + const name = entry.name + const text = fs.readFileSync(path.join(root, 'bin', name), 'utf8') + for (const match of text.matchAll(/(?:omarchy-text\s+|\btext\(\s*)"((?:[^"\\]|\\.)*)"/g)) { + if (match[1].includes('$')) continue + senders.push([name, JSON.parse('"' + match[1] + '"')]) + } +} +assert(senders.length > 40, 'senders translate their parameterized messages', `found ${senders.length}`) +const unsent = senders.filter(([, key]) => !(key in table)).map(([file, key]) => `bin/${file}: ${key}`) +assert(unsent.length === 0, 'every sender message has a Chinese entry', unsent.join('\n')) 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 + +text=$ROOT/bin/omarchy-text +[[ $(OMARCHY_PATH=$ROOT LANG=zh_CN.UTF-8 "$text" "Sent to %1" "phone %2") == "已发送给 phone %2" ]] || + fail "omarchy-text translates and fills %1 in one pass" +[[ $(OMARCHY_PATH=$ROOT LANG=en_US.UTF-8 "$text" "Battery is down to %1%" 9) == "Battery is down to 9%" ]] || + fail "omarchy-text keeps English and a literal % outside zh_CN" +[[ $(OMARCHY_PATH=$ROOT LANG=zh_CN.UTF-8 "$text" "Unknown 50% of %1" x) == "Unknown 50% of x" ]] || + fail "omarchy-text passes an unknown message through" +pass "omarchy-text translates by exact key and fills arguments in one pass" diff --git a/test/shell.d/tailscale-receive-test.sh b/test/shell.d/tailscale-receive-test.sh index 93ec4da3..8eee819b 100644 --- a/test/shell.d/tailscale-receive-test.sh +++ b/test/shell.d/tailscale-receive-test.sh @@ -10,6 +10,7 @@ trap cleanup EXIT downloads="$WORKDIR/downloads" mkdir -p "$WORKDIR/bin" "$downloads" "$WORKDIR/outbox" +ln -s "$ROOT/bin/omarchy-text" "$WORKDIR/bin/omarchy-text" printf 'mine' >"$downloads/unrelated.txt" # Stands in for the daemon handing over whatever is waiting in the inbox. A