Internationalization
Both apps/web and apps/admin support internationalization. The implementation is self-rolled (no react-i18next or similar library) using Zustand stores and flat TypeScript locale files.
Locale files
Web portal (apps/web)
app/locales/
├── index.ts # exports useLocaleStore and t() helper
├── en-US.ts # English translations
└── zh-CN.ts # Chinese translationsEach locale file exports a flat record of key-value pairs:
export default {
"dashboard.title": "Energy Dashboard",
"dashboard.welcome": "Welcome, {name}",
"nav.overview": "Overview",
"nav.energy": "Energy",
// ...
};Using translations
In a React component:
import { useLocaleStore } from "~/locales";
function Header() {
const { t } = useLocaleStore();
return <h1>{t("dashboard.title")}</h1>;
}Interpolation is supported:
<p>{t("dashboard.welcome", { name: user.username })}</p>Switching language
The locale store persists the user's choice in localStorage. On first visit, it detects the browser language and falls back to zh-CN if the browser prefers Chinese, otherwise en-US.
const { setLocale } = useLocaleStore();
setLocale("zh-CN"); // or "en-US"Adding a new translation key
Add the key to both
en-US.tsandzh-CN.tswith the appropriate translation.Use the key in your component via
t("your.new.key").Run the dead-code checker to ensure no orphaned keys exist:
bashuv run apps/web/scripts/check-locale-dead-code.py
Removing a translation key
- Remove all usages of the key from components.
- Remove the key from both locale files.
- Run the dead-code checker to confirm cleanliness.
Admin dashboard i18n notes
The admin dashboard does not currently use the locale system — most admin-facing text is kept in English inline. Only apps/web implements full i18n.
Locale file conventions
- Use dot notation for namespacing:
"nav.overview","form.email.placeholder". - Keep keys alphabetical within each namespace for readability.
- Do not nest objects — flat keys are easier to search and grep.
- Use sentence case for English, avoid ALL CAPS.