Conventions
File-size limits, structural conformity, i18n string naming, error handling, and package READMEs.
Every section below is rendered from the repository's
CONTRIBUTING.md
and
AGENTS.md
when this site is built, in the repository's own words.
File size limits
CI enforces file size limits to keep code LLM-maintainable:
- Go files: 400 lines max (excluding generated code and tests)
- Svelte components: 250 lines max
If a file is too large, split it. Every file should have a single responsibility.
Structural conformity
Before writing ANY new code, find an existing example of the same pattern and follow it exactly.
| What you’re building | Where to look |
|---|---|
| New API endpoint | internal/api/cards.go — huma.Register pattern |
| New sqlc query | internal/db/queries/cards.sql — query style |
| New job type | internal/jobs/handlers_track_edit.go — Register + handler pattern |
| New Svelte component | An existing component of similar size/purpose |
| New API client function | frontend/src/lib/api/client.ts — existing function style |
| Error handling in Go | Use Err(ctx, status, msg, err) — never raw errors |
| Error handling in Svelte | const { data, error } = await client.X() — never raw fetch |
| Icons | Phosphor duotone only — import X from 'phosphor-svelte/lib/X' |
| Spacing/layout | Match adjacent components exactly — no new patterns |
| i18n strings | Add to frontend/messages/en.json, use m.key() in components |
Never introduce a pattern that doesn’t already exist in the codebase. If you think a new pattern is needed, stop and explain why.
Internationalization (i18n)
All user-facing strings live in frontend/messages/en.json and are accessed
via Paraglide JS.
CI enforces this — scripts/check-i18n.sh fails if hardcoded English strings
are detected.
Adding a new string
- Add the key to
frontend/messages/en.json:
{
"cards_new_feature_label": "My new label"
}
- Import and use in your Svelte component:
<script>
import * as m from "$paraglide/messages";
</script>
<p>{m.cards_new_feature_label()}</p>
- For Astro pages, import from the relative path:
---
import * as m from "../paraglide/messages.js";
---
<p>{m.cards_new_feature_label()}</p>
Naming convention
Keys follow area_context_description:
| Prefix | Area |
|---|---|
nav_ |
Sidebar / navigation |
settings_ |
Settings page |
cards_ |
Card grid, selectors |
tracks_ |
Track list, detail editor |
publish_ |
Publish matrix, progress |
icons_ |
Icon generation, editor |
admin_ |
Admin panel |
llm_ |
LLM provider config |
labels_ |
Label generation |
share_ |
Share links |
devices_ |
Device management |
accounts_ |
Yoto account linking |
members_ |
Collection members |
login_ / forgot_password_ / reset_password_ |
Auth flows |
page_ |
Page-level headings and empty states |
aria_ |
Accessibility labels |
common_ |
Shared (Cancel, Save, Close, etc.) |
Parameterized messages
Use {name} placeholders for dynamic values:
{
"tracks_delete_confirm": "Delete \"{title}\"? This cannot be undone."
}
{m.tracks_delete_confirm({ title: track.title })}
What CI checks
scripts/check-i18n.sh catches:
- Svelte files with visible text but no paraglide import
- Hardcoded
placeholder="English text" - Hardcoded
aria-label="English text" - Hardcoded
alert('...')/confirm('...') - Error variables assigned hardcoded English strings
Run it locally: bash scripts/check-i18n.sh
Error handling
Use named error constants from internal/api/errors.go. Don’t invent error strings:
// YES — pick from the catalog
return nil, ErrCardNotFound
// NO — inventing a string
return nil, huma.Error404NotFound("card not found")
Package READMEs
Every internal/ package has a README.md explaining its purpose, dependencies,
and dependents. Read it before modifying a package.