diff --git a/AGENTS.md b/AGENTS.md index 62319bab..f58d2482 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,6 +19,7 @@ This fork carries the OmarchyCN China-integration layer on top of upstream `base - `cn/` holds the data layer: `mirrors.json`, `dev-mirrors.json`, `apps.json`, `registry/` (AI providers/harnesses/compatibility), `fcitx5/`, `fontconfig/`, `keys/`, `lib/` (sourced helpers), `release` (cn release number) - cn migrations live in `cn/migrations/*.sh`, run by `omarchy-cn-update` with per-file completion markers under `~/.local/state/omarchycn/` - Packaging: `packages/omarchy-pkgs-cn.patch` must be applied to the sibling `omarchy-pkgs` checkout so `omarchy-dev` ships `cn/`; keyring in `packages/omarchycn-keyring/` +- ISO: `packages/omarchy-iso-cn.patch` must be applied to the sibling `omarchy-iso` checkout — Chinese installer (cage+foot graphical console with English VT fallback) and live-env packages - Release process: `docs/release-checklist.md`; signing: `docs/release-signing.md`; pacman repo: `docs/pacman-repo.md` - Upstream sync: `.gitea/workflows/upstream-sync.yml` opens a PR per upstream change; keep upstream file edits minimal (currently: one `GROUP_DESCRIPTIONS[cn]` line in `bin/omarchy`, the OmarchyCN section in `default/omarchy/omarchy-menu.jsonc`, a rewritten `README.md` (known recurring sync conflict, resolve toward ours), `AGENTS.md` additions, and two `.gitignore` lines) - cn tests: `test/shell.d/omarchycn-test.sh`, `test/shell.d/omarchycn-ai-test.sh` diff --git a/install/provisioning/setup-form.sh b/install/provisioning/setup-form.sh index 909668eb..470da4b1 100644 --- a/install/provisioning/setup-form.sh +++ b/install/provisioning/setup-form.sh @@ -24,6 +24,49 @@ OMARCHY_FORM_BACK=1 OMARCHY_FORM_SIGNAL=130 +# Prompt strings; the ISO's graphical console exports OMARCHY_INSTALL_LANG=zh +if [[ ${OMARCHY_INSTALL_LANG:-} == zh ]]; then + OMARCHY_L_KEYBOARD_HEADER="选择键盘布局" + OMARCHY_L_USERNAME_PROMPT="用户名> " + OMARCHY_L_USERNAME_HINT="仅限字母数字、不含空格(如 dhh)" + OMARCHY_L_USERNAME_RESERVED="该用户名已被系统保留" + OMARCHY_L_USERNAME_TAKEN="该用户名在本机已存在" + OMARCHY_L_USERNAME_INVALID="用户名必须为字母数字且不含空格" + OMARCHY_L_PASSWORD_PROMPT="密码> " + OMARCHY_L_PASSWORD_HINT="用于用户与 root,启用加密时也用于磁盘解锁" + OMARCHY_L_CONFIRM_PROMPT="确认密码> " + OMARCHY_L_CONFIRM_HINT="需与刚输入的密码一致" + OMARCHY_L_PASSWORD_BLANK="密码不能为空!" + OMARCHY_L_PASSWORD_MISMATCH="两次输入的密码不一致!" + OMARCHY_L_FULLNAME_PROMPT="姓名> " + OMARCHY_L_IDENTITY_HINT="用于 git 身份(回车跳过)" + OMARCHY_L_EMAIL_PROMPT="邮箱> " + OMARCHY_L_HOSTNAME_PROMPT="主机名> " + OMARCHY_L_HOSTNAME_HINT="字母、数字与连字符(回车默认 omarchy)" + OMARCHY_L_HOSTNAME_INVALID="主机名须为 1-63 位字母/数字/连字符,且不能以连字符开头或结尾" + OMARCHY_L_TIMEZONE_HEADER="时区" +else + OMARCHY_L_KEYBOARD_HEADER="Select keyboard layout" + OMARCHY_L_USERNAME_PROMPT="Username> " + OMARCHY_L_USERNAME_HINT="Alphanumeric without spaces (like dhh)" + OMARCHY_L_USERNAME_RESERVED="Username is reserved for system" + OMARCHY_L_USERNAME_TAKEN="That username already exists on this machine" + OMARCHY_L_USERNAME_INVALID="Username must be alphanumeric with no spaces" + OMARCHY_L_PASSWORD_PROMPT="Password> " + OMARCHY_L_PASSWORD_HINT="Used for user + root, and disk encryption when enabled" + OMARCHY_L_CONFIRM_PROMPT="Confirm> " + OMARCHY_L_CONFIRM_HINT="Must match the password you just typed" + OMARCHY_L_PASSWORD_BLANK="Your password can'"'"'t be blank!" + OMARCHY_L_PASSWORD_MISMATCH="Passwords didn'"'"'t match!" + OMARCHY_L_FULLNAME_PROMPT="Full name> " + OMARCHY_L_IDENTITY_HINT="Used for git authentication (hit return to skip)" + OMARCHY_L_EMAIL_PROMPT="Email address> " + OMARCHY_L_HOSTNAME_PROMPT="Hostname> " + OMARCHY_L_HOSTNAME_HINT="Letters, digits, and dashes (or return for '"'"'omarchy'"'"')" + OMARCHY_L_HOSTNAME_INVALID="Hostname must be 1-63 letters, digits, or dashes, and cannot start or end with a dash" + OMARCHY_L_TIMEZONE_HEADER="Timezone" +fi + # The English layouts lead, then everything else alphabetically. gum choose # paginates in --height-sized pages and jumps to the page holding --selected, # so an alphabetical English (US) landed deep enough to sit alone at the edge @@ -94,7 +137,7 @@ omarchy_username_taken() { return 1; } omarchy_prompt_keyboard() { local choice status choice=$(printf '%s\n' "$OMARCHY_KEYBOARD_LAYOUTS" | cut -d'|' -f1 | - gum choose --height 10 --selected "English (US)" --header "Select keyboard layout") && status=0 || status=$? + gum choose --height 10 --selected "English (US)" --header "$OMARCHY_L_KEYBOARD_HEADER") && status=0 || status=$? ((status == 0)) || return $status keyboard_label="$choice" @@ -104,19 +147,19 @@ omarchy_prompt_keyboard() { omarchy_prompt_username() { local status while true; do - username=$(gum input --placeholder "Alphanumeric without spaces (like dhh)" --prompt.foreground="#845DF9" --prompt "Username> ") && status=0 || status=$? + username=$(gum input --placeholder "$OMARCHY_L_USERNAME_HINT" --prompt.foreground="#845DF9" --prompt "$OMARCHY_L_USERNAME_PROMPT") && status=0 || status=$? ((status == 0)) || return $status if [[ "$username" =~ $OMARCHY_USERNAME_PATTERN ]]; then if [[ "$username" =~ $OMARCHY_RESERVED_USERNAMES ]]; then - notice "Username is reserved for system" 1 + notice "$OMARCHY_L_USERNAME_RESERVED" 1 elif omarchy_username_taken "$username"; then - notice "That username already exists on this machine" 1 + notice "$OMARCHY_L_USERNAME_TAKEN" 1 else return 0 fi else - notice "Username must be alphanumeric with no spaces" 1 + notice "$OMARCHY_L_USERNAME_INVALID" 1 fi done } @@ -124,17 +167,17 @@ omarchy_prompt_username() { omarchy_prompt_password() { local status while true; do - password=$(gum input --placeholder "Used for user + root, and disk encryption when enabled" --prompt.foreground="#845DF9" --password --prompt "Password> ") && status=0 || status=$? + password=$(gum input --placeholder "$OMARCHY_L_PASSWORD_HINT" --prompt.foreground="#845DF9" --password --prompt "$OMARCHY_L_PASSWORD_PROMPT") && status=0 || status=$? ((status == 0)) || return $status - password_confirmation=$(gum input --placeholder "Must match the password you just typed" --prompt.foreground="#845DF9" --password --prompt "Confirm> ") && status=0 || status=$? + password_confirmation=$(gum input --placeholder "$OMARCHY_L_CONFIRM_HINT" --prompt.foreground="#845DF9" --password --prompt "$OMARCHY_L_CONFIRM_PROMPT") && status=0 || status=$? ((status == 0)) || return $status if [[ -n "$password" && "$password" == "$password_confirmation" ]]; then return 0 elif [[ -z "$password" ]]; then - notice "Your password can't be blank!" 1 + notice "$OMARCHY_L_PASSWORD_BLANK" 1 else - notice "Passwords didn't match!" 1 + notice "$OMARCHY_L_PASSWORD_MISMATCH" 1 fi done } @@ -143,16 +186,16 @@ omarchy_prompt_password() { # only Esc/Ctrl+C end the prompt early. omarchy_prompt_identity() { local status - full_name=$(gum input --placeholder "Used for git authentication (hit return to skip)" --prompt.foreground="#845DF9" --prompt "Full name> ") && status=0 || status=$? + full_name=$(gum input --placeholder "$OMARCHY_L_IDENTITY_HINT" --prompt.foreground="#845DF9" --prompt "$OMARCHY_L_FULLNAME_PROMPT") && status=0 || status=$? ((status == 0)) || return $status - email_address=$(gum input --placeholder "Used for git authentication (hit return to skip)" --prompt.foreground="#845DF9" --prompt "Email address> ") && status=0 || status=$? + email_address=$(gum input --placeholder "$OMARCHY_L_IDENTITY_HINT" --prompt.foreground="#845DF9" --prompt "$OMARCHY_L_EMAIL_PROMPT") && status=0 || status=$? return $status } omarchy_prompt_hostname() { local status while true; do - hostname=$(gum input --placeholder "Letters, digits, and dashes (or return for 'omarchy')" --prompt.foreground="#845DF9" --prompt "Hostname> ") && status=0 || status=$? + hostname=$(gum input --placeholder "$OMARCHY_L_HOSTNAME_HINT" --prompt.foreground="#845DF9" --prompt "$OMARCHY_L_HOSTNAME_PROMPT") && status=0 || status=$? ((status == 0)) || return $status if [[ -z $hostname ]]; then @@ -161,7 +204,7 @@ omarchy_prompt_hostname() { elif [[ "$hostname" =~ $OMARCHY_HOSTNAME_PATTERN ]]; then return 0 else - notice "Hostname must be 1-63 letters, digits, or dashes, and cannot start or end with a dash" 1 + notice "$OMARCHY_L_HOSTNAME_INVALID" 1 fi done } @@ -173,9 +216,9 @@ omarchy_prompt_timezone() { guess=$(tzupdate -p 2>/dev/null) || guess="" if [[ -n $guess ]]; then - timezone=$(timedatectl list-timezones | gum choose --height 10 --selected "$guess" --header "Timezone") && status=0 || status=$? + timezone=$(timedatectl list-timezones | gum choose --height 10 --selected "$guess" --header "$OMARCHY_L_TIMEZONE_HEADER") && status=0 || status=$? else - timezone=$(timedatectl list-timezones | gum filter --height 10 --header "Timezone") && status=0 || status=$? + timezone=$(timedatectl list-timezones | gum filter --height 10 --header "$OMARCHY_L_TIMEZONE_HEADER") && status=0 || status=$? fi ((status == 0)) || return $status diff --git a/packages/omarchy-iso-cn.patch b/packages/omarchy-iso-cn.patch new file mode 100644 index 00000000..a0250064 --- /dev/null +++ b/packages/omarchy-iso-cn.patch @@ -0,0 +1,672 @@ +diff --git a/builder/build-iso.sh b/builder/build-iso.sh +index db8daa2..5e4ca44 100755 +--- a/builder/build-iso.sh ++++ b/builder/build-iso.sh +@@ -118,7 +118,7 @@ cp "/tmp/$NODE_FILENAME" "$build_cache_dir/airootfs/opt/packages/" + # The selected omarchy-settings package is needed here so its post_install hook + # drops Omarchy's plymouthd.conf into /etc/plymouth before mkarchiso builds the + # live initramfs. +-arch_packages=(linux-t2 git gum jq openssl plymouth ttfx tzupdate omarchy-keyring "$OMARCHY_SETTINGS_PACKAGE" lvm2 cryptsetup parted) ++arch_packages=(linux-t2 git gum jq openssl plymouth ttfx tzupdate omarchy-keyring "$OMARCHY_SETTINGS_PACKAGE" lvm2 cryptsetup parted cage foot noto-fonts-cjk) + printf '%s\n' "${arch_packages[@]}" >> "$build_cache_dir/packages.x86_64" + + # The live ISO boots linux-t2 (see airootfs/etc/mkinitcpio.d/linux-t2.preset), so +diff --git a/configs/airootfs/root/.automated_script.sh b/configs/airootfs/root/.automated_script.sh +index 16904f8..780ff6a 100644 +--- a/configs/airootfs/root/.automated_script.sh ++++ b/configs/airootfs/root/.automated_script.sh +@@ -10,7 +10,20 @@ + # - COLUMNS/LINES so gum picks up real terminal size + set -euo pipefail + +-[[ $(tty) == /dev/tty1 ]] || exit 0 ++[[ $(tty) == /dev/tty1 || -n ${OMARCHY_CN_GUI:-} ]] || exit 0 ++ ++# OmarchyCN: run the installer in a CJK-capable graphical console when KMS ++# allows; otherwise fall back to the original English VT flow. ++if [[ -z ${OMARCHY_CN_GUI:-} ]] && command -v cage > /dev/null 2>&1; then ++ export XDG_RUNTIME_DIR=/run/omarchy-gui ++ mkdir -p "$XDG_RUNTIME_DIR" ++ chmod 700 "$XDG_RUNTIME_DIR" ++ if OMARCHY_CN_GUI=1 OMARCHY_INSTALL_LANG=zh WLR_RENDERER=pixman cage -- foot -F "$0"; then ++ exit 0 ++ fi ++ echo "图形安装环境不可用,回退英文控制台 (falling back to the English VT installer)" ++ unset OMARCHY_INSTALL_LANG XDG_RUNTIME_DIR ++fi + + export OMARCHY_MIRROR="$(cat /root/omarchy_mirror)" + if [[ -f /root/omarchy_iso_ref ]]; then +diff --git a/configs/airootfs/root/configurator b/configs/airootfs/root/configurator +index 4cd3db3..92f7421 100644 +--- a/configs/airootfs/root/configurator ++++ b/configs/airootfs/root/configurator +@@ -9,6 +9,133 @@ OMARCHY_SETTINGS_PACKAGE="${OMARCHY_SETTINGS_PACKAGE:-omarchy-settings}" + + LOGO_PATH="$OMARCHY_PATH/logo.txt" + ++# Installer strings; the graphical console stage exports OMARCHY_INSTALL_LANG=zh ++if [[ ${OMARCHY_INSTALL_LANG:-} == zh ]]; then ++ L_TAGLINE='华丽、现代、观点鲜明的 Linux —— 中国版' ++ L_HINT='按回车开始安装' ++ L_ABORTED='安装已中止' ++ L_RETRY='稍后可重新运行: ./.automated_script.sh' ++ L_OWNER_PREP='这将把本机准备给另一位使用者。' ++ L_OWNER_PREP2='系统现在安装,但初始设置延迟到首次开机进行。' ++ L_OWNER_CONFIRM='为另一位使用者准备这台机器?' ++ L_OWNER_YES='是,为他人准备' ++ L_OWNER_NO='否,继续本人设置' ++ L_USER_STEP='开始设置你的用户账户……' ++ L_SUMMARY_OK='以上信息正确吗?' ++ L_SUMMARY_NO='不对,去修改' ++ L_F_FIELD='项目' ++ L_F_VALUE='值' ++ L_F_USER='用户名' ++ L_F_PASS='密码' ++ L_F_NAME='姓名' ++ L_F_EMAIL='邮箱' ++ L_F_HOST='主机名' ++ L_F_TZ='时区' ++ L_F_KB='键盘' ++ L_F_SKIP='[已跳过]' ++ L_NOSPACE_STEP='磁盘 %s 可用空间不足' ++ L_NOSPACE_1='%s 可用空间 %s;Omarchy 至少需要 32GB。' ++ L_NOSPACE_2='请用分区工具在该盘腾出至少 32GB,然后重试。' ++ L_NOSPACE_CONFIRM='返回安装方式选择?' ++ L_BACK='返回' ++ L_OPEN_PT='打开分区工具' ++ L_BL_STEP='检查 %s 上的 BitLocker' ++ L_BL_1='检测到 %s 存在 BitLocker 签名。' ++ L_BL_2='请在 Windows 中关闭 BitLocker 并等待解密完成后重试。' ++ L_BL_3='仅暂停 BitLocker 不够——磁盘仍处于加密状态。' ++ L_BL_ABORT='已中止:该磁盘启用了 BitLocker。' ++ L_EFI_STEP='检查 %s 上已有的 EFI 分区' ++ L_EFI_WIN='发现 Windows ESP 位于 %s——保持原样不动。' ++ L_EFI_OWN='Omarchy 将在空闲空间中创建自己的 EFI 分区。' ++ L_EFI_NEW='将在空闲空间中新建 EFI 分区。' ++ L_FREE_STEP='分析 %s 的空闲空间' ++ L_MKPART_STEP='在 %s 上创建分区' ++ L_LUKS_STEP='在 %s 上配置 LUKS2 加密' ++ L_BTRFS_STEP='创建 Btrfs 文件系统与子卷' ++ L_FREESAY='在 %s 空闲空间中安装 Omarchy。' ++ L_CTRLC_UNENC='按 Ctrl+C 改为不加密安装。' ++ L_YES_INSTALL='是,开始安装' ++ L_YES_NOENC='是,不加密安装' ++ L_NO_CHANGE='否,去修改' ++ L_CONFIRM_ON='确认安装到 %s' ++ L_DISK_STEP='选择要安装 Omarchy 的磁盘……' ++ L_DISK_HEADER='选择安装磁盘' ++ L_PT_STEP='%s 的分区工具' ++ L_PT_1='为 Omarchy 腾出未分配空间,然后写入更改并退出。' ++ L_PT_2='不要在这里创建 Omarchy 分区——目标区域保持为空闲空间即可。' ++ L_MODE_FULL='整盘安装' ++ L_MODE_FREE='空闲空间安装(保留现有数据)' ++ L_MODE_OTHER='换一块磁盘' ++ L_MODE_STEP='选择 Omarchy 的安装方式……' ++ L_MODE_HEADER='选择 %s 上的安装方式' ++ L_WIPE_WARN='磁盘将被完全覆写,数据无法恢复。' ++ L_WIPE_CONFIRM='确认覆写 %s' ++ L_TAGLINE_W=38 ++ L_HINT_W=14 ++else ++ L_TAGLINE='Beautiful, Modern & Opinionated Linux by DHH' ++ L_HINT='Press Return to Start Install' ++ L_ABORTED='Aborted installation' ++ L_RETRY='You can retry later by running: ./.automated_script.sh' ++ L_OWNER_PREP='This prepares the machine for another owner.' ++ L_OWNER_PREP2='The system installs now, but setup is delayed until first boot.' ++ L_OWNER_CONFIRM='Prepare this machine for another owner?' ++ L_OWNER_YES='Yes, prepare for another owner' ++ L_OWNER_NO='No, keep setting up' ++ L_USER_STEP='Let'\''s setup your user account...' ++ L_SUMMARY_OK='Does this look right?' ++ L_SUMMARY_NO='No, change it' ++ L_F_FIELD='Field' ++ L_F_VALUE='Value' ++ L_F_USER='Username' ++ L_F_PASS='Password' ++ L_F_NAME='Full name' ++ L_F_EMAIL='Email address' ++ L_F_HOST='Hostname' ++ L_F_TZ='Timezone' ++ L_F_KB='Keyboard' ++ L_F_SKIP='[Skipped]' ++ L_NOSPACE_STEP='Not enough free space on %s' ++ L_NOSPACE_1='%s has %s of usable free space; Omarchy needs at least 32GB.' ++ L_NOSPACE_2='Open the partition tool to free at least 32GB, then try again.' ++ L_NOSPACE_CONFIRM='Return to installation mode?' ++ L_BACK='Back' ++ L_OPEN_PT='Open partition tool' ++ L_BL_STEP='Checking for BitLocker on %s' ++ L_BL_1='BitLocker signature detected on %s.' ++ L_BL_2='Turn BitLocker off in Windows and wait for the drive to finish decrypting, then try again.' ++ L_BL_3='Suspending BitLocker is not enough — the drive stays encrypted.' ++ L_BL_ABORT='Aborted: BitLocker is enabled on this disk.' ++ L_EFI_STEP='Checking existing EFI partitions on %s' ++ L_EFI_WIN='Found a Windows ESP at %s — leaving it untouched.' ++ L_EFI_OWN='Omarchy will create its own dedicated EFI partition in free space.' ++ L_EFI_NEW='A new EFI partition will be created in free space.' ++ L_FREE_STEP='Analyzing free space on %s' ++ L_MKPART_STEP='Creating partitions on %s' ++ L_LUKS_STEP='Setting up LUKS2 on %s' ++ L_BTRFS_STEP='Creating Btrfs filesystem and subvolumes' ++ L_FREESAY='Install Omarchy in the %s of free space.' ++ L_CTRLC_UNENC='Press Ctrl+C for unencrypted install.' ++ L_YES_INSTALL='Yes, install' ++ L_YES_NOENC='Yes, install without encryption' ++ L_NO_CHANGE='No, change it' ++ L_CONFIRM_ON='Confirm installing on %s' ++ L_DISK_STEP='Let'\''s select where to install Omarchy...' ++ L_DISK_HEADER='Select install disk' ++ L_PT_STEP='Partition tool for %s' ++ L_PT_1='Create unallocated free space for Omarchy, then write changes and quit.' ++ L_PT_2='Do not create an Omarchy partition here — leave the target area as Free space.' ++ L_MODE_FULL='Full disk install' ++ L_MODE_FREE='Free space install (alongside existing data)' ++ L_MODE_OTHER='Choose a different disk' ++ L_MODE_STEP='Let'\''s select how to install Omarchy...' ++ L_MODE_HEADER='Select installation mode on %s' ++ L_WIPE_WARN='Everything will be overwritten. There is no recovery possible.' ++ L_WIPE_CONFIRM='Confirm overwriting %s' ++ L_TAGLINE_W=44 ++ L_HINT_W=29 ++fi ++ + # The setup form — keyboard, account, hostname, and timezone questions, plus the + # rules their answers are checked against — shared verbatim with the first-boot + # owner setup that finishes a deferred install. build-iso.sh vendors it out of +@@ -122,12 +249,12 @@ greeter() { + printf '\033[%d;1H' "$logo_row" + gum style --foreground 2 --padding "0 0 0 $PADDING_LEFT" "$(<"$LOGO_PATH")" + +- tagline="Beautiful, Modern & Opinionated Linux by DHH" +- tpad=$(((cols - ${#tagline}) / 2)); (( tpad < 0 )) && tpad=0 ++ tagline="$L_TAGLINE" ++ tpad=$(((cols - L_TAGLINE_W) / 2)); (( tpad < 0 )) && tpad=0 + printf '\033[%d;%dH%s' "$tagline_row" "$((tpad + 1))" "$tagline" + +- hint="Press Return to Start Install" +- hpad=$(((cols - ${#hint}) / 2)); (( hpad < 0 )) && hpad=0 ++ hint="$L_HINT" ++ hpad=$(((cols - L_HINT_W) / 2)); (( hpad < 0 )) && hpad=0 + printf '\033[%d;%dH\033[2m%s\033[0m' "$hint_row" "$((hpad + 1))" "$hint" + + # ColorShift the logo: a green base (indexed color 2) with a cyan accent (6) +@@ -176,9 +303,9 @@ greeter() { + } + + abort() { +- gum style "${1:-Aborted installation}" ++ gum style "${1:-$L_ABORTED}" + echo +- gum style "You can retry later by running: ./.automated_script.sh" ++ gum style "$L_RETRY" + exit 1 + } + +@@ -245,11 +372,11 @@ keyboard_form() { + confirm_prepare_for_another_owner() { + clear_logo + echo +- say "This prepares the machine for another owner." +- say --foreground 8 "The system installs now, but setup is delayed until first boot." ++ say "$L_OWNER_PREP" ++ say --foreground 8 "$L_OWNER_PREP2" + echo +- gum confirm --affirmative "Yes, prepare for another owner" --negative "No, keep setting up" \ +- "Prepare this machine for another owner?" ++ gum confirm --affirmative "$L_OWNER_YES" --negative "$L_OWNER_NO" \ ++ "$L_OWNER_CONFIRM" + } + + # The user step. Deferred-provisioning installs skip it entirely — the machine's first owner +@@ -257,7 +384,7 @@ confirm_prepare_for_another_owner() { + # operator sets nothing user-specific. + + user_form() { +- step "Let's setup your user account..." ++ step "$L_USER_STEP" + + # Each prompt reports 0 (set), OMARCHY_FORM_BACK (Esc), or OMARCHY_FORM_SIGNAL + # (Ctrl+C). Both non-zero cases unwind to user_step, which decides what they +@@ -291,18 +418,18 @@ user_step() { + fi + + # Add manual padding since gum table -p doesn't respect padding +- echo -e "Field,Value +-Username,$username +-Password,$(printf "%${#password}s" | tr ' ' '*') +-Full name,${full_name:-[Skipped]} +-Email address,${email_address:-[Skipped]} +-Hostname,$hostname +-Timezone,$timezone +-Keyboard,$keyboard" | ++ echo -e "$L_F_FIELD,$L_F_VALUE ++$L_F_USER,$username ++$L_F_PASS,$(printf "%${#password}s" | tr ' ' '*') ++$L_F_NAME,${full_name:-$L_F_SKIP} ++$L_F_EMAIL,${email_address:-$L_F_SKIP} ++$L_F_HOST,$hostname ++$L_F_TZ,$timezone ++$L_F_KB,$keyboard" | + gum table -s "," -p | sed "s/^/${PADDING_LEFT_SPACES}/" + + echo +- if gum confirm --negative "No, change it" "Does this look right?"; then ++ if gum confirm --negative "$L_SUMMARY_NO" "$L_SUMMARY_OK"; then + break + else + keyboard_form +@@ -504,11 +631,11 @@ print_dry_run_files() { + # action so users have a way to actually fix the situation. + not_enough_space() { + local available="${1:-0}" +- step "Not enough free space on $disk" +- say --foreground 1 "$disk has $(to_gb $available) of usable free space; Omarchy needs at least 32GB." +- say "Open the partition tool to free at least 32GB on $disk, then try again." ++ step "$(printf "$L_NOSPACE_STEP" "$disk")" ++ say --foreground 1 "$(printf "$L_NOSPACE_1" "$disk" "$(to_gb $available)")" ++ say "$L_NOSPACE_2" + echo +- if ! gum confirm --affirmative "Back" --negative "Open partition tool" "Return to installation mode?"; then ++ if ! gum confirm --affirmative "$L_BACK" --negative "$L_OPEN_PT" "$L_NOSPACE_CONFIRM"; then + open_partition_tool + fi + return 1 +@@ -518,14 +645,14 @@ not_enough_space() { + # encryption confirm. Sets the globals run_partition_execute needs. Returns 1 + # to fall back to the install-mode picker. + run_partition_decide() { +- step "Checking for BitLocker on $disk" ++ step "$(printf "$L_BL_STEP" "$disk")" + local bl_part + bl_part=$(detect_bitlocker || true) + if [[ -n "$bl_part" ]]; then +- say --foreground 1 "BitLocker signature detected on $bl_part." +- say "Turn BitLocker off in Windows and wait for the drive to finish decrypting, then try again." +- say "Suspending BitLocker is not enough — the drive stays encrypted." +- abort "Aborted: BitLocker is enabled on this disk." ++ say --foreground 1 "$(printf "$L_BL_1" "$bl_part")" ++ say "$L_BL_2" ++ say "$L_BL_3" ++ abort "$L_BL_ABORT" + fi + + # Omarchy always creates its own dedicated ESP in free space and never +@@ -534,17 +661,17 @@ run_partition_decide() { + # Windows ESP is far too small for our Unified Kernel Images, and a shared + # ESP forces the install unencrypted. A separate Linux ESP keeps Windows + # and Omarchy fully isolated and keeps LUKS on the table. +- step "Checking existing EFI partitions on $disk" ++ step "$(printf "$L_EFI_STEP" "$disk")" + local detected_win_esp + detected_win_esp=$(detect_windows_esp || true) + if [[ -n "$detected_win_esp" ]]; then +- say "Found a Windows ESP at $detected_win_esp — leaving it untouched." +- say "Omarchy will create its own dedicated EFI partition in free space." ++ say "$(printf "$L_EFI_WIN" "$detected_win_esp")" ++ say "$L_EFI_OWN" + else +- say "A new EFI partition will be created in free space." ++ say "$L_EFI_NEW" + fi + +- step "Analyzing free space on $disk" ++ step "$(printf "$L_FREE_STEP" "$disk")" + partprobe "$disk" 2>/dev/null || true + sleep 1 + +@@ -619,21 +746,21 @@ run_partition_decide() { + while true; do + clear_logo + echo +- say "Install Omarchy in the $(to_gb $INSTALL_MAX_B) of free space." ++ say "$(printf "$L_FREESAY" "$(to_gb $INSTALL_MAX_B)")" + + case $mode in + encrypted) +- say --foreground 8 "Press Ctrl+C for unencrypted install." +- affirmative="Yes, install" ++ say --foreground 8 "$L_CTRLC_UNENC" ++ affirmative="$L_YES_INSTALL" + ;; + unencrypted) +- affirmative="Yes, install without encryption" ++ affirmative="$L_YES_NOENC" + ;; + esac + echo + +- gum confirm --affirmative "$affirmative" --negative "No, change it" \ +- "Confirm installing on $disk" ++ gum confirm --affirmative "$affirmative" --negative "$L_NO_CHANGE" \ ++ "$(printf "$L_CONFIRM_ON" "$disk")" + confirm_status=$? + + case $confirm_status in +@@ -691,7 +818,7 @@ run_partition_execute() { + say "[dry] partition numbers are assigned by parted at creation, then read back" + say "[dry] encrypted=$encrypt_installation kernel=$kernel_choice" + else +- step "Creating partitions on $disk" ++ step "$(printf "$L_MKPART_STEP" "$disk")" + if $needs_mklabel; then + disk_step "initializing GPT on $disk" parted --script "$disk" mklabel gpt + partprobe "$disk" 2>/dev/null || true +@@ -729,7 +856,7 @@ run_partition_execute() { + disk_step "clearing stale signatures on $root_partition_device" wipefs -af "$root_partition_device" + + if [[ "$encrypt_installation" == "true" ]]; then +- step "Setting up LUKS2 on $root_partition_device" ++ step "$(printf "$L_LUKS_STEP" "$root_partition_device")" + # Kept as pipes rather than routed through disk_step: the passphrase must + # not become an argv the process table can show. Folding stderr into + # stdout still lands any error in the install log. +@@ -746,7 +873,7 @@ run_partition_execute() { + root_mapper="$root_partition_device" + fi + +- step "Creating Btrfs filesystem and subvolumes" ++ step "$L_BTRFS_STEP" + disk_step "creating the Btrfs filesystem on $root_mapper" \ + mkfs.btrfs -f -L OMARCHY "$root_mapper" + wait_for_device "$root_mapper" || disk_abort_hook "Root device $root_mapper never appeared" +@@ -862,7 +989,7 @@ _EOF_ + } + + disk_form() { +- step "Let's select where to install Omarchy..." ++ step "$L_DISK_STEP" + + # Don't offer the install media as an option (Arch ISO mounts it here) + local boot_source +@@ -888,16 +1015,16 @@ disk_form() { + fi + done <<<"$available_disks" + +- selected_display=$(echo "$disk_options" | gum choose --header "Select install disk") || abort ++ selected_display=$(echo "$disk_options" | gum choose --header "$L_DISK_HEADER") || abort + disk=$(echo "$selected_display" | awk '{print $1}') + } + + # STEP 4: INSTALL MODE + + open_partition_tool() { +- step "Partition tool for $disk" +- gum style "Create unallocated free space for Omarchy, then write changes and quit." +- gum style --foreground 8 "Do not create an Omarchy partition here — leave the target area as Free space." ++ step "$(printf "$L_PT_STEP" "$disk")" ++ gum style "$L_PT_1" ++ gum style --foreground 8 "$L_PT_2" + gum style --foreground 8 "Tip: free-space install needs at least 32GB unallocated, plus 2GB more if no ESP already exists." + echo + gum confirm --affirmative "Open cfdisk" --negative "Back" "Open partition tool for $disk?" || return 0 +@@ -933,17 +1060,17 @@ requires_full_disk_install() { + } + + install_mode_form() { +- local choices=("Full disk install") ++ local choices=("$L_MODE_FULL") + + # The free-space/protected path registers an EFI boot entry and is UEFI-only. + # Full-disk installs still support BIOS through the orchestrator's BIOS branch. + if [[ -d /sys/firmware/efi ]] && ! $full_disk_only; then +- choices+=("Free space install (alongside existing data)") ++ choices+=("$L_MODE_FREE") + fi +- choices+=("Choose a different disk") ++ choices+=("$L_MODE_OTHER") + +- step "Let's select how to install Omarchy..." +- install_mode=$(gum choose --header "Select installation mode on $disk" "${choices[@]}") || abort ++ step "$L_MODE_STEP" ++ install_mode=$(gum choose --header "$(printf "$L_MODE_HEADER" "$disk")" "${choices[@]}") || abort + } + + confirm_disk_overwrite() { +@@ -953,17 +1080,17 @@ confirm_disk_overwrite() { + while true; do + clear_logo + echo +- say "Everything will be overwritten. There is no recovery possible." ++ say "$L_WIPE_WARN" + + if [[ $mode == "encrypted" ]]; then +- say --foreground 8 "Press Ctrl+C for unencrypted install." +- affirmative="Yes, install" ++ say --foreground 8 "$L_CTRLC_UNENC" ++ affirmative="$L_YES_INSTALL" + else +- affirmative="Yes, install without encryption" ++ affirmative="$L_YES_NOENC" + fi + + echo +- gum confirm --affirmative "$affirmative" --negative "No, change it" "Confirm overwriting ${disk}" ++ gum confirm --affirmative "$affirmative" --negative "No, change it" "$(printf "$L_WIPE_CONFIRM" "${disk}")" + confirm_status=$? + + case $confirm_status in +@@ -1002,13 +1129,13 @@ select_installation() { + # With no free-space option to offer, a full-disk install is the only + # mode, so skip the picker and go straight to the overwrite confirm. + if $full_disk_only; then +- install_mode="Full disk install" ++ install_mode="$L_MODE_FULL" + else + install_mode_form + fi + + case "$install_mode" in +- "Full disk install") ++ "$L_MODE_FULL") + if confirm_disk_overwrite; then + install_target="full_disk" + return 0 +@@ -1017,13 +1144,13 @@ select_installation() { + # wants a different disk. + $full_disk_only && disk_form + ;; +- "Free space install"*) ++ "$L_MODE_FREE") + if run_partition_decide; then + install_target="free_space" + return 0 + fi + ;; +- "Choose a different disk") ++ "$L_MODE_OTHER") + disk_form + ;; + esac +diff --git a/configs/airootfs/usr/local/bin/omarchy-install-dashboard b/configs/airootfs/usr/local/bin/omarchy-install-dashboard +index 91e960b..2a615e2 100755 +--- a/configs/airootfs/usr/local/bin/omarchy-install-dashboard ++++ b/configs/airootfs/usr/local/bin/omarchy-install-dashboard +@@ -53,6 +53,44 @@ export GUM_CONFIRM_UNSELECTED_BACKGROUND="${GUM_CONFIRM_UNSELECTED_BACKGROUND:-0 + child_pid="" + child_pgid="" + ++if [[ ${OMARCHY_INSTALL_LANG:-} == zh ]]; then ++tips=( ++ "Super + Space 打开 Omarchy 菜单:应用、设置与更多" ++ "Super + K 查看全部快捷键" ++ "Super 就是键盘上的 Windows / Command 键" ++ "用 Xournal++ 给 PDF 签名" ++ "用 LocalSend 与手机、电脑互传文件" ++ "菜单 Install > Web App 可把任意网站变成应用" ++ "Super + Return 打开终端,Super + Shift + Return 打开浏览器" ++ "图片用 Pinta,视频用 Kdenlive,文档用 LibreOffice" ++ "Super + Ctrl + Print 用 OCR 抓取屏幕文字" ++ "Print 截屏,Alt + Print 录屏" ++ "Super + Ctrl + R 设置提醒" ++ "菜单 Style > Theme 一键换主题" ++ "双击顶栏可切换透明" ++ "菜单 Install > Windows 可运行完整 Windows 虚拟机" ++ "Super + Ctrl + V 打开剪贴板管理器" ++ "Super + 1 到 0 切换工作区,加 Shift 带窗口一起" ++ "Super + Print 拾取屏幕任意位置的颜色" ++ "菜单里的 Update 保持系统常新" ++) ++L_INSTALLING="正在安装 Omarchy CN" ++L_TIP="提示:" ++L_DONE_IN="安装完成,用时 %s" ++L_STOPPED="Omarchy 安装已停止" ++L_EXIT_STATUS="安装器退出,状态码 %s" ++L_ENTER="按回车继续……" ++L_REBOOT_NOW="立即重启" ++L_FAIL_HEADER="接下来做什么?" ++L_C_UPLOAD="上传日志求助" ++L_C_VIEWLOG="查看完整日志" ++L_C_SHELL="进入 Shell" ++L_C_REBOOT="重启" ++L_C_POWEROFF="关机" ++L_NO_UPLOADER="当前环境没有可用的日志上传工具。" ++L_LOG_AT="安装日志: %s" ++L_TARGET_LOG="目标系统日志: /mnt/var/log/omarchy-install.log" ++else + tips=( + "Super + Space opens the Omarchy menu for apps, settings, and more" + "Super + K shows all the key bindings" +@@ -73,6 +111,23 @@ tips=( + "Super + Print picks a color from anywhere on screen" + "Keep the system fresh with Update in the Omarchy menu" + ) ++L_INSTALLING="Installing Omarchy" ++L_TIP="Tip:" ++L_DONE_IN="Installed Omarchy in %s" ++L_STOPPED="Omarchy installation stopped" ++L_EXIT_STATUS="Installer exited with status %s" ++L_ENTER="Press Enter to continue…" ++L_REBOOT_NOW="Reboot Now" ++L_FAIL_HEADER="What would you like to do?" ++L_C_UPLOAD="Upload log for support" ++L_C_VIEWLOG="View full log" ++L_C_SHELL="Drop to shell" ++L_C_REBOOT="Reboot" ++L_C_POWEROFF="Power off" ++L_NO_UPLOADER="No log uploader is available in this environment." ++L_LOG_AT="Install log: %s" ++L_TARGET_LOG="Target log: /mnt/var/log/omarchy-install.log" ++fi + + cleanup() { + printf '%s%s' "$RESET" "$SHOW_CURSOR" >"$TTY_PATH" 2>/dev/null || true +@@ -452,13 +507,13 @@ render_dynamic() { + LAST_PM=$pm + + printf '%s%d;1H' "$CSI" "$DYNAMIC_ROW" +- center "Installing Omarchy" "$CONTENT_WIDTH" ++ center "$L_INSTALLING" "$CONTENT_WIDTH" + blank_line + line_at "$CONTENT_WIDTH" $(( (CONTENT_WIDTH - 34) / 2 )) "" + progress_bar "$pm" 34 + printf '\n' + blank_line +- center "${DIM}Tip:${RESET} ${GREEN}$(current_tip)${RESET}" "$CONTENT_WIDTH" ++ center "${DIM}${L_TIP}${RESET} ${GREEN}$(current_tip)${RESET}" "$CONTENT_WIDTH" + printf '%s' "$CLEAR_TO_END" + } + +@@ -511,7 +566,7 @@ render_finish() { + printf '%s%s%s%d;1H' "$SHOW_CURSOR" "$CLEAR" "$CSI" "$FINISH_TOP_ROW" + render_logo + blank_line +- center "Installed Omarchy in ${duration}" "$CONTENT_WIDTH" ++ center "$(printf "$L_DONE_IN" "${duration}")" "$CONTENT_WIDTH" + blank_line + } >"$TTY_PATH" + +@@ -556,7 +611,7 @@ reboot_prompt() { + --padding "0 0 0 $prompt_pad" \ + --show-help=false \ + --default \ +- --affirmative "Reboot Now" \ ++ --affirmative "$L_REBOOT_NOW" \ + --negative "" \ + "" <"$TTY_PATH" >"$TTY_PATH" 2>&1 + } +@@ -617,7 +672,7 @@ find_log_uploader() { + } + + prompt_enter() { +- printf '\nPress Enter to continue…' >"$TTY_PATH" ++ printf '\n%s' "$L_ENTER" >"$TTY_PATH" + IFS= read -r _ <"$TTY_PATH" || true + } + +@@ -628,9 +683,9 @@ upload_failure_log() { + "$uploader" install >"$TTY_PATH" 2>&1 || true + else + { +- echo "No log uploader is available in this environment." +- echo "Install log: $LOG_FILE" +- [[ -f /mnt/var/log/omarchy-install.log ]] && echo "Target log: /mnt/var/log/omarchy-install.log" ++ echo "$L_NO_UPLOADER" ++ printf "$L_LOG_AT\n" "$LOG_FILE" ++ [[ -f /mnt/var/log/omarchy-install.log ]] && echo "$L_TARGET_LOG" + } >"$TTY_PATH" + fi + prompt_enter +@@ -696,8 +751,8 @@ render_failure() { + blank_line + render_logo + blank_line +- center "${RED}Omarchy installation stopped${RESET}" "$CONTENT_WIDTH" +- center "Installer exited with status $status" "$CONTENT_WIDTH" ++ center "${RED}${L_STOPPED}${RESET}" "$CONTENT_WIDTH" ++ center "$(printf "$L_EXIT_STATUS" "$status")" "$CONTENT_WIDTH" + + render_media_diagnosis "$media_diagnosis" + +@@ -730,34 +785,34 @@ failure_menu() { + if [[ -n $uploader ]]; then + choice=$(gum choose \ + --height 6 \ +- --header "What would you like to do?" \ +- "Upload log for support" \ +- "View full log" \ +- "Drop to shell" \ +- "Reboot" \ +- "Power off" \ +- <"$TTY_PATH" 2>"$TTY_PATH") || choice="Drop to shell" ++ --header "$L_FAIL_HEADER" \ ++ "$L_C_UPLOAD" \ ++ "$L_C_VIEWLOG" \ ++ "$L_C_SHELL" \ ++ "$L_C_REBOOT" \ ++ "$L_C_POWEROFF" \ ++ <"$TTY_PATH" 2>"$TTY_PATH") || choice="$L_C_SHELL" + else + choice=$(gum choose \ + --height 5 \ +- --header "What would you like to do?" \ +- "View full log" \ +- "Drop to shell" \ +- "Reboot" \ +- "Power off" \ +- <"$TTY_PATH" 2>"$TTY_PATH") || choice="Drop to shell" ++ --header "$L_FAIL_HEADER" \ ++ "$L_C_VIEWLOG" \ ++ "$L_C_SHELL" \ ++ "$L_C_REBOOT" \ ++ "$L_C_POWEROFF" \ ++ <"$TTY_PATH" 2>"$TTY_PATH") || choice="$L_C_SHELL" + fi + + case "$choice" in +- "Upload log for support") ++ "$L_C_UPLOAD") + upload_failure_log + render_failure "${failure_status:-1}" "${failure_summary:-}" "${failure_media_diagnosis:-}" >"$TTY_PATH" 2>/dev/null || true + ;; +- "View full log") ++ "$L_C_VIEWLOG") + view_failure_log + render_failure "${failure_status:-1}" "${failure_summary:-}" "${failure_media_diagnosis:-}" >"$TTY_PATH" 2>/dev/null || true + ;; +- "Reboot") ++ "$L_C_REBOOT") + reboot 2>/dev/null || systemctl reboot 2>/dev/null || true + ;; + "Power off")