Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,19 @@ an environment value the menu would not offer (say 12 GiB on a 16 GiB Mac)
is still accepted — the menu is deliberately conservative, the launcher is a
safety floor.

## Traditional Chinese

Choose **Switch to Traditional Chinese (繁體中文)** next to **Language** on
the start menu to boot Omarchy in Traditional Chinese (`zh_TW.UTF-8`);
**Use English (Default)** switches back. The change takes effect on the next
launch.

The desktop, file manager, browser, and system dialogs are translated, and
fcitx5 adds Chewing (Bopomofo) input, reachable with `Ctrl + Space`; the US
keyboard layout remains the default input method. Omarchy's own setup wizard
and menus stay in English either way: upstream Omarchy has no translation
mechanism, and those strings are hardcoded in its shell scripts.

## Requirements

- Apple Silicon Mac (`arm64`)
Expand Down
10 changes: 10 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,16 @@ creates the account on first boot.
An Omarchy-supported `pre-refresh-pacman` hook restores them after a channel
refresh writes its x86_64 templates to `/etc`; the upstream templates remain
unchanged.
- Traditional Chinese is available to opt into from the start menu's Language
row, without changing the default session: the choice becomes the
`tryomarchy.locale=zh_TW.UTF-8` kernel argument, and a guest oneshot unit
consumes it to write `LANG` into `/etc/locale.conf` before either login
entry point starts, which is where the login shell takes it from. `zh_TW.UTF-8`
is generated alongside `en_US.UTF-8`, fcitx5 is seeded with US and Chewing
(Bopomofo) input, a fontconfig rule prefers Traditional Chinese Han glyph
variants for `zh-TW` text, and Chromium is launched with the Wayland IME
flag it needs to receive fcitx5 input at all. Leaving the row untouched
emits no kernel argument, and `LANG`/`KEYMAP` stay `en_US`/`us`.

Nothing is overwritten while the app runs. The app bundle and packaged factory
disk remain unchanged. Normal user launches use one private writable disk under
Expand Down
84 changes: 84 additions & 0 deletions guest/factory-overlay/etc/fonts/conf.d/30-try-omarchy.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "urn:fontconfig:fonts.dtd">
<!--
Noto Sans/Serif CJK is one family with different glyph shapes per
region (JP/KR/SC/TC/HK). An unqualified match can silently render
zh-TW text with Simplified or Japanese Han forms even though every
codepoint is "covered", so prefer the Traditional Chinese (TC) face
when the requested text language is zh-tw or zh-hant (the latter
isn't a fontconfig orthography code; matched via compare="contains").

Numbered 30, in fontconfig's 30-39 "family substitution" range, so
this runs before 60-69's generic-alias stage (e.g. 65-nonlatin.conf)
appends its own fallback list.
-->
<fontconfig>
<match target="pattern">
<test qual="any" name="family">
<string>sans-serif</string>
</test>
<test name="lang" compare="contains">
<string>zh-tw</string>
</test>
<edit name="family" mode="prepend" binding="strong">
<string>Noto Sans CJK TC</string>
</edit>
</match>
<match target="pattern">
<test qual="any" name="family">
<string>sans-serif</string>
</test>
<test name="lang" compare="contains">
<string>zh-hant</string>
</test>
<edit name="family" mode="prepend" binding="strong">
<string>Noto Sans CJK TC</string>
</edit>
</match>

<match target="pattern">
<test qual="any" name="family">
<string>serif</string>
</test>
<test name="lang" compare="contains">
<string>zh-tw</string>
</test>
<edit name="family" mode="prepend" binding="strong">
<string>Noto Serif CJK TC</string>
</edit>
</match>
<match target="pattern">
<test qual="any" name="family">
<string>serif</string>
</test>
<test name="lang" compare="contains">
<string>zh-hant</string>
</test>
<edit name="family" mode="prepend" binding="strong">
<string>Noto Serif CJK TC</string>
</edit>
</match>

<match target="pattern">
<test qual="any" name="family">
<string>monospace</string>
</test>
<test name="lang" compare="contains">
<string>zh-tw</string>
</test>
<edit name="family" mode="prepend" binding="strong">
<string>Noto Sans Mono CJK TC</string>
</edit>
</match>
<match target="pattern">
<test qual="any" name="family">
<string>monospace</string>
</test>
<test name="lang" compare="contains">
<string>zh-hant</string>
</test>
<edit name="family" mode="prepend" binding="strong">
<string>Noto Sans Mono CJK TC</string>
</edit>
</match>
</fontconfig>
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,12 @@ ELECTRON_OZONE_PLATFORM_HINT=wayland
MOZ_ENABLE_WAYLAND=1
OZONE_PLATFORM=wayland
QT_QPA_PLATFORM=wayland

# fcitx5 input method: XWayland clients only speak the legacy XIM
# protocol, so they still need XMODIFIERS. Do NOT set GTK_IM_MODULE or
# QT_IM_MODULE here: GTK4 and Qt6 talk to fcitx5 over text-input-v3,
# which Hyprland implements natively, and those variables are global
# to every process, so setting them would also force native Wayland
# GTK/Qt apps off text-input-v3 and onto the legacy im-module path,
# making them worse, not better.
XMODIFIERS=@im=fcitx
1 change: 1 addition & 0 deletions guest/fragments/chromium-flags-wayland-ime.append.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--enable-wayland-ime
19 changes: 19 additions & 0 deletions guest/fragments/fcitx5-profile.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[Groups/0]
Name=Default
Default Layout=us
# DefaultIM= below is inert: fcitx5 resolves the group's real default from
# Items order and rewrites this line (to chewing) on first save. What keeps
# English users unaffected is keyboard-us being Items/0, ahead of chewing —
# don't "fix" that ordering to match this line.
DefaultIM=keyboard-us

[Groups/0/Items/0]
Name=keyboard-us
Layout=

[Groups/0/Items/1]
Name=chewing
Layout=

[GroupOrder]
0=Default
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[Unit]
Description=Set /etc/locale.conf from the host-chosen UI locale
# Must finish before anything that starts a login session can read the
# system locale. sddm.service (aliased to display-manager.service once
# enabled) is the graphical entry point: omarchy-provision-owner.service,
# this project's pinned upstream first-boot unit, itself orders
# Before=display-manager.service and hands off to SDDM purely on that
# ordering once it exits -- no explicit `systemctl start`. getty@tty1.service
# is the console entry point; the same upstream unit also orders
# Before=getty@tty1.service (and briefly Conflicts= it) while it owns tty1.
# Both of those already-correct orderings are the proof that ordering
# Before= the same three units guarantees this one has run first, regardless
# of which of the two starts sooner: systemd will not start either before
# every unit ordered Before= it -- including this one -- has finished.
After=local-fs.target
Before=sddm.service display-manager.service getty@tty1.service

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/bin/try-omarchy-locale

[Install]
WantedBy=multi-user.target
55 changes: 55 additions & 0 deletions guest/native-overlay/usr/local/bin/try-omarchy-locale
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#!/bin/sh

# Write a host-chosen UI locale (tryomarchy.locale=<value> on the kernel
# command line) into /etc/locale.conf. Run by try-omarchy-locale.service,
# early enough that the value is in place before anything starts a login
# session.
#
# This used to be a system generator that wrote /run/environment.d instead,
# which works for XMODIFIERS (nothing else sets it) but not for LANG:
# Omarchy's session starts through uwsm, which reads the systemd user
# manager's environment, merges in the *login shell's* environment -- whose
# LANG comes from /etc/locale.conf via PAM -- and pushes the merge back over
# D-Bus after environment.d has already run. English always wins that race.
# A generator must never write persistent state, so this lives in a real
# (enabled) oneshot unit instead, and writes /etc/locale.conf directly.

set -eu

cmdline_path=${TRY_OMARCHY_LOCALE_CMDLINE_PATH:-/proc/cmdline}
locale_conf=${TRY_OMARCHY_LOCALE_CONF_PATH:-/etc/locale.conf}

# Refuse to write through a symlink (dangling or not). This runs as root on
# every boot, and locale_conf's target would otherwise be unchecked.
[ ! -L "$locale_conf" ] || exit 1

command_line=""
IFS= read -r command_line <"$cmdline_path" || exit 1

# Default to the image's own default locale. This is deliberate, not merely
# a placeholder: writing it even when no token is present is what makes
# switching back to English in the launcher work on a persistent VM, instead
# of leaving a stale non-English locale from an earlier boot in place.
locale=en_US.UTF-8
set -f
for argument in $command_line; do
case $argument in
tryomarchy.locale=*)
candidate=${argument#tryomarchy.locale=}
# Allowlist: only locales configure-rootfs.sh actually generates into
# /etc/locale.gen. The kernel command line is attacker-influenced in
# principle and this value is about to be written into a file PAM
# reads on every login, so anything else is ignored (keeping whatever
# is already selected) rather than trusted or shell-interpolated.
case $candidate in
en_US.UTF-8 | zh_TW.UTF-8) locale=$candidate ;;
esac
;;
esac
done

# Whole-file rewrite, not a patch: /etc/locale.conf in this image holds only
# LANG (KEYMAP lives in vconsole.conf and is never touched here), so the
# output depends only on the command line and is byte-identical on repeat
# runs with the same command line.
printf 'LANG=%s\n' "$locale" >"$locale_conf"
17 changes: 13 additions & 4 deletions guest/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,18 @@
"duktape": "2.7.0-7",
"e2fsprogs": "1.47.4-1",
"elfutils": "0.196-1",
"enchant": "2.8.21-1",
"exempi": "2.6.6-3",
"exiv2": "0.28.9-1",
"expat": "2.8.4-1",
"eza": "0.23.5-2",
"fakeroot": "1:1.37.2-3",
"fastfetch": "2.68.1-1",
"fcft": "3.3.3-1",
"fcitx5": "5.1.22-1",
"fcitx5-chewing": "5.1.13-1",
"fcitx5-gtk": "5.1.7-1",
"fcitx5-qt": "5.1.15-1",
"fd": "10.5.0-3",
"ffmpeg": "2:9.0.1-4",
"fftw": "3.3.11-1",
Expand Down Expand Up @@ -212,6 +217,7 @@
"libcdio": "2.4.0-1",
"libcdio-paranoia": "10.2+2.0.2-3",
"libcgif": "0.5.4-1",
"libchewing": "0.13.1-1",
"libcloudproviders": "0.4.1-1",
"libcolord": "1.4.8-1",
"libcue": "2.3.0-1",
Expand Down Expand Up @@ -398,8 +404,8 @@
"libzip": "1.11.4-1",
"licenses": "20240728-1",
"lilv": "0.28.0-1",
"linux-aarch64": "7.2.3-2",
"linux-aarch64-headers": "7.2.3-2",
"linux-aarch64": "7.2.4-1",
"linux-aarch64-headers": "7.2.4-1",
"linux-api-headers": "7.2-1",
"llhttp": "9.3.1-1",
"llvm-libs": "22.1.8-2",
Expand Down Expand Up @@ -436,6 +442,7 @@
"networkmanager": "1.58.1-1",
"nftables": "1:1.1.7-3",
"noto-fonts": "1:2026.09.01-1",
"noto-fonts-cjk": "20240730-1",
"noto-fonts-emoji": "1:2.051-1",
"npth": "1.8-1",
"nspr": "4.40-1",
Expand All @@ -453,7 +460,7 @@
"openssh": "10.5p1-1",
"openssl": "3.6.4-1",
"opus": "1.6.1-1",
"orc": "0.4.43-1",
"orc": "0.4.44-1",
"osinfo-db": "20260812-1",
"p11-kit": "0.26.5-1",
"pacman": "7.1.0.r9.g54d9411-2",
Expand Down Expand Up @@ -578,6 +585,7 @@
"wpa_supplicant": "2:2.12-1",
"x264": "3:0.165.r3222.b35605a-2",
"x265": "4.3-1",
"xcb-imdkit": "1.0.9-1",
"xcb-proto": "1.17.0-4",
"xcb-util": "0.4.1-2",
"xcb-util-cursor": "0.1.6-1",
Expand Down Expand Up @@ -606,6 +614,7 @@
"xvidcore": "1.3.7-4",
"xxhash": "0.8.3-1",
"xz": "5.8.3-1",
"yoga": "3.2.1-1",
"yyjson": "0.12.0-1",
"zeromq": "4.3.5-3",
"zimg": "3.0.6-2",
Expand All @@ -616,6 +625,6 @@
"zram-generator": "1.2.1-1",
"zstd": "1.5.7-3"
},
"requestedFileSha256": "addad9e5b4e8c7ce1aadd3a38d45e4abad341725edd1465ca9abd3c75f659ad5",
"requestedFileSha256": "1943b67414bfe03c3889052dfe286e49816437f3dcfacdc947bfa30db862e218",
"schemaVersion": 1
}
5 changes: 5 additions & 0 deletions guest/packages.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ e2fsprogs
eza
fakeroot
fastfetch
fcitx5
fcitx5-chewing
fcitx5-gtk
fcitx5-qt
fd
fontconfig
foot
Expand Down Expand Up @@ -41,6 +45,7 @@ nautilus
neovim
networkmanager
noto-fonts
noto-fonts-cjk
noto-fonts-emoji
omarchy-keyring
openssh
Expand Down
32 changes: 31 additions & 1 deletion guest/scripts/configure-rootfs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,22 @@ cat >"$root/etc/hosts" <<EOF
::1 localhost
127.0.1.1 $hostname
EOF
printf 'en_US.UTF-8 UTF-8\n' >"$root/etc/locale.gen"
# zh_TW is generated so Traditional Chinese is available to opt into (see the
# fcitx5 profile seeded into /etc/skel below), but LANG and KEYMAP stay
# en_US/us -- the default session must not change for a user who never
# touches the IME.
printf 'en_US.UTF-8 UTF-8\nzh_TW.UTF-8 UTF-8\n' >"$root/etc/locale.gen"
printf 'LANG=en_US.UTF-8\n' >"$root/etc/locale.conf"
printf 'KEYMAP=us\n' >"$root/etc/vconsole.conf"
# try-omarchy-locale.service rewrites LANG in /etc/locale.conf from the
# kernel command line on every boot (see the unit and its ExecStart= script
# for why this must be a real unit rather than a system generator). This
# script runs before arch-chroot, with no systemd/D-Bus available to run
# `systemctl enable`, so it links the unit into multi-user.target.wants
# itself -- the same symlink that command would create.
mkdir -p "$root/etc/systemd/system/multi-user.target.wants"
ln -sfn /usr/lib/systemd/system/try-omarchy-locale.service \
"$root/etc/systemd/system/multi-user.target.wants/try-omarchy-locale.service"
# An unprovisioned machine receives a new identity from systemd on first boot.
: >"$root/etc/machine-id"
ln -sfn /usr/share/zoneinfo/UTC "$root/etc/localtime"
Expand All @@ -165,6 +178,23 @@ install -d -m 0755 "$root/etc/skel/.config/omarchy/hooks/pre-refresh-pacman.d"
install -m 0755 "$refresh_hook" \
"$root/etc/skel/.config/omarchy/hooks/pre-refresh-pacman.d/restore-arm-pacman"

# Seed fcitx5 with the US keyboard as item 0 and Chewing (Bopomofo/zhuyin) as
# item 1. Ctrl+Space is fcitx5's built-in default trigger, so a user who never
# presses it stays on plain US input; pressing it reaches Traditional Chinese.
fcitx5_profile="$guest_dir/fragments/fcitx5-profile.ini"
[[ -f $fcitx5_profile ]] || fail "fcitx5 profile fragment not found: $fcitx5_profile"
install -d -m 0755 "$root/etc/skel/.config/fcitx5"
install -m 0644 "$fcitx5_profile" \
"$root/etc/skel/.config/fcitx5/profile"

# Chromium's Ozone/Wayland backend drops input-method text unless launched
# with --enable-wayland-ime, so fcitx5 can't reach it otherwise. Basecamp's
# chromium-flags.conf is already materialized into /etc/skel, so append
# rather than replace it to keep the upstream ozone/extension flags intact.
chromium_flags="$root/etc/skel/.config/chromium-flags.conf"
[[ -f $chromium_flags ]] || fail "expected upstream chromium-flags.conf not found: $chromium_flags"
cat "$guest_dir/fragments/chromium-flags-wayland-ime.append.conf" >>"$chromium_flags"

provision_unit="$root/usr/share/omarchy/install/provisioning/omarchy-provision-owner.service"
[[ -f $provision_unit ]] || fail "pinned upstream owner-provisioning service is missing"
mkdir -p "$root/etc/systemd/system" "$root/var/lib/omarchy/provisioning"
Expand Down
1 change: 1 addition & 0 deletions guest/spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,7 @@
"bin/omarchy-provision-user",
"bin/omarchy-screensaver",
"bin/omarchy-theme-set",
"config/chromium-flags.conf",
"config/foot/foot.ini",
"config/hypr/hyprland.lua",
"config/hypr/monitors.lua",
Expand Down
Loading