This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
LingoTracker is a translation management system designed to work with the Transloco library. It provides CLI, REST API, and web UI interfaces for managing translation resources with metadata tracking, ICU format validation, and Git-friendly JSON storage.
# Build all projects
pnpm run build
# Build individual apps
pnpm run build:cli # CLI application
pnpm run build:api # NestJS API
pnpm run build:tracker # Angular UI# Run all tests
pnpm run test
# Run tests for individual apps/libs
pnpm run test:cli
pnpm run test:api
pnpm run test:core
pnpm run test:domain
pnpm run test:tracker
# Run a single test file (path relative to the project root)
pnpm nx test core --testFile=src/lib/resource/checksum.spec.ts # core, domain, tracker (@nx/vitest)
pnpm nx test cli -- src/commands/move.test.ts # cli (vitest): positional path after --
pnpm nx test api -- src/app/app.service.spec.ts # api (jest): positional path after --
# Typecheck (tracker, core, cli and api also typecheck their specs via typecheck-spec)
pnpm nx typecheck trackerpnpm run serve:cli # Watch mode for CLI
pnpm run serve:api # API server (default port 3030)
pnpm run serve:tracker # Angular dev serverUse the /lingo-tracker skill for detecting hardcoded strings, creating translation resources, and updating components to use Transloco. It handles the full workflow: detect → add-resource → bundle → update code.
Use the /playwright-cli skill for browser automation, UI testing, screenshots, and interacting with the running Tracker UI. Do NOT use the Playwright MCP plugin or invoke Playwright directly.
pnpm run commit # Interactive conventional commit
pnpm run test:release # Dry-run semantic release
pnpm nx # Direct Nx CLI accessThe codebase follows a layered architecture with three applications sharing core business logic:
apps/
├── cli/ # Command-line interface (Node.js + Commander)
├── api/ # REST API backend (NestJS + Express)
└── tracker/ # Web UI (Angular 21 + Material)
libs/
├── domain/ # Pure business logic (NO Node.js deps — browser-safe)
│ └── src/lib/ # Flat structure: key validation, status helpers,
│ # format conversion, validation utilities, shared types
├── core/ # Node.js business logic (file I/O, checksums, bundles)
│ └── src/
│ ├── config/ # Configuration interfaces
│ ├── collections-manager/ # Collection operations
│ └── lib/resource/ # Resource CRUD, Folder Address, metadata, reading
└── data-transfer/ # DTOs shared between API/CLI/UI
- CLI (
apps/cli): Commands for init, add-collection, edit-collection, delete-collection, add-locale, remove-locale, add-resource, edit-resource, delete-resource, move, normalize, translate-locale, bundle, export, import, validate, find-similar, glossary, protected-terms, preferred-terminology, install-skill. Supports both interactive (TTY) and non-interactive (CI/CD) modes. - API (
apps/api): REST endpoints at/api/*, serves static Tracker UI, uses mappers to convert between core domain models and DTOs. - Tracker UI (
apps/tracker): Angular app with Material UI for browsing/managing translations, uses NgRx Signals for state management.
- Resources use dot-delimited keys (e.g.,
apps.common.buttons.ok) - Keys decompose into folder paths:
apps.common.buttons.ok→apps/common/buttons/folder withokas entry - Each folder contains:
resource_entries.json- Translation datatracker_meta.json- Metadata (checksums, status)
- Global:
.lingo-tracker.jsonat project root - Per-Collection: Collections can override global settings
- Key settings:
baseLocale,locales,translationsFolder
new- Not yet translatedtranslated- Has translation but not verifiedstale- Base value changed, translation out of syncverified- Reviewed and approved
- MD5 checksums track source and translation changes
baseChecksum- Hash of source locale valuechecksum- Hash of current translation- Enables automatic stale detection when source changes
- Use Signals: Prefer
signal(),computed(),effect()over BehaviorSubject/Observable for component state - Functional Injection: Use
inject()function instead of constructor injection - Standalone Components: All new components should be standalone with direct imports
- OnPush Change Detection: Default to
ChangeDetectionStrategy.OnPush - Typed Forms: Always specify types for FormGroup and FormControl
- Host Bindings: Use
hostin decorator over@HostBinding/@HostListener - Lifecycle Interfaces: Implement interfaces (OnInit, OnDestroy, etc.) for type safety
Example:
@Component({
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule],
host: {
'[class.active]': 'isActive()',
},
})
export class ExampleComponent implements OnInit {
private service = inject(DataService);
count = signal(0);
doubledCount = computed(() => this.count() * 2);
ngOnInit(): void {
/* ... */
}
}- No non-null assertions: Never use the
!operator. The linter (lint/style/noNonNullAssertion) forbids it.- In production code: use an explicit null-guard (
if (!x) return;orif (!x) throw) before accessing the value - In tests: use
expect(value).toBeDefined()followed by optional chaining (value?.property) for subsequent assertions
- In production code: use an explicit null-guard (
- Domain Logic (
@simoncodes-ca/domain): Pure business logic with zero Node.js dependencies — importable by all apps including the browser-based Tracker UI. This is where platform-agnostic logic belongs: key validation/parsing, translation status helpers, ICU↔Transloco format conversion, validation utilities (locale, key length, duplicates, hierarchical conflicts), and shared types (TranslationStatus,LocaleMetadata). - Core Logic (
@simoncodes-ca/core): Node.js-dependent business logic — file I/O, checksums (crypto), directory traversal, bundle generation, import/export. Core depends on domain; domain must never depend on core. - DTOs (
@simoncodes-ca/data-transfer): API contracts shared between API/CLI/UI - Mappers: Convert between domain models and DTOs in API layer
Belongs in domain |
Belongs in core |
|---|---|
| Pure functions (string transforms, validation) | Anything using fs, path, crypto |
| Types and interfaces shared across all apps | File read/write operations |
| Key parsing, status logic, format conversion | Bundle generation, import/export from disk |
| Regex-based validation | Directory traversal, checksum calculation |
- Validate the key (and optional
targetFolder) and resolve it to a folder path - Check whether the resolved key exists; fail by default before locale seeding, or replace only when requested
- Normalize values to ICU and seed every target locale (supplied value, else auto-translation, else a
newcopy of the base) before touching the disk - Write through
openResourceFolder(libs/core/src/lib/resource/resource-folder.ts), the one read-modify-write path forresource_entries.json+tracker_meta.json: it computes MD5 checksums and applies the staleness rule save()writes both files together (not atomically)
export const addLocaleCommand = defineCommand<AddLocaleOptions>()({
name: 'Add locale', // "❌ Add locale cancelled."
collection: 'writable', // 'writable' | 'read' | 'none'
prompts: (options) => (options.locale ? [] : [{ type: 'text', name: 'locale', message: 'Locale' }]),
required: ['locale'], // exit 1 when missing; typed as present in run
run: async ({ collection, cwd, answers }) => {
const result = await addLocaleToCollection(
collection,
createConfigFileOperations({ cwd, snapshot: collection.sourceConfig }),
answers.locale,
);
ConsoleFormatter.success(result.message);
},
});- The runner owns config loading, collection resolution, the interactive rule, cancellation and exit codes (
process.exitCode, neverprocess.exit).runreturns{ exitCode: 1 }for a failure it has already reported, or throws. - Diagnostics go to stderr via
ConsoleFormatter.error/warning; the payload goes to stdout. - Commands are registered (flags, help text, lazy import) in
apps/cli/src/main.ts;main.spec.tscovers the flag wiring.
@Controller('collections')
export class CollectionsController {
@Post()
async createCollection(@Body() body: CreateCollectionDto): Promise<{ message: string }> {
const mapped = mapDtoToCollection(body.collection); // DTO → core shape (apps/api/src/app/mappers/)
const result = addCollection(body.name, mapped); // synchronous core call
return { message: result.message };
}
}- Package Manager: pnpm 10+ required (enforced by engines)
- Node Version: >=22.16.0
- Commits: Use
pnpm run commitfor conventional commits with commitizen - Testing: Vitest for unit tests (the API uses Jest); see Testing for running a single file
- API Port: Default 3030, configurable via
LINGO_TRACKER_PORTenv var - CORS: Enabled with wildcard origin in development mode
- When running tasks (for example build, lint, test, e2e, etc.), always prefer running the task through
nx(i.e.nx run,nx run-many,nx affected) instead of using the underlying tooling directly - You have access to the Nx MCP server and its tools, use them to help the user
- When answering questions about the repository, use the
nx_workspacetool first to gain an understanding of the workspace architecture where applicable. - When working in individual projects, use the
nx_project_detailsmcp tool to analyze and understand the specific project structure and dependencies - For questions around nx configuration, best practices or if you're unsure, use the
nx_docstool to get relevant, up-to-date docs. Always use this instead of assuming things about nx configuration - If the user needs help with an Nx configuration or project graph error, use the
nx_workspacetool to get any errors