feat(i18n): add gettext zh_CN runtime POC - #1775
Conversation
📝 WalkthroughWalkthroughThis PR adds gettext-based runtime localization infrastructure to HyDE. Python and Bash modules provide translation utilities; a build system manages ChangesInternationalization Infrastructure and Gettext Integration
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related issues
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with 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.
Inline comments:
In `@Configs/.local/lib/hyde/i18n.sh`:
- Around line 13-14: The i18n normalization maps "zh_TW | zh_HK | zh_MO" to
"zh_TW" in Configs/.local/lib/hyde/i18n.sh but there is no corresponding
zh_TW.json in Configs/.local/share/hyde/i18n/, so Traditional Chinese
translations will be missing; fix by either adding a zh_TW.json translation file
to Configs/.local/share/hyde/i18n/ with the needed strings, or change the
normalization in i18n.sh (the case that currently returns "zh_TW") to map those
locales to an existing locale (e.g., "zh_CN") or to a documented fallback value,
and update any README/docs to state the chosen fallback behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: d17344dc-5ea0-4853-a30d-5176619dfb8a
📒 Files selected for processing (7)
CHANGELOG.mdConfigs/.local/lib/hyde/i18n.shConfigs/.local/lib/hyde/keybinds_hint.shConfigs/.local/lib/hyde/notifications.pyConfigs/.local/lib/hyde/pyutils/i18n.pyConfigs/.local/share/hyde/i18n/en.jsonConfigs/.local/share/hyde/i18n/zh_CN.json
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
Configs/.local/lib/hyde/pyutils/i18n.py (1)
43-50:⚠️ Potential issue | 🟠 Major | ⚡ Quick winBroaden exception handling to prevent crashes on invalid JSON structure or I/O errors.
The function can crash in several scenarios not covered by the current exception handling:
- Non-dict JSON root: If the JSON file contains
[],"string", or123, line 50'sdata.items()raisesAttributeError.- I/O errors:
read_text()can raiseUnicodeDecodeError(invalid UTF-8),PermissionError, or otherOSErrorvariants that aren't caught.The Bash contract (context snippet 3) checks file readability and gracefully returns empty on errors.
🛡️ Recommended fix to handle all errors gracefully
`@lru_cache`(maxsize=None) def _load(lang: str) -> dict[str, str]: path = _base_dir() / f"{lang}.json" try: data = json.loads(path.read_text(encoding="utf-8")) - except (FileNotFoundError, json.JSONDecodeError): + if not isinstance(data, dict): + return {} + except (FileNotFoundError, json.JSONDecodeError, OSError): return {} return {key: value for key, value in data.items() if isinstance(value, str)}🤖 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 43 - 50, The _load(lang: str) function should tolerate non-dict JSON roots and I/O/decoding errors; update its error handling so that any exceptions from path.read_text() (e.g., OSError, UnicodeDecodeError) and JSON parsing are caught and that the parsed data is validated as a dict before calling data.items() (e.g., check isinstance(data, dict) and return {} otherwise). In short: wrap read_text/json.loads in a broader try/except (catch OSError, UnicodeDecodeError, json.JSONDecodeError) and after parsing guard against non-dict roots (or catch AttributeError/TypeError) and return {} on any failure, leaving the `@lru_cache-decorated` _load signature intact.
🧹 Nitpick comments (1)
Configs/.local/lib/hyde/pyutils/i18n.py (1)
16-16: ⚡ Quick winDead code: empty string check is unreachable.
Line 8 converts empty string to
"en"via theoroperator before any checks, so the""case on line 16 can never match. The behavior is still correct (empty becomes"en"), but the check is confusing.♻️ Simplify by removing unreachable condition
- if lang in {"", "C", "POSIX"}: + if lang in {"C", "POSIX"}: return "en"🤖 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` at line 16, The check `if lang in {"", "C", "POSIX"}:` contains an unreachable `""` branch because `lang` is normalized earlier with `lang = lang or "en"`; remove the empty-string literal from the set so the condition becomes `if lang in {"C", "POSIX"}:` (locate the check by searching for the `lang` variable and the `if lang in` expression in i18n.py) and run tests to confirm behavior remains the same.
🤖 Prompt for all review comments with 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.
Outside diff comments:
In `@Configs/.local/lib/hyde/pyutils/i18n.py`:
- Around line 43-50: The _load(lang: str) function should tolerate non-dict JSON
roots and I/O/decoding errors; update its error handling so that any exceptions
from path.read_text() (e.g., OSError, UnicodeDecodeError) and JSON parsing are
caught and that the parsed data is validated as a dict before calling
data.items() (e.g., check isinstance(data, dict) and return {} otherwise). In
short: wrap read_text/json.loads in a broader try/except (catch OSError,
UnicodeDecodeError, json.JSONDecodeError) and after parsing guard against
non-dict roots (or catch AttributeError/TypeError) and return {} on any failure,
leaving the `@lru_cache-decorated` _load signature intact.
---
Nitpick comments:
In `@Configs/.local/lib/hyde/pyutils/i18n.py`:
- Line 16: The check `if lang in {"", "C", "POSIX"}:` contains an unreachable
`""` branch because `lang` is normalized earlier with `lang = lang or "en"`;
remove the empty-string literal from the set so the condition becomes `if lang
in {"C", "POSIX"}:` (locate the check by searching for the `lang` variable and
the `if lang in` expression in i18n.py) and run tests to confirm behavior
remains the same.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 026fcc4c-b910-4210-8aae-1cd0bf8866d4
📒 Files selected for processing (2)
Configs/.local/lib/hyde/i18n.shConfigs/.local/lib/hyde/pyutils/i18n.py
🚧 Files skipped from review as they are similar to previous changes (1)
- Configs/.local/lib/hyde/i18n.sh
|
iirc there is a standard way to do this . python uses that already and other language. and I remembered I tried making a translation for bash too. Let me check again I forgot about that template |
|
So My Idea is to have a |
|
Some of you ideas are amazing. Will try to head to that direction. |
|
That sounds good to me. Using gettext with |
493631e to
780efcd
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with 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.
Inline comments:
In `@Configs/.local/lib/hyde/pyutils/i18n.py`:
- Around line 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.
In `@Configs/.local/lib/hyde/shutils/l10n.sh`:
- Around line 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.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 4801b530-6ef8-42d5-afe3-b9e66ce5c363
📒 Files selected for processing (11)
CHANGELOG.mdConfigs/.local/lib/hyde/keybinds_hint.shConfigs/.local/lib/hyde/notifications.pyConfigs/.local/lib/hyde/pyutils/i18n.pyConfigs/.local/lib/hyde/shutils/l10n.shConfigs/.local/share/hyde/locale/zh_CN.shConfigs/.local/share/hyde/locale/zh_CN/LC_MESSAGES/hyde.moSource/locale/README.mdSource/locale/build.pySource/locale/hyde.potSource/locale/zh_CN/LC_MESSAGES/hyde.po
✅ Files skipped from review due to trivial changes (5)
- Source/locale/hyde.pot
- Configs/.local/share/hyde/locale/zh_CN.sh
- Source/locale/README.md
- Source/locale/zh_CN/LC_MESSAGES/hyde.po
- CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (2)
- Configs/.local/lib/hyde/keybinds_hint.sh
- Configs/.local/lib/hyde/notifications.py
| 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 |
There was a problem hiding this comment.
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.
| _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 | ||
| } |
There was a problem hiding this comment.
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.
|
This PR is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 10 days. |
Summary
This PR adds a small gettext-based runtime i18n proof of concept for HyDE, focused on Simplified Chinese (
zh_CN). It replaces the earlier custom JSON language-pack direction with a more standard gettext-style workflow.The scope is intentionally small: this PR establishes the foundation and demonstrates it on notifications and the keybinding hint UI only.
Changes
Source/locale/:hyde.potzh_CN/LC_MESSAGES/hyde.pobuild.pyREADME.mdConfigs/.local/share/hyde/locale/zh_CN/LC_MESSAGES/hyde.moConfigs/.local/share/hyde/locale/zh_CN.shConfigs/.local/lib/hyde/pyutils/i18n.pyConfigs/.local/lib/hyde/shutils/l10n.shConfigs/.local/lib/hyde/notifications.pyConfigs/.local/lib/hyde/keybinds_hint.shDesign notes
.pofiles are the editable translation source..mofiles are generated for Python/gettext consumers..pofile.HYDE_LANG,I18N_LANGUAGE,LC_MESSAGES,LANG, then English/original strings.Test plan
Passed locally:
Also tested with fake desktop commands:
HYDE_LANG=zh_CNmakesnotifications.pyoutput Chinese tooltip labels.HYDE_LANG=enkeeps English notification labels.HYDE_LANG=zh_CNmakeskeybinds_hint.shuse Chinese no-rofi text, Rofi labels, and repeat prompt text.HYDE_LANGtakes precedence over an existingDESKTOP_LANGvalue.I did not use full
pre-commit run --all-filesbecause the upstream baseline still includes unrelated full-repo issues such as the known broken symlink.Summary by CodeRabbit
New Features
HYDE_LANG,I18N_LANGUAGE,LC_MESSAGES, orLANGenvironment variables; falls back to English when translations are unavailable.Documentation