localization
GitHub管理多语言架构与流程,涵盖目录结构、解析优先级及RTL支持。用于添加/修改用户界面字符串、新增语言包或处理本地化错误,确保前后端翻译一致性与正确显示。
Trigger Scenarios
Install
npx skills add bagofwords1/bagofwords --skill localization -g -y
SKILL.md
Frontmatter
{
"name": "localization",
"description": "The locale\/i18n architecture of bagofwords — catalogs, resolution order, RTL, backend contracts — and the procedures for adding strings, adding a locale, or translating UI. Use when adding\/changing user-facing strings, working on RTL, emails, localized errors, or anything locale-related."
}
Localization — architecture & procedures
Authoritative deep-dive: docs/design/i18n.md. This skill is the working map.
Locales
Catalogs live at the repo root: locales/{en,es,he,fr,sv,ar,ru,de,pt,it}.json
(10 locales). en is the authoritative catalog — its key set and ordering
define the shape; the target is identical key structure in every catalog.
es and he are the actively maintained translations; the other seven are
known to lag behind en (run the sync check below to see current drift).
he is the RTL reference — anything RTL must work for Hebrew; ar is also
RTL (plugin whitelists he/ar/fa/ur).
Hebrew vocabulary conventions: "הנחיות" (never "הוראות"), "גלריה" (never "לוח מחוונים"), "בדיקות איכות" (never "הערכות").
Resolution order (highest wins)
X-Localerequest header — test-only override; must be inenabled_locales.- Per-user choice —
localStorage["bow.locale"]on the client. - Organization locale —
OrganizationSettings.config["locale"], set via/settings/general, served byGET/PUT /api/organization/locale(PUT gated bymanage_settings). - System default —
bow_config.i18n.default_locale(en). Enabled set:settings.bow_config.i18n.enabled_locales(backend/app/settings/bow_config.py).
Architecture map
Frontend (vue-i18n@9, composition mode):
frontend/plugins/i18n.ts— creates the global instance, imports catalogs, applies persistedbow.locale, exposes$setLocale, sets<html lang>and<html dir>.frontend/layouts/default.vue— after session ready, fetchesGET /api/organization/localeand applieseffective_localeonly when no personal override exists.frontend/composables/useErrorMessage.ts— maps backenderror_code+paramstot('errors.<code>', params), falling back to serverdetail.- RTL: Tailwind logical properties only (
ms-*,me-*,ps-*,pe-*,start-*,end-*— neverml-*/pl-*/left-*). Global icon flips and third-party overrides infrontend/assets/css/rtl.css; opt out per element withrtl-no-flip. Neverdir="auto"on empty contenteditable — bind:dirto the active locale.
Backend:
- Dependencies:
get_current_locale(request)(unauthed-safe, header→default),get_org_locale(request, organization)(header→org→default),_locale_from_org(organization)for services holding an org object. - Typed errors (
backend/app/errors/): raiseAppError.*(ErrorCode.X, ...), never bareHTTPException. New code = enum entry inapp/errors/codes.py+ a matchingerrors.<code>key in every locale catalog. - Emails:
app/services/email_renderer.py+email_strings.py, shared Jinja templates inapp/templates/emails/*.jinja2honoringlang/dir. Keep substitutions HTML-escaped —descriptioninshare.html.jinja2is| safe. - LLM prompts:
app/ai/prompt_language.pyinjects a "mirror the user's language" directive for conversational agents only (planner/answer/judge/ reporter/suggest_instructions) — reply in the language of the user's latest message, overriding tool/page/schema/instruction languages; org locale is the ambiguous-message fallback. Always emitted, including theendefault. Code/artifact agents stay English so SQL, identifiers, and JSON fields never get translated. - Public boot config:
GET /api/config/i18n.
Procedures
Adding a string: add the key to locales/en.json under the right
namespace, then add the same key path to all other catalogs in one pass.
Use named interpolation (t('key', { name })) — never concatenation; word
order varies by language. Locale-reactive label/option arrays in
<script setup> must be wrapped in computed(() => [...]).
Run the sync check before and after your change — the catalogs have known
pre-existing drift, so the rule is: your diff must not increase any
missing/extra count (and should reduce them where cheap):
python3 - <<'EOF'
import json, pathlib
def flatten(d, p=''):
out = set()
for k, v in d.items():
f = f'{p}.{k}' if p else k
out |= flatten(v, f) if isinstance(v, dict) else {f}
return out
cats = {f.stem: flatten(json.loads(f.read_text())) for f in pathlib.Path('locales').glob('*.json')}
en = cats.pop('en')
for loc, keys in sorted(cats.items()):
missing, extra = en - keys, keys - en
print(f'{loc}: missing={len(missing)} extra={len(extra)}')
for k in sorted(missing)[:10]: print(f' - {k}')
print(f'en: {len(en)} keys')
EOF
Adding a locale: create locales/<code>.json mirroring en.json's full
shape; add the code to enabled_locales in backend/app/settings/bow_config.py
and the import in frontend/plugins/i18n.ts; if RTL, add it to the plugin's
RTL whitelist; extend frontend/tests/i18n/locale-sweep.spec.ts.
Testing: cd frontend && npx playwright test --config=playwright.i18n.config.ts
runs the locale sweep (asserts html[lang]/html[dir] flip, strings render,
no {{…}}/unresolved key paths leak, no [intlify] console warnings). Keep it
green for any catalog or locale change. For RTL-visible changes, capture a
Hebrew screenshot per the ui-evidence skill.
Pitfalls that recur
- Adding a key to
en.jsononly → sync check fails for the other 9 catalogs. - Hard-coded literals in toasts/
confirm()/script-side strings — every user-facing string comes from the catalog. - Keying CSS-class maps on localized labels — key on canonical identifiers.
- Hand-formatted dates/numbers — always
Intl.*withlocale.value. - Physical CSS properties sneaking in via copy-paste — logical only.
Version History
- 1529fca Current 2026-08-20 15:41


