This project is actively evolving. Contributions are welcome, but we have a few rules to keep releases stable and avoid regressions (especially around audio output).
- Write clear, concise English (no emojis in code, comments, or commit messages).
- Keep PRs focused and small when possible.
- Do not change app branding or legal disclaimers without discussing it first.
- Do not modify protected audio-backend behavior unless explicitly requested by the maintainer.
We use a consistent branch naming scheme:
<type>/<origin>/<branch_name>
type:feature|bugfix|hotfix|refactor|release|chore|docsorigin:internal: created/owned by maintainersexternal: branches/commits authored by third-party contributors (PRs)
Examples:
feature/internal/offline-cache-encryptionbugfix/internal/login-footer-alignmentdocs/internal/contributing-processfeature/external/add-album-to-playlist
We use a pre-release integration branch to keep main stable and release-ready at all times.
feature/xyz ──┐
bugfix/abc ──┼──> pre-release ──> main (tagged release)
hotfix/123 ──┘
main- Releases ONLY. Protected branch. Merging here triggers a tagged release.pre-release- Integration branch. All features and fixes merge here first.feature/*,bugfix/*, etc. - Individual work branches.
All PRs must target pre-release, not main.
PRs targeting main will be closed and asked to retarget to pre-release.
- Triage
- Confirm scope and that it does not touch protected areas (audio routing/backends, credential storage, etc.) unless requested.
- Verify PR targets
pre-release(notmain).
- Check out the PR
gh pr checkout <PR_NUMBER>
- Rename the checked-out branch (local)
- Use an
externalbranch name so it's obvious these commits are third-party authored: git branch -m <type>/external/<topic>
- Use an
- Merge to pre-release
git checkout pre-releasegit merge --no-ff <type>/external/<topic>
- Run checks
- Frontend:
npm run build - Backend (when Rust changes):
cargo check(run fromsrc-tauri/)
- Frontend:
- Push pre-release
git push origin pre-release
- Close the PR with a comment explaining it was merged to
pre-release.
When ready to release:
git checkout main
git merge pre-release
git push origin main
git tag vX.Y.Z
git push origin vX.Y.ZThis is done exclusively by maintainers.
If you want the git history to clearly show third-party authored commits, avoid “squash merge”. Prefer:
- Create a merge commit, or
- Rebase and merge (preserves individual commits/authors)
- A short description of the problem and solution.
- Screenshots for UI changes when possible.
- Notes about any breaking changes or migrations.
- Large refactors mixed with feature work.
- Changes that reintroduce removed UI/UX patterns (for example, exporting offline cache files).
All UI text must use the translation system. No hardcoded strings in Svelte templates.
Translations are stored in:
src/lib/i18n/locales/en.json(English)src/lib/i18n/locales/es.json(Spanish)
- Read the existing locale files first to check if a translation already exists
- Reuse existing keys - avoid duplicating translations
- Add to both files - every new key must exist in en.json AND es.json
Import and use the t store:
<script lang="ts">
import { t } from '$lib/i18n';
</script>
<!-- In templates -->
<button>{$t('actions.save')}</button>
<span>{$t('settings.audio.title')}</span>svelte-i18n requires a specific format for variable interpolation:
// ❌ WRONG - will show {name} literally
$t('greeting', { name: userName })
// ✅ CORRECT - wrap in values object
$t('greeting', { values: { name: userName } })Use dot notation for namespacing:
{
"actions": {
"save": "Save",
"cancel": "Cancel",
"delete": "Delete"
},
"settings": {
"audio": {
"title": "Audio",
"streamingQuality": "Streaming Quality"
}
}
}Common top-level sections:
actions- Buttons and common actionstoast- Toast notification messagessettings- Settings view labelslibrary- Local library related textplayer- Player controls and statuserrors- Error messagesempty- Empty state messages
The $t() store can only be used in reactive contexts. In Svelte 5:
// ❌ WRONG - breaks Svelte preprocessing
const label = $t('some.key');
// ✅ CORRECT - use $derived
const label = $derived($t('some.key'));- No hardcoded strings in Svelte templates
- New translation keys added to both en.json and es.json
- Variable interpolation uses
{ values: { ... } }format - Checked existing keys before creating new ones (no duplicates)
- Keys follow naming conventions