Word parameterized messages at the sender with omarchy-text and translate only exact text in the shell

This commit is contained in:
ZacharyZhang-NY committed 2026-10-06 22:29:57 -04:00
1 parent 619979a716
commit 051952600d
38 files changed
+198 -156

No files matched your search

+1 -1
View File
@@ -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
+16 -16
View File
@@ -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 <<EOF
@@ -201,7 +201,7 @@ Don't log out of the $name CLI to do this: logging out revokes the saved sign-in
EOF
fi
home=$(omarchy-agent-account-state begin "$provider") || fail "Couldn't set up a home for the new account."
home=$(omarchy-agent-account-state begin "$provider") || fail "$(omarchy-text "Couldn't set up a home for the new account.")"
private_browser
interruptible env BROWSER="$browser" "$home_env=$home" "${login_command[@]}"
@@ -211,9 +211,9 @@ EOF
added=$(jq -r '.label + (if .email != "" then " (" + .email + ")" else "" end)' <<<"$account")
omarchy-agent-usage-update --force "$provider" >/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
+29 -22
View File
@@ -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
+2 -2
View File
@@ -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
+11 -4
View File
@@ -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
+1 -1
View File
@@ -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"
+3 -3
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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"
}
+1 -1
View File
@@ -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")"
+1 -1
View File
@@ -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")"
+1 -1
View File
@@ -48,4 +48,4 @@ cat >~/.config/xdg-terminals.list <<EOF
$desktop_id
EOF
omarchy-notification-send -g $glyph "$name is now the default terminal"
omarchy-notification-send -g $glyph "$(omarchy-text "%1 is now the default terminal" "$name")"
+1 -1
View File
@@ -35,7 +35,7 @@ on() {
if omarchy-hyprland-toggle-disabled $TOGGLE; then
printf 'hl.monitor({ output = "%s", mode = "preferred", position = "auto", scale = 1, mirror = "%s" })\n' "$EXTERNAL" "$INTERNAL" >"$TOGGLE_FLAG"
omarchy-notification-send -g 󰍹 "Mirroring enabled ($EXTERNAL)"
omarchy-notification-send -g 󰍹 "$(omarchy-text "Mirroring enabled (%1)" "$EXTERNAL")"
hyprctl reload
fi
}
+2 -2
View File
@@ -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() {
+1 -1
View File
@@ -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")"
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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")"
+2 -2
View File
@@ -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,
+1 -1
View File
@@ -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)")"
+6 -6
View File
@@ -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" \
+1 -1
View File
@@ -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
}
+1 -1
View File
@@ -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)
+3 -3
View File
@@ -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
+38
View File
@@ -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=<english> [args...]
# omarchy:examples=omarchy-text "Sent to %1" phone
# omarchy:hidden=true
set -euo pipefail
if (($# < 1)); then
echo "Usage: omarchy-text <english> [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"
+3 -3
View File
@@ -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
}
-4
View File
@@ -13,8 +13,4 @@ QtObject {
function trMatch(text, templates) {
return Tr.trMatch(text, templates)
}
function trMessage(text) {
return Tr.trMessage(text)
}
}
+23 -54
View File
@@ -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."
]
+1 -6
View File
@@ -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)
}
+4 -4
View File
@@ -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.")) : ""
}
}
@@ -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
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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
@@ -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" \
+1 -1
View File
@@ -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")
+1
View File
@@ -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
+1 -1
View File
@@ -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(
+25 -3
View File
@@ -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"
+1
View File
@@ -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