Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
587d137
refactor(core): own the resource folder pair and unify the staleness …
SimonNodel-AI Sep 23, 2026
a4dcfa1
refactor(core): resolve collections once with loadConfig and openColl…
SimonNodel-AI Sep 23, 2026
540058f
refactor(core): collapse import into importResources and add runExport
SimonNodel-AI Sep 23, 2026
bef2974
refactor(api): replace the cache mechanism with a CollectionIndex
SimonNodel-AI Sep 23, 2026
ef5b287
refactor: typed core errors with one HTTP filter and one CLI reporter
SimonNodel-AI Sep 23, 2026
2e289b9
refactor(tracker): lift ResourceEntryDraft out of the translation editor
SimonNodel-AI Sep 23, 2026
38b0de1
refactor(tracker): one translation status summary for roll-ups
SimonNodel-AI Sep 23, 2026
00cb70e
refactor(libs): make the domain and core barrels explicit
SimonNodel-AI Sep 23, 2026
650d968
refactor(core): bind resource and folder operations to an opened Coll…
SimonNodel-AI Sep 23, 2026
76b87e7
refactor: one Resource Summary with an explicit address
SimonNodel-AI Sep 23, 2026
51db74d
refactor(core): one Collection Reader for every whole-collection read
SimonNodel-AI Sep 23, 2026
7930e3d
refactor(core): one Translator behind the machine-translation seam
SimonNodel-AI Sep 24, 2026
845b159
refactor(core): one Bundle Selection over opened Collections
SimonNodel-AI Sep 24, 2026
ce033da
refactor(cli): one Command Runner for every command
SimonNodel-AI Sep 24, 2026
6ec3d9e
refactor: one Resource Search that ranks, then limits
SimonNodel-AI Sep 24, 2026
5402dbc
refactor: one bundle definition module in domain
SimonNodel-AI Sep 24, 2026
6c58460
fix(tracker): one display status for a locale without metadata
SimonNodel-AI Sep 24, 2026
6bc971c
fix: stderr diagnostics, status sort, delete confirm, job error, simi…
SimonNodel-AI Sep 24, 2026
02229cc
chore: typecheck tracker specs, expose move --dest-collection, refres…
SimonNodel-AI Sep 24, 2026
8472712
chore: typecheck cli, core and api specs; fix xliff typing and notes
SimonNodel-AI Sep 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion .claude/skills/lingo-tracker/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Before calling `add-resource` for any detected string, run:
bash .claude/skills/lingo-tracker/scripts/find-similar.sh "<the string value>"
```

**If the command returns any results, evaluate each match** (the tool already filters to ≥ 80% similarity):
**If the command returns any results, evaluate each match** (the tool shows values that are ≥ 80% similar, or that contain the string or are contained in it as whole words with ≥ 40% similarity; such a containment match shows its real similarity, between 40% and 80%):

- **Reuse the existing key if**:
- The stored value is identical or nearly identical (≥ 95% similarity)
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,8 @@ jobs:
- name: Test (affected)
run: pnpm nx affected -t test --base=${{ steps.nx-base.outputs.NX_BASE }}

- name: Typecheck (affected)
run: pnpm nx affected -t typecheck --base=${{ steps.nx-base.outputs.NX_BASE }}

- name: Build (affected)
run: pnpm nx affected -t build --base=${{ steps.nx-base.outputs.NX_BASE }}
64 changes: 35 additions & 29 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,13 @@ pnpm run test:core
pnpm run test:domain
pnpm run test:tracker

# Run a single test file (using Nx)
pnpm nx test core --testFile=src/lib/resource/checksum.spec.ts
# Run a single test file (path relative to the project root)
pnpm nx test core --testFile=src/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 tracker
```

### Development Servers
Expand Down Expand Up @@ -63,15 +68,15 @@ pnpm nx # Direct Nx CLI access

## Architecture

### Monorepo Structure (Nx 21.5.3)
### Monorepo Structure (Nx 22.7.9)

The 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 20 + Material)
└── tracker/ # Web UI (Angular 21 + Material)

libs/
├── domain/ # Pure business logic (NO Node.js deps — browser-safe)
Expand Down Expand Up @@ -178,41 +183,42 @@ export class ExampleComponent implements OnInit {

## Key Implementation Patterns

### Adding a Resource (libs/core/src/lib/resource/)
### Adding a Resource (libs/core/src/resource/add-resource.ts)

1. Validate key format and resolve to folder path
2. Load existing `resource_entries.json` and `tracker_meta.json`
3. Compute checksums (MD5) for source and translations
4. Determine translation status based on checksums
5. Write updated files atomically
1. Validate the key (and optional `targetFolder`) and resolve it to a folder path
2. Normalize values to ICU and seed every target locale (supplied value, else auto-translation, else a `new` copy of the base) before touching the disk
3. Write through `openResourceFolder` (`libs/core/src/lib/resource/resource-folder.ts`), the one read-modify-write path for `resource_entries.json` + `tracker_meta.json`: it computes MD5 checksums and applies the staleness rule
4. `save()` writes both files together (not atomically)

### CLI Command Pattern (apps/cli/src/commands/)
### CLI Command Pattern (apps/cli/src/runner/command-runner.ts)

```typescript
export const commandName = new Command('command-name')
.description('Description')
.argument('<arg>', 'Arg description')
.action(async (arg) => {
// Use dynamic import for prompts
const prompts = (await import('prompts')).default;

// Call core library functions
const result = await coreFunction(arg);
});
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.name, answers.locale, { cwd });
ConsoleFormatter.success(result.message);
},
});
```

### API Controller Pattern (apps/api/src/app/controllers/)
- The runner owns config loading, collection resolution, the interactive rule, cancellation and exit codes (`process.exitCode`, never `process.exit`). `run` returns `{ 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.ts` covers the flag wiring.

### API Controller Pattern (apps/api/src/app/<feature>/<feature>.controller.ts)

```typescript
@Controller('collections')
export class CollectionsController {
@Post()
async create(@Body() dto: CreateCollectionDto) {
// Call core library
const result = await addCollection(dto.name, dto.config);

// Map to DTO if needed
return mapper.toDto(result);
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 };
}
}
```
Expand All @@ -222,7 +228,7 @@ export class CollectionsController {
- **Package Manager**: pnpm 10+ required (enforced by engines)
- **Node Version**: >=22.16.0
- **Commits**: Use `pnpm run commit` for conventional commits with commitizen
- **Testing**: Vitest for unit tests, use `--testFile` flag to run single test
- **Testing**: Vitest for unit tests (the API uses Jest); see [Testing](#testing) for running a single file
- **API Port**: Default 3030, configurable via `LINGO_TRACKER_PORT` env var
- **CORS**: Enabled with wildcard origin in development mode

Expand Down
12 changes: 12 additions & 0 deletions apps/api/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,18 @@
}
}
},
"typecheck": {
"dependsOn": ["^typecheck", "typecheck-spec"]
},
"typecheck-spec": {
"executor": "nx:run-commands",
"cache": true,
"inputs": ["default", "^default", "{workspaceRoot}/tsconfig.base.json", "{workspaceRoot}/types/**/*"],
"options": {
"cwd": "apps/api",
"command": "tsc --noEmit -p tsconfig.spec.json"
}
},
"test": {
"options": {
"passWithNoTests": true
Expand Down
14 changes: 12 additions & 2 deletions apps/api/src/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { Logger, Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { BundleJobService } from './bundles/bundle-job.service';
import { BundlesController } from './bundles/bundles.controller';
import { CollectionCacheService } from './cache/collection-cache.service';
import { CollectionIndex } from './cache/collection-index.service';
import { CollectionsController } from './collections/collections.controller';
import { FoldersController } from './collections/folders/folders.controller';
import { LocalesController } from './collections/locales/locales.controller';
import { ResourcesController } from './collections/resources/resources.controller';
import { ConfigController } from './config/config.controller';
import { ConfigService } from './config/config.service';
import { LingoTrackerExceptionFilter } from './errors/lingo-tracker-exception.filter';
import { TranslationJobService } from './translation-job/translation-job.service';

@Module({
Expand All @@ -23,7 +25,15 @@ import { TranslationJobService } from './translation-job/translation-job.service
LocalesController,
BundlesController,
],
providers: [AppService, ConfigService, CollectionCacheService, TranslationJobService, BundleJobService, Logger],
providers: [
AppService,
ConfigService,
CollectionIndex,
TranslationJobService,
BundleJobService,
Logger,
{ provide: APP_FILTER, useClass: LingoTrackerExceptionFilter },
],
})
export class AppModule {
constructor() {
Expand Down
3 changes: 2 additions & 1 deletion apps/api/src/app/bundles/bundle-job.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ describe('BundleJobService', () => {
expect(service.getJob('nope')).toBeUndefined();
});

it('passes the bundle params, locales and an onProgress callback to generateBundle', async () => {
it('passes the bundle params, locales, the project directory and an onProgress callback to generateBundle', async () => {
mockGenerateBundle.mockResolvedValue(makeResult());

service.startJob({ ...makeParams(), locales: ['fr'] });
Expand All @@ -87,6 +87,7 @@ describe('BundleJobService', () => {
expect(params.bundleDefinition).toBe(bundleDefinition);
expect(params.config).toBe(config);
expect(params.locales).toEqual(['fr']);
expect(params.cwd).toBe(process.cwd());
expect(typeof params.onProgress).toBe('function');
});

Expand Down
4 changes: 3 additions & 1 deletion apps/api/src/app/bundles/bundle-job.service.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { randomUUID } from 'node:crypto';
import { Injectable, Logger } from '@nestjs/common';
import type { BundleDefinition, BundleProgressEvent, LingoTrackerConfig } from '@simoncodes-ca/core';
import type { BundleProgressEvent, LingoTrackerConfig } from '@simoncodes-ca/core';
import { generateBundle } from '@simoncodes-ca/core';
import type {
BundleGenerateJobDto,
BundleGenerateJobProgressDto,
BundleGenerateJobResultDto,
BundleGenerateJobStatus,
} from '@simoncodes-ca/data-transfer';
import type { BundleDefinition } from '@simoncodes-ca/domain';
import { mapGenerateBundleResultToJobResult } from '../mappers/bundle.mapper';

export interface StartBundleJobParams {
Expand Down Expand Up @@ -93,6 +94,7 @@ export class BundleJobService {
config: params.config,
...(params.locales && { locales: params.locales }),
onProgress,
cwd: process.cwd(),
});

const completed = this.#jobs.get(jobId);
Expand Down
Loading
Loading