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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
- Package Manager: Added --no-confirm flag to 'hyde-shell pm.py' commands.
- Lua: Added 'hyde-shell luainit' to initialize the Lua runtime. It is slow and should be optional for now.
- Python: 'hyde-shell pyinit' will now sync like pip to preserve user packages.
- i18n: Added a gettext-based zh_CN runtime localization POC for notifications and keybinding hints.

## v26.4.3 | 3rd week of April 2026 Release!

Expand Down
29 changes: 22 additions & 7 deletions Configs/.local/lib/hyde/keybinds_hint.sh
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
#!/usr/bin/env bash
pkill -x rofi && exit
[[ $HYDE_SHELL_INIT -ne 1 ]] && eval "$(hyde-shell init)"
# shellcheck source=/dev/null
source "${LIB_DIR}/hyde/shutils/l10n.sh"
confDir="${XDG_CONFIG_HOME:-$HOME/.config}"
keyconfDir="$confDir/hypr"
kb_hint_conf=("$keyconfDir/hyprland.conf" "$keyconfDir/keybindings.conf" "$keyconfDir/userprefs.conf")
kb_hint_conf+=("${ROFI_KEYBIND_HINT_CONFIG[@]}")
kb_cache="$XDG_RUNTIME_DIR/hyde/keybinds_hint.rofi"

update_keybind_cache() {
"${LIB_DIR}/hyde/keybinds/hint-hyprland.py" --format rofi > "$kb_cache" && {
hyde_gettext "Keybind cache updated"
printf '\n'
}
}

[ -f "$kb_cache" ] && {
trap '${LIB_DIR}/hyde/keybinds/hint-hyprland.py --format rofi > "$kb_cache" && echo "Keybind cache updated" ' EXIT
trap update_keybind_cache EXIT
}
output="$(if
! cat "$kb_cache" 2> /dev/null
Expand All @@ -16,12 +26,13 @@ then
fi)"
wait
if [ -z "$output" ]; then
notify-send "Keybind Hint" "Initialization failed."
notify-send "$(hyde_gettext "Keybind Hint")" "$(hyde_gettext "Initialization failed.")"
exit 0
fi
if ! command -v rofi &> /dev/null; then
echo "$output"
echo "rofi not detected. Displaying on terminal instead"
hyde_gettext "rofi not detected. Displaying on terminal instead"
printf '\n'
exit 0
fi
hypr_border=${hypr_border:-$(hyprctl -j getoption decoration:rounding | jq '.int')}
Expand All @@ -43,9 +54,12 @@ font_name=${font_name:-$(get_hyprConf "FONT")}
font_override="* {font: \"${font_name:-"JetBrainsMono Nerd Font"} $font_scale\";}"
icon_override=$(gsettings get org.gnome.desktop.interface icon-theme | sed "s/'//g")
icon_override="configuration {icon-theme: \"$icon_override\";}"
keybindings_label="$(hyde_gettext "Keybindings")"
keybinds_label="$(hyde_gettext "Keybinds")"
description_label="$(hyde_gettext "Description")"
selected=$(echo -e "$output" | rofi -dmenu -p \
-theme-str "entry { placeholder: \"\t⌨️ Keybindings \";}" \
" Keybinds \t\tﴕ Description" \
-theme-str "entry { placeholder: \"\t⌨️ ${keybindings_label} \";}" \
" ${keybinds_label} \t\tﴕ ${description_label}" \
-p -i \
-display-columns 1 \
-display-column-separator ":::" \
Expand All @@ -63,8 +77,9 @@ RUN() {
if [ -n "$dispatch" ] && [ "$(echo "$dispatch" | wc -l)" -eq 1 ]; then
if [ "$repeat" = repeat ]; then
while true; do
repeat_command=$(echo -e "Repeat" | rofi -dmenu -no-custom -p - "[Enter] repeat; [ESC] exit" -theme "notification")
if [ "$repeat_command" = "Repeat" ]; then
repeat_label="$(hyde_gettext "Repeat")"
repeat_command=$(echo -e "$repeat_label" | rofi -dmenu -no-custom -p - "$(hyde_gettext "[Enter] repeat; [ESC] exit")" -theme "notification")
if [ "$repeat_command" = "$repeat_label" ]; then
RUN
else
exit 0
Expand Down
14 changes: 8 additions & 6 deletions Configs/.local/lib/hyde/notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import json
import sys

from pyutils.i18n import t


def get_dunst_history():
result = subprocess.run(["dunstctl", "history"], stdout=subprocess.PIPE, check=True)
Expand All @@ -15,11 +17,11 @@ def format_history(history):
count = len(history["data"][0])
alt = "none"
tooltip_click = []
tooltip_click.append("󰎟 Notifications")
tooltip_click.append("󰳽 scroll-down:  history pop")
tooltip_click.append("󰳽 click-left:  Enable & Disable DND")
tooltip_click.append("󰳽 click-middle: 󰛌 clear history")
tooltip_click.append("󰳽 click-right: 󱄊 close all")
tooltip_click.append("󰎟 " + t("Notifications"))
tooltip_click.append("󰳽 " + t("scroll-down: history pop", "scroll-down:  history pop"))
tooltip_click.append("󰳽 " + t("click-left: Enable & Disable DND", "click-left:  Enable & Disable DND"))
tooltip_click.append("󰳽 " + t("click-middle: clear history", "click-middle: 󰛌 clear history"))
tooltip_click.append("󰳽 " + t("click-right: close all", "click-right: 󱄊 close all"))

tooltip = []

Expand Down Expand Up @@ -51,7 +53,7 @@ def format_history(history):
def main():
history = get_dunst_history()
formatted_history = format_history(history)
sys.stdout.write(json.dumps(formatted_history) + "\n")
sys.stdout.write(json.dumps(formatted_history, ensure_ascii=False) + "\n")
sys.stdout.flush()


Expand Down
63 changes: 63 additions & 0 deletions Configs/.local/lib/hyde/pyutils/i18n.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import gettext
import os
from functools import lru_cache
from pathlib import Path

DOMAIN = "hyde"


def _normalize_locale(lang: str | None) -> str:
lang = (lang or "en").split(":", 1)[0].split(".", 1)[0].split("@", 1)[0]
lang = lang.replace("-", "_")
lowered = lang.lower()

if lowered in {"", "c", "posix"}:
return "en"
if lowered in {"zh", "zh_cn", "zh_sg"}:
return "zh_CN"
return lang
Comment on lines +9 to +18

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Locale normalization doesn't standardize case for non-special-cased locales.

The function preserves the original casing after basic transformations (strip modifiers, replace hyphens) for all locales except the hardcoded special cases. Standard locale identifiers use lowercase language codes and uppercase territory codes (e.g., fr_FR, de_DE). If a user sets LANG=FR_FR or LANG=fr-fr, the normalization will produce FR_FR or fr_fr respectively, but gettext will look for directories with those exact names. If future locale directories follow the standard fr_FR pattern, lookups will fail for non-canonical casings.

Currently this only affects hypothetical future locales, since zh_CN is explicitly normalized. Consider normalizing all locales to lowercase language + uppercase territory to handle arbitrary LANG values robustly.

Suggested fix to normalize case consistently
 def _normalize_locale(lang: str | None) -> str:
     lang = (lang or "en").split(":", 1)[0].split(".", 1)[0].split("@", 1)[0]
     lang = lang.replace("-", "_")
-    lowered = lang.lower()
+    parts = lang.split("_", 1)
+    normalized = parts[0].lower() + ("_" + parts[1].upper() if len(parts) > 1 else "")

-    if lowered in {"", "c", "posix"}:
+    if normalized in {"", "c", "posix"}:
         return "en"
-    if lowered in {"zh", "zh_cn", "zh_sg"}:
+    if normalized in {"zh", "zh_CN", "zh_SG"}:
         return "zh_CN"
-    return lang
+    return normalized
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Configs/.local/lib/hyde/pyutils/i18n.py` around lines 9 - 18, The
_normalize_locale function currently preserves original casing for most locales;
update it to produce canonical casing (lowercase language, uppercase territory)
after stripping modifiers and replacing hyphens with underscores. In
_normalize_locale, after computing lang = ... and lang = lang.replace("-", "_"),
split lang on "_" into parts; if there are two parts (language and territory)
return f"{parts[0].lower()}_{parts[1].upper()}"; if there is one part return
parts[0].lower(); keep the existing special-case returns for "", "c", "posix" ->
"en" and the zh variants -> "zh_CN", and ensure you still return lang (now
normalized) at the end.



def _language() -> str:
return _normalize_locale(
os.getenv("HYDE_LANG")
or os.getenv("I18N_LANGUAGE")
or os.getenv("LC_MESSAGES")
or os.getenv("LANG")
or "en"
)


def _locale_dir() -> Path:
share_dir = os.getenv("SHARE_DIR")
if share_dir:
return Path(share_dir) / "hyde" / "locale"

return Path(os.getenv("XDG_DATA_HOME", Path.home() / ".local/share")) / "hyde" / "locale"


@lru_cache(maxsize=None)
def _translation(lang: str) -> gettext.NullTranslations:
try:
return gettext.translation(
DOMAIN,
localedir=str(_locale_dir()),
languages=[lang],
fallback=True,
)
except Exception:
return gettext.NullTranslations()


def t(message: str, default: str | None = None, *args: object) -> str:
translated = _translation(_language()).gettext(message)
if translated == message and default is not None:
translated = default

if not args:
return translated

try:
return translated % args
except (TypeError, ValueError):
return translated
74 changes: 58 additions & 16 deletions Configs/.local/lib/hyde/shutils/l10n.sh
Original file line number Diff line number Diff line change
@@ -1,17 +1,57 @@
#!/usr/bin/env bash
# Source this file in any script that needs localization support. It sets up the _T associative array with translations based on the user's locale.
# Source this file in scripts that need lightweight gettext-style localization.

# Extract the system locale and set DESKTOP_LANG to the first two characters (language code)
_raw_sys_lang="${LC_ALL:-${LANG:-en}}"
export DESKTOP_LANG="${DESKTOP_LANG:-${_raw_sys_lang:0:2}}"
export DESKTOP_LANG="${DESKTOP_LANG,,}"
#? Handles edge cases where locale is set to "C" or "POSIX" which are not actual languages
[[ "$DESKTOP_LANG" == "c" || "$DESKTOP_LANG" == "po" ]] && export DESKTOP_LANG="en"
_hyde_l10n_normalize_locale() {
local lang="${1:-${HYDE_LANG:-${I18N_LANGUAGE:-${LC_MESSAGES:-${LANG:-en}}}}}"
lang="${lang%%:*}"
lang="${lang%%.*}"
lang="${lang%%@*}"
lang="${lang//-/_}"

# Initialize the _T associative array for translations
declare -A _T 2>/dev/null || : # Localization support
[[ -f "${XDG_DATA_HOME:-$HOME/.local/share}/hyde/locale/${DESKTOP_LANG}.sh" ]] && source "${XDG_DATA_HOME:-$HOME/.local/share}/hyde/locale/${DESKTOP_LANG}.sh"
[[ -f "${XDG_CONFIG_HOME:-$HOME/.config}/hyde/locale/${DESKTOP_LANG}.sh" ]] && source "${XDG_CONFIG_HOME:-$HOME/.config}/hyde/locale/${DESKTOP_LANG}.sh"
case "${lang,,}" in
"" | c | posix)
printf '%s\n' "en"
;;
zh | zh_cn | zh_sg)
printf '%s\n' "zh_CN"
;;
*)
printf '%s\n' "$lang"
;;
esac
}
Comment on lines +4 to +22

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Locale normalization doesn't standardize case for non-special-cased locales.

This Bash function has the same case-preservation behavior as the Python version in i18n.py. The case "${lang,,}" statement lowercases for comparison, but line 19 returns the original $lang for non-special-cased locales. If a user sets LANG=FR_FR or LANG=fr-fr, the function will return FR_FR or fr_fr, but the locale map files are expected to match the exact casing of the source directory structure.

Since only zh_CN is currently special-cased to always return "zh_CN", this works today. Future locales should either be special-cased or the normalization should enforce lowercase language + uppercase territory for consistency.

Suggested fix to normalize case consistently
     case "${lang,,}" in
         "" | c | posix)
             printf '%s\n' "en"
             ;;
         zh | zh_cn | zh_sg)
             printf '%s\n' "zh_CN"
             ;;
         *)
-            printf '%s\n' "$lang"
+            # Normalize to lowercase language + uppercase territory
+            if [[ "$lang" =~ ^([a-zA-Z]+)_([a-zA-Z]+)$ ]]; then
+                printf '%s\n' "${BASH_REMATCH[1],,}_${BASH_REMATCH[2]^^}"
+            else
+                printf '%s\n' "${lang,,}"
+            fi
             ;;
     esac
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Configs/.local/lib/hyde/shutils/l10n.sh` around lines 4 - 22, The
_hyde_l10n_normalize_locale function currently returns the original $lang for
non-special cases which preserves user casing; change the default normalization
to produce a consistent format of lowercase language and uppercase territory
with an underscore separator (e.g., fr_FR): transform $lang to use '_' not '-',
split on '_' (or '-') into language and territory, lowercase the language token,
uppercase the territory token if present, then printf the reconstructed value;
keep the special-cased zh/zh_CN behavior and only apply this normalization in
the default (*) branch so functions like _hyde_l10n_normalize_locale always
return a predictable casing.


_hyde_l10n_locale_dir() {
if [[ -n "${SHARE_DIR:-}" ]]; then
printf '%s\n' "${SHARE_DIR%/}/hyde/locale"
return
fi

printf '%s\n' "${XDG_DATA_HOME:-$HOME/.local/share}/hyde/locale"
}

_hyde_l10n_source_map() {
local map_file="$1"
[[ -r "$map_file" ]] || return 0
# shellcheck source=/dev/null
source "$map_file"
}

DESKTOP_LANG="$(_hyde_l10n_normalize_locale)"
export DESKTOP_LANG

declare -gA _T 2>/dev/null || declare -A _T 2>/dev/null || :
_hyde_l10n_source_map "$(_hyde_l10n_locale_dir)/${DESKTOP_LANG}.sh"
_hyde_l10n_source_map "${XDG_CONFIG_HOME:-$HOME/.config}/hyde/locale/${DESKTOP_LANG}.sh"

hyde_gettext() {
local msg="${1:-}"
if [[ -n "$msg" && -n "${_T[$msg]+_}" ]]; then
printf '%s' "${_T[$msg]}"
return
fi
printf '%s' "$msg"
}

# method overrides for localization

Expand All @@ -21,7 +61,7 @@ send_notifs() {
for arg in "$@"; do
# If it's not a flag (starts with -), try to translate it
if [[ ! "$arg" =~ ^- ]]; then
args+=("${_T[$arg]:-$arg}")
args+=("$(hyde_gettext "$arg")")
else
args+=("$arg")
fi
Expand All @@ -35,7 +75,8 @@ print_log_L() {
case "$1" in
-r | +r | -g | +g | -y | +y | -b | +b | -m | +m | -c | +c | -wt | +w | -n | +n | -stat | -crit | -warn | -sec | -err)
# $2 is the message. Translate it or use original.
local msg="${_T[$2]:-$2}"
local msg
msg="$(hyde_gettext "$2")"
case "$1" in
-r | +r) echo -ne "\e[31m$msg\e[0m" >&2 ;;
-g | +g) echo -ne "\e[32m$msg\e[0m" >&2 ;;
Expand All @@ -55,18 +96,19 @@ print_log_L() {
;;
+)
# Custom color: $3 is the message
local msg="${_T[$3]:-$3}"
local msg
msg="$(hyde_gettext "$3")"
echo -ne "\e[38;5;$2m$msg\e[0m" >&2
shift 3
;;
*)
# Standard text
echo -ne "${_T[$1]:-$1}" >&2
echo -ne "$(hyde_gettext "$1")" >&2
shift
;;
esac
done
echo "" >&2
}

export -f send_notifs print_log_L
export -f hyde_gettext send_notifs print_log_L
18 changes: 18 additions & 0 deletions Configs/.local/share/hyde/locale/zh_CN.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/usr/bin/env bash
# Generated by Source/locale/build.py; do not edit manually.
# shellcheck shell=bash disable=SC2034
declare -gA _T 2>/dev/null || declare -A _T 2>/dev/null || :
_T[$'Description']=$'说明'
_T[$'Initialization failed.']=$'初始化失败。'
_T[$'Keybind Hint']=$'快捷键提示'
_T[$'Keybind cache updated']=$'快捷键缓存已更新'
_T[$'Keybindings']=$'快捷键'
_T[$'Keybinds']=$'快捷键'
_T[$'Notifications']=$'通知'
_T[$'Repeat']=$'重复执行'
_T[$'[Enter] repeat; [ESC] exit']=$'[Enter] 重复;[ESC] 退出'
_T[$'click-left: Enable & Disable DND']=$'左键:启用/关闭免打扰'
_T[$'click-middle: clear history']=$'中键:清空历史记录'
_T[$'click-right: close all']=$'右键:关闭全部通知'
_T[$'rofi not detected. Displaying on terminal instead']=$'未检测到 rofi,改为在终端中显示'
_T[$'scroll-down: history pop']=$'向下滚动:弹出历史通知'
Binary file not shown.
43 changes: 43 additions & 0 deletions Source/locale/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# HyDE Runtime Localization

HyDE uses gettext `.po` files as the editable source for runtime translations.
Generated `.mo` files are used by Python and other gettext consumers, while Bash
loads a small generated associative-array map from the same `.po` file.

## Language selection

Force Simplified Chinese:

```sh
HYDE_LANG=zh_CN hyde-shell keybinds_hint
```

Force English:

```sh
HYDE_LANG=en hyde-shell keybinds_hint
```

Language resolution checks `HYDE_LANG`, `I18N_LANGUAGE`, `LC_MESSAGES`, and
`LANG`, then falls back to English/original strings.

## Regenerating files

After editing `Source/locale/zh_CN/LC_MESSAGES/hyde.po`, regenerate runtime
translation files:

```sh
python3 Source/locale/build.py
```

The script uses standard gettext tools when available:

- `xgettext` updates `Source/locale/hyde.pot`
- `msgmerge` updates `.po` files from the template
- `msgfmt` compiles `.po` files into `.mo`

It also generates Bash maps such as
`Configs/.local/share/hyde/locale/zh_CN.sh`.

Missing translation files or missing keys safely fall back to the original
English strings.
Loading
Loading