Contributing
Thanks for your interest in contributing to Norish! This guide covers the conventions and processes once your development environment is up.
Project structure
norish/
├── apps/ # App workspaces
│ ├── web/ # Next.js app (App Router + server entry)
│ ├── mobile/ # Expo app workspace (@norish/mobile)
│ ├── parser-api/ # Python recipe parser service
│ ├── landing/ # Marketing site (norish.dev)
│ └── docs/ # This documentation site (docs.norish.dev)
├── packages/ # Shared libraries
│ ├── api/ # Server API logic (routers, AI, parsing)
│ ├── auth/ # Auth helpers
│ ├── config/ # Shared config
│ ├── db/ # Drizzle schema + repositories
│ ├── i18n/ # Locale tooling and data
│ ├── queue/ # Background jobs
│ ├── shared/ # Shared utilities and contracts
│ ├── shared-react/ # Shared React hooks and contexts
│ ├── shared-server/# Shared server utilities
│ ├── trpc/ # tRPC router definitions
│ └── ui/ # UI component library
├── tooling/ # Repo tooling
│ ├── eslint/ # @norish/eslint-config
│ ├── github/ # Shared GitHub Actions composite actions
│ ├── monorepo/ # Circular dependency checks
│ ├── prettier/ # @norish/prettier-config
│ ├── tailwind/ # @norish/tailwind-config
│ └── typescript/ # @norish/tsconfig
└── docker/ # Local runtime containers
Script ownership
- Root
package.jsonscripts are orchestration only and delegate into owned workspaces. - Monorepo control scripts live in
tooling/monorepo/scripts/. - App-owned scripts live in
apps/<app>/scripts/. - Package-owned scripts live in
packages/<package>/scripts/.
Shared tooling packages
- Shared lint, format, and TypeScript settings are published as workspace packages under
tooling/. - Workspaces should compose from these packages instead of creating root-level config files.
- Current shared packages:
@norish/eslint-config(tooling/eslint) withbase,react, andnextjsexports@norish/prettier-config(tooling/prettier)@norish/tsconfig(tooling/typescript) withbase.jsonandcompiled-package.json@norish/tailwind-config(tooling/tailwind) withthemeandpostcss-configexports
Adding a new shared config
- Create or update a workspace package under
tooling/with apackage.jsonand explicitexports. - Add the package to
pnpm-workspace.yamland consume it viaworkspace:*from each owning workspace. - Wire each workspace through local
package.jsonscripts (for examplelint,format,typecheck) and local config files that import shared config exports. - Run
pnpm run deps:cyclesand relevantturbo runchecks before opening a PR.
Code style
Imports
Always use the @/ path alias for imports:
// Good
import { useRecipesContext } from "@/context/recipes-context";
// Bad
import { useRecipesContext } from "../../../context/recipes-context";
Type safety
Never suppress TypeScript errors — avoid as any, @ts-ignore, and
@ts-expect-error.
Logging
Use the Pino logger instead of console.log:
// Server-side
import { createLogger } from "@/server/logger";
const log = createLogger("my-module");
log.info("Something happened");
// Client-side
import { createClientLogger } from "@/lib/logger";
const log = createClientLogger("MyComponent");
Database access
Always use the repository pattern instead of direct db access in routers:
// Good — use a repository
import { getRecipeById } from "@/server/db/repositories/recipes";
const recipe = await getRecipeById(id);
Naming conventions
- Hooks:
use-{domain}-{type}.ts(e.g.use-recipes-query.ts) - Components: PascalCase (e.g.
RecipeCard.tsx) - Files: kebab-case (e.g.
recipe-card.tsx)
Pull request process
- Create a branch —
feature/your-feature-nameorfix/your-bug-fix. - Make focused changes — clear commits, follow the code style, add tests for new functionality.
- Test your changes:
pnpm lintpnpm test:runpnpm i18n:checkpnpm build
- Open a PR — follow the PR template and link an issue (
Fixes #...in the body). PRs without a linked issue will be closed, except translation-only PRs. Ensure CI passes.
Testing
Tests are colocated in workspace __tests__/ directories (e.g.
apps/web/__tests__/...). We use Vitest with React Testing Library.
# Run all tests
pnpm test:run
# Run tests for a specific workspace
pnpm --filter @norish/web run test
# Run a specific test file (from within the workspace directory)
cd apps/web && pnpm exec vitest run __tests__/hooks/recipes/use-recipes-query.test.ts
Adding translations
Norish uses a configurable locale system. The bundled catalog lives in
packages/i18n/src/locales.ts, server defaults are derived from it in
packages/config/src/server-config-loader.ts, and locales can be
enabled/disabled at runtime via the Admin UI or environment variables.
1. Add the locale to the catalog
Edit packages/i18n/src/locales.ts:
export const LOCALE_CATALOG = {
en: { name: "English" },
nl: { name: "Nederlands" },
"your-locale": { name: "Your Language" },
} as const;
This is the single source of truth for bundled locale metadata used by web, mobile fallback, and server defaults.
2. Create translation files
Create packages/i18n/src/messages/{your-locale}/ with these files, copying the
structure from packages/i18n/src/messages/en/ as a starting point:
common.json, recipes.json, groceries.json, calendar.json,
settings.json, navbar.json, auth.json.
Register message loaders (required for web and mobile)
Expo Metro does not support fully dynamic JSON imports for locale bundles. After
adding a locale folder, update packages/i18n/src/messages.ts and add static
loader entries for every section under MESSAGE_LOADERS. Skipping this can make
iOS/Android bundling fail with an "Invalid call" error from dynamic import(...).
3. Verify translations
pnpm i18n:check
This uses en as the source of truth and reports missing keys (exist in
en but not your locale — CI fails) and extra keys (in your locale but not
en — warning only). The check runs in CI and blocks PRs with missing
translations.
4. Enable the locale
New locales are enabled by default once added to LOCALE_CATALOG and wired
into packages/i18n/src/messages.ts. You can also control runtime availability
via Settings → Admin → General or the ENABLED_LOCALES environment variable
(e.g. ENABLED_LOCALES=en,nl,your-locale).
License
By contributing to Norish, you agree that your contributions will be licensed under the AGPL-3.0 License.