-
-
Notifications
You must be signed in to change notification settings - Fork 625
feat(i18n): add gettext zh_CN runtime POC #1775
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
|
|
||
|
|
||
| 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 | ||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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 Since only 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 |
||
|
|
||
| _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 | ||
|
|
||
|
|
@@ -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 | ||
|
|
@@ -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 ;; | ||
|
|
@@ -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 | ||
| 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']=$'向下滚动:弹出历史通知' |
| 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 setsLANG=FR_FRorLANG=fr-fr, the normalization will produceFR_FRorfr_frrespectively, but gettext will look for directories with those exact names. If future locale directories follow the standardfr_FRpattern, lookups will fail for non-canonical casings.Currently this only affects hypothetical future locales, since
zh_CNis explicitly normalized. Consider normalizing all locales to lowercase language + uppercase territory to handle arbitraryLANGvalues 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