Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
0739085
add add-i18n-check-script OpenSpec
oinsio Jul 12, 2026
ea0204f
implement add-i18n-check-script OpenSpec
oinsio Jul 12, 2026
1f67f2a
implement add-i18n-check-script OpenSpec
oinsio Jul 12, 2026
c91280c
archive add-i18n-check-script OpenSpec
oinsio Jul 12, 2026
1407710
add i18n-cleanup-and-lint OpenSpec
oinsio Jul 12, 2026
5dad406
implement i18n-cleanup-and-lint OpenSpec
oinsio Jul 12, 2026
d44ed05
archive i18n-cleanup-and-lint OpenSpec
oinsio Jul 12, 2026
5b03acc
add fix-i18n-check-dynamic-prefix-gaps OpenSpec
oinsio Jul 12, 2026
666c2c9
implement fix-i18n-check-dynamic-prefix-gaps OpenSpec
oinsio Jul 12, 2026
a9760af
implement fix-i18n-check-dynamic-prefix-gaps OpenSpec
oinsio Jul 12, 2026
557ddbf
implement fix-i18n-check-dynamic-prefix-gaps OpenSpec
oinsio Jul 12, 2026
56601f6
archive fix-i18n-check-dynamic-prefix-gaps OpenSpec
oinsio Jul 12, 2026
83f59b9
add deduplicate-i18n-common-keys OpenSpec
oinsio Jul 12, 2026
c1201fe
implement deduplicate-i18n-common-keys OpenSpec
oinsio Jul 13, 2026
fc7f3ce
fix tests
oinsio Jul 13, 2026
7d24cf6
archive deduplicate-i18n-common-keys OpenSpec
oinsio Jul 13, 2026
ee2b4ad
add rework-house-locale OpenSpec
oinsio Jul 13, 2026
af0f6ef
add rework-house-locale OpenSpec
oinsio Jul 13, 2026
f20bf6e
implement rework-house-locale OpenSpec
oinsio Jul 13, 2026
4fcfe43
archive rework-house-locale OpenSpec
oinsio Jul 13, 2026
25e9b99
add add-startrek-locale OpenSpec
oinsio Jul 13, 2026
d8e1558
implement add-startrek-locale OpenSpec
oinsio Jul 14, 2026
70ce009
implement add-startrek-locale OpenSpec
oinsio Jul 14, 2026
1859f92
archive add-startrek-locale OpenSpec
oinsio Jul 14, 2026
fbd3d58
add add-project-locales OpenSpec
oinsio Jul 14, 2026
1506922
add add-project-locales OpenSpec
oinsio Jul 14, 2026
b835da4
implement add-project-locales OpenSpec
oinsio Jul 14, 2026
27c6180
archive add-project-locales OpenSpec
oinsio Jul 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/rules/antipatterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
## Code antipatterns

- **Direct imports from sibling module internals** — only through `index.ts`. Enforce with linter.
- **Files over 400 lines** — bad for AI context. Split into smaller units.
- **Files over 200 lines** — bad for AI context. Split into smaller units.

## UI antipatterns

Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/deploy-qa.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Check i18n
run: pnpm i18n:check

- name: Lint
run: pnpm lint

Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Check i18n
run: pnpm i18n:check

- name: Build client
run: pnpm --filter @clear-progress/client... build
env:
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# Clear Progress

[![deploy](https://img.shields.io/github/actions/workflow/status/oinsio/clear-progress/deploy.yml?branch=main&label=deploy)](https://github.com/oinsio/clear-progress/actions/workflows/deploy.yml)
[![License: PolyForm Noncommercial 1.0.0](https://img.shields.io/badge/License-PolyForm%20Noncommercial%201.0.0-purple)](LICENSE)

[![en](https://img.shields.io/badge/lang-en-blue.svg)](README.md)
[![ru](https://img.shields.io/badge/lang-ru-red.svg)](README.ru.md)

[![deploy](https://img.shields.io/github/actions/workflow/status/oinsio/clear-progress/deploy.yml?branch=main&label=deploy)](https://github.com/oinsio/clear-progress/actions/workflows/deploy.yml)

Personal task, goal, and idea manager. Built for GTD and Jedi Empty Inbox techniques. Client-first PWA that works offline — your data stays on your device, with optional sync via your own Supabase server.

**[Open the App](https://oinsio.github.io/clear-progress/)**
Expand Down
3 changes: 2 additions & 1 deletion README.ru.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# Clear Progress

[![deploy](https://img.shields.io/github/actions/workflow/status/oinsio/clear-progress/deploy.yml?branch=main&label=deploy)](https://github.com/oinsio/clear-progress/actions/workflows/deploy.yml)
[![License: PolyForm Noncommercial 1.0.0](https://img.shields.io/badge/License-PolyForm%20Noncommercial%201.0.0-purple)](LICENSE)

[![en](https://img.shields.io/badge/lang-en-blue.svg)](README.md)
[![ru](https://img.shields.io/badge/lang-ru-red.svg)](README.ru.md)

[![deploy](https://img.shields.io/github/actions/workflow/status/oinsio/clear-progress/deploy.yml?branch=main&label=deploy)](https://github.com/oinsio/clear-progress/actions/workflows/deploy.yml)

Персональное приложение для управления задачами, целями и идеями. Создано для GTD и Джедайских техник пустого инбокса. Client-first PWA, работающее офлайн — данные хранятся на устройстве, с опциональной синхронизацией через собственный сервер Supabase.

**[Открыть приложение](https://oinsio.github.io/clear-progress/)**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-07-12
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
## Context

The project uses i18next with JSON locale files (`src/locales/{en,ru,house}.json`). Keys are referenced in source code via `t("key")` calls, dynamic template literals (`` t(`prefix.${var}`) ``), and indirect `messageKey` patterns. There is no tooling to detect drift between locale files and code usage.

Project-specific concerns:
- `_meta.*` block in each locale is service metadata (read by `localeRegistry.ts`), not a translation key
- `house.json` is a partial override of `ru` (incompleteness is normal; only orphan keys are errors)
- Plural suffixes (`_one|_two|_few|_many|_other|_zero`) and ordinal suffixes (`_ordinal_*`) differ legitimately between en/ru — comparisons must use normalized base keys
- Some keys are constructed dynamically: `` t(`repeat.month${m}`) ``, `` t(`goal.status.${status}`) ``, `messageKey` in `healingRules.ts`

## Goals / Non-Goals

**Goals:**
- Statically validate all classes of i18n inconsistency (FR1–FR10)
- Integrate into existing test suite without new runtime dependencies
- Keep maintenance burden minimal (only `whitelist.ts` requires manual updates)

**Non-Goals:**
- Runtime checking or type generation (NG1, NG3)
- Auto-removal of dead keys (NG2 — separate change)

## Decisions

### D1: Script location — `packages/client/scripts/i18n-check/`

**Rationale**: The script operates exclusively on client package files. Placing it inside `packages/client` keeps paths simple and avoids cross-package imports. The `scripts/` directory separates tooling from application source.

**Alternatives considered**: Root-level `tools/` directory — rejected because the script reads `src/locales` and `src/**/*.{ts,tsx}` relative to the client package.

### D2: Module structure — 8 files with `index.ts` facade

```
scripts/i18n-check/
├── index.ts # public facade (project invariant)
├── types.ts # shared types
├── flatten.ts # JSON flattening + plural normalization
├── scan.ts # source file scanning for key references
├── whitelist.ts # explicit patterns for undetectable dynamic keys
├── checks.ts # 4 check functions (undefined/unused/parity/orphans)
├── duplicates.ts # report-only duplicate detection
├── run.ts # orchestrator (no process.exit)
└── cli.ts # entrypoint with process.exit and CLI flags
```

**Rationale**: Separation of `run.ts` (pure logic) from `cli.ts` (side effects) enables Vitest to import check logic without triggering `process.exit`. Each file stays under 200 lines (NFR-M1).

### D3: Key detection strategy — regex-based static scan

Two regex patterns extract keys from source:
1. **Literal keys**: dot-separated identifiers in quotes — filtered by top-level namespace membership (from `en.json` root keys) to reject false matches like `"package.json"` or URLs
2. **Dynamic prefixes**: template literal prefix before `${...}` — matches keys where the remainder after prefix contains no dots (for `.`-terminated prefixes) or is digits-only (for non-`.` prefixes like `repeat.month`)

**Rationale**: No AST parsing needed — regex is fast (<2s) and sufficient for the project's patterns. The namespace filter eliminates most false positives without maintaining a stop-list.

**Trade-off**: String concatenation (`"repeat." + x`) is not detected. This is acceptable given the project convention of using template literals; edge cases go into the whitelist.

### D4: Whitelist design — explicit `RegExp` patterns with self-validation

Each whitelist entry is a `{ pattern: RegExp; reason: string }`. The script validates that every pattern matches at least one key in `en.json` — stale entries become errors automatically.

**Rationale**: Patterns are more expressive than simple prefix lists (can express numeric ranges like `repeat.weekday1..7`). Self-validation prevents whitelist rot.

### D5: Dynamic prefix matching — conservative rest-character check

A dynamic prefix `P` matches key `K` only if:
- `P` ends with `.` AND the rest (`K` minus `P`) contains no dots, OR
- `P` does not end with `.` AND the rest is digits-only

**Rationale**: Without this restriction, prefix `repeat.month` would falsely cover `repeat.monthAndDay` or `repeat.monthGenitive1`. The conservative check ensures only intended suffixes are matched.

### D6: Execution — `tsx` runner via `pnpm i18n:check`

The script uses TypeScript directly via `tsx` (already a dev dependency). No compilation step needed.

**Alternative**: Run only via Vitest integration test — rejected because a standalone CLI command is useful for local debugging and CI readability.

### D7: Test file separation — keys in test files reported differently

Keys found exclusively in test files (`*.test.*`, `*.spec.*`, `__mocks__/`, `test/`) do not trigger `undefined` errors (test fixtures may use non-existent keys). However, they are flagged in the `unused` report with a "test-only" marker.

**Rationale**: Prevents test helper strings (like `"task.name"` as entity field names) from blocking CI, while still surfacing potentially dead keys.

## Risks / Trade-offs

- **[Risk]** New dynamic patterns in future code may not be auto-detected → **Mitigation**: Developer guide documents the two-step process: (1) use template literal with dot prefix (auto-detected), (2) if impossible, add to `whitelist.ts`
- **[Risk]** False positives from non-i18n strings matching namespace filter (e.g., `"task.name"` as DB field) → **Mitigation**: Such strings are counted as "used", which only causes false negatives in `unused` (acceptable) — never false `undefined` errors
- **[Risk]** Script reads all source files synchronously — could slow down on very large codebases → **Mitigation**: Current codebase is ~150 files; synchronous I/O is simpler and fast enough (NFR-P1)
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# add-i18n-check-script

## Why

Locale files (`src/locales/*.json`) and source code (`src/**/*.{ts,tsx}`) drift apart silently: keys get added to code but not to locale files (runtime crashes), keys accumulate in locale files after code removal (~90 unused keys today), and override locales (`house.json`) retain orphan keys for deleted base keys. There is no automated check — issues are discovered manually or by users. A static analysis script will catch all classes of i18n inconsistency on CI before merge.

## What Changes

- **ADDED**: Static i18n consistency check script (`packages/client/scripts/i18n-check/`) that validates locale files against source code usage
- **ADDED**: `pnpm i18n:check` command in `packages/client/package.json`
- **ADDED**: Vitest integration test that runs the check as part of the test suite

## Capabilities

### New Capabilities

- `i18n-check`: Static analysis script that detects undefined keys, unused keys, locale parity violations, override orphans, and duplicate values across locale files

### Modified Capabilities

- `i18n`: Adds automated consistency enforcement to the existing i18n infrastructure

## Goals

- G1: Zero undefined/unused/parity/orphan errors pass CI undetected
- G2: Script runs in < 2s on the current codebase
- G3: False positive rate is zero for the current codebase (no suppressions needed for legitimate patterns)

## Non-Goals

- NG1: Type-safe `t()` calls via generated TypeScript declarations (future change)
- NG2: Auto-fixing detected issues (separate `i18n-cleanup` change)
- NG3: Runtime i18n validation or browser-side checks
- NG4: Checking interpolation parameter consistency between locales

## Users & Scenarios

- U1: Developer adds a new UI string — script catches missing locale key before merge
- U2: Developer removes a feature — script reports unused keys for cleanup
- U3: Developer adds keys to `en.json` but forgets `ru.json` — parity check fails
- U4: Developer edits `house.json` override with a key that no longer exists in `ru.json` — orphan check fails

## Requirements

### Functional

- FR1: Detect keys used in production code (`src/**/*.{ts,tsx}`) that are absent from `en.json` (undefined check)
- FR2: Detect keys present in `en.json` that are not referenced by any source file, dynamic prefix, or whitelist pattern (unused check)
- FR3: Detect base-key set differences between `en.json` and `ru.json` after normalizing plural/ordinal suffixes (parity check)
- FR4: Detect keys in override locales (identified by `_meta.baseLanguage`) that do not exist in their base locale (override-orphans check)
- FR5: Report groups of keys with identical values in both `en` and `ru` as candidates for deduplication (always printed, non-blocking)
- FR6: Recognize i18next plural suffixes (`_one|_two|_few|_many|_other|_zero`) and ordinal suffixes (`_ordinal_*`) — compare base keys only, since en/ru have different plural forms
- FR7: Recognize dynamic key patterns from template literals (`` `prefix.${var}` ``) and exclude matched keys from unused report
- FR8: Support an explicit whitelist for keys that cannot be detected statically (numeric suffixes, `messageKey` indirection) — each whitelist entry must match at least one existing key (stale entries are errors)
- FR9: Exclude `_meta.*` keys from all checks (service metadata read by `localeRegistry.ts`)
- FR10: Keys found only in test files do not trigger `undefined` errors but are flagged in `unused` report

### Non-Functional

#### Performance

- NFR-P1: Full check completes in under 2 seconds on the current codebase (~150 source files, 4 locale files)

#### Maintainability

- NFR-M1: Each script module file stays under 200 lines
- NFR-M2: Script is importable by Vitest without `process.exit` side effects (separation of logic and CLI)

## UX Acceptance Criteria

- UX1: On failure, output lists each error as `[kind] key — detail` with a summary count
- UX2: On success, output is a single line: `i18n-check: OK`
- UX3: Exit code is 1 on errors, 0 on success

## Success Metrics

- M1: Script detects all currently known issues (≥90 unused keys, ≥16 override orphans) when run against the unmodified codebase
- M2: After `i18n-cleanup` change removes dead keys, script reports 0 errors on CI
- M3: Mutation testing score ≥ 95% on flatten/scan/checks modules

## Open Questions

None — all resolved.
Loading
Loading