YotoShelf
Contribute

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

  1. Add the key to frontend/messages/en.json:
{
  "cards_new_feature_label": "My new label"
}
  1. Import and use in your Svelte component:
<script>
  import * as m from "$paraglide/messages";
</script>

<p>{m.cards_new_feature_label()}</p>
  1. 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.