Conventions
Single source of truth for code naming and the i18n translation glossary.
This page is the single source of truth for code naming and the i18n translation glossary. Anything that used to live in packages/views/locales/glossary.md or in scattered comments now lives here.
If you write Multica code or change a translation, this is the page to reference.
1. Code naming
Routes
Pre-workspace routes (the routes that exist before the user is in a workspace) MUST use either a single word or the /{noun}/{verb} pattern.
- ✅
/login,/inbox,/workspaces/new - ❌
/new-workspace,/create-team,/accept-invite
Hyphenated word groups at the root collide with user-chosen workspace slugs and force endless reserved-slug audits. Reserving the noun (workspaces) automatically protects the entire /workspaces/* subtree.
Workspace-scoped routes
Always live under /{slug}/{section} — /{slug}/issues, /{slug}/agents, /{slug}/settings. Never duplicate workspace routing logic; use useNavigation().push() from shared code, never framework-specific link APIs.
Packages and modules
The monorepo enforces strict package boundaries:
| Package | May depend on | Must NOT depend on |
|---|---|---|
packages/core | nothing app-specific | react-dom, localStorage, process.env, next/*, UI libraries |
packages/ui | nothing | @multica/core, business logic |
packages/views | core/, ui/ | next/*, react-router-dom, stores |
apps/web/platform/ | next/* | other apps |
apps/desktop/.../platform/ | react-router-dom, electron | other apps |
If logic appears in both apps, it MUST be extracted to a shared package. There are no exceptions for "small" duplication.
Files and components
- Files:
kebab-case.tsx/kebab-case.ts(e.g.agent-row-actions.tsx) - Components:
PascalCase(e.g.AgentRowActions) - Hooks:
useCamelCase(e.g.useWorkspaceId) - Tests: colocated as
<file>.test.ts(x) - Stores (Zustand):
<feature>-store.ts, exported asuse<Feature>Store
Database (Go + sqlc)
- Tables:
snake_casesingular (user,workspace,agent_runtime) - Columns:
snake_case(workspace_id,created_at,last_seen_at) - Foreign keys:
<table>_id - Booleans:
is_<state>or<state>_at(timestamp form preferred for state changes) - Migration files:
NNN_descriptive_name.up.sql+.down.sql— always provide both directions
Go
- Standard
gofmt+go vet. No exceptions. - Handler files mirror domain:
agent.go,auth.go,runtime.go - Tests:
<file>_test.gocolocated - For UUID parsing in handlers, follow the rule in the root
CLAUDE.md—parseUUIDOrBadRequestfor boundary input,parseUUID(panicking) for trusted round-trips, neverutil.ParseUUIDdirectly without checking the error.
TypeScript
- API responses on the wire are
snake_case; the api client converts tocamelCaseat the boundary. Inside TS code, always camelCase. - Types:
PascalCase(Issue,AgentRuntime); neverIPrefix, never_tsuffix. - Enums: prefer string literal unions; reserve
enumfor runtime-iterable cases. - TanStack Query keys: factory functions in
<feature>/queries.ts, e.g.issueKeys.detail(id).
Issue keys
Every issue has a human-readable key like MUL-123: workspace issue_prefix (uppercase letters and digits, typically 3 chars, max 10) + sequence number. Workspace admins can change the prefix in Settings → General; changing it renumbers every existing issue, so external references that embed the old prefix (PR titles, branch names, links in docs and chat) stop resolving.
Comments in code
English only. The repo enforces this for both Go and TypeScript. If you find a Chinese comment in code, it's a bug — replace it.
Commit messages
Conventional format: feat(scope), fix(scope), refactor(scope), docs, test(scope), chore(scope). Atomic commits grouped by intent.
2. i18n translation glossary
This is the mandatory glossary for every translation PR. It used to live at packages/views/locales/glossary.md; that file is now a stub pointing here.
Don't translate — brands and acronyms
| Category | Terms |
|---|---|
| Brands | Multica, GitHub, Slack, Google, Anthropic, OpenAI, Claude, Codex, Cursor, Linear, Jira |
| Acronyms | API, CLI, URL, SDK, OAuth, JWT, SSO, WebSocket, HTTP, JSON, YAML, SQL |
Plurals and counts
i18next uses _one / _other.
// en/issues.json
{
"issue_count_one": "{{count}} issue",
"issue_count_other": "{{count}} issues"
}Interpolation
Use {{var}}.
// en
{ "welcome_message": "Welcome back, {{name}}!" }Translation key naming
Three-level nesting: feature.component.action.
{
"feature_or_component": {
"subcomponent_or_section": {
"action_or_label": "..."
}
}
}Examples:
issues.toolbar.batch_update_successissues.detail.comment_form.placeholderinbox.empty.titlesettings.preferences.language.title
Web-only / desktop-only copy
- Shared copy: top level of the namespace JSON
- Web-only:
websection - Desktop-only:
desktopsection
See auth.json for the canonical example (the web section contains prefer_desktop / desktop_handoff.*).
Updating this page
If you change a rule here, also:
- Apply it in the relevant locale JSONs / CLAUDE.md / docs page
- Note the change in the PR description so reviewers know to look for downstream sweep
This page is the contract; nothing else overrides it.