Skip to content

Feat/i18n tooling#163

Open
agualdron wants to merge 4 commits into
stagingfrom
feat/i18n_tooling
Open

Feat/i18n tooling#163
agualdron wants to merge 4 commits into
stagingfrom
feat/i18n_tooling

Conversation

@agualdron

Copy link
Copy Markdown
Collaborator

Summary

  • Adds a new bun run i18n:catalog developer CLI under apps/tradinggoose/scripts/i18n-catalog to scan either a single localized route or the full app and report used keys, missing keys, target-locale gaps, optional orphaned keys, dynamic protected roots, and hardcoded copy candidates.
  • Adds route entry discovery and route-ownership logic so the scanner can follow localized layouts, route boundaries, shared app files, and canonical dynamic route patterns when attributing i18n usage.
  • Updates apps/tradinggoose/i18n/public-copy.test.ts to guard landing array shapes and cross-locale catalog shape alignment, and updates app type-checking to run next typegen through a dedicated tsconfig.typecheck.json.
  • Type of changes: New feature, Other (developer tooling, validation, and test coverage).

Why

  • We need a reliable way to audit localization coverage and catch missing translations, orphaned keys, and hardcoded copy before they ship.
  • The scanner also reduces manual review cost by pointing reviewers to route-owned namespaces and likely hardcoded strings.
  • Running next typegen before tsc keeps type-checking aligned with generated Next route types instead of relying on incomplete compile inputs.

Affected Areas

  • apps/tradinggoose
  • apps/docs
  • packages/*
  • Workflows / execution
  • Realtime / sockets
  • Market data / charting
  • Dev tooling / CI / infra
  • Documentation only
  • Other:

Validation

bunx biome check --files-ignore-unknown=true apps/tradinggoose/i18n/public-copy.test.ts apps/tradinggoose/package.json apps/tradinggoose/tsconfig.typecheck.json apps/tradinggoose/scripts/i18n-catalog
git diff --check staging...HEAD
cd apps/tradinggoose && bun run test -- scripts/i18n-catalog/index.test.ts i18n/public-copy.test.ts
cd apps/tradinggoose && bun run type-check

agualdron added 2 commits July 6, 2026 16:45
Add an *i18n:catalog* CLI that scans route or full-app entry graphs,
resolves route ownership, and reports used keys, missing keys, orphaned
keys, locale gaps, and hardcoded copy candidates.

Cover the scanner with fixture-heavy tests for entry discovery, route
resolution, namespace ownership, dynamic copy access, and hardcoded
metadata detection. Strengthen *public-copy* tests to preserve landing
array shapes and keep locale catalog structures aligned.

Update *type-check* to run *next typegen* first and use the dedicated
*tsconfig.typecheck.json* so generated Next route types are included in
type validation.
@greptile-apps

greptile-apps Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds a developer CLI (bun run i18n:catalog) under apps/tradinggoose/scripts/i18n-catalog that performs static AST analysis to report used keys, missing keys, target-locale gaps, orphaned keys, dynamic-protected roots, and hardcoded copy candidates, scoped either to a single route or the full app. The PR also tightens the type-check script to run next typegen first and point tsc at a dedicated tsconfig.typecheck.json, and adds two snapshot-style test guards to public-copy.test.ts.

  • scan.ts — ~3 500-line TypeScript AST walker: parses files with the TypeScript compiler API, builds per-file semantic descriptors for i18n root/translator bindings, and emits coverage/hardcoded-candidate records for each reachable entry point.
  • catalog.ts / entries.ts / ownership.ts — report builder, Next.js entry-point discovery, and the route-ownership/namespace-mapping tables; the CLI façade in index.ts wires them together with --route, --all, and --with-orphans flags.
  • public-copy.test.ts — adds landing-array shape guards and a cross-locale catalog shape alignment test using normalizeShape.

Confidence Score: 4/5

Safe to merge after adding a changelog file; the tooling changes are dev-only and do not touch any production runtime paths.

The PR introduces a substantial new developer tool (AST-based i18n scanner) and a tighter type-check script. Neither path touches production runtime code, billing, Yjs collaboration, or data persistence, so the blast radius of any issue is limited to developer workflow. The one blocking gap is the missing changelog entry required for every staging PR. Two additional findings in catalog.ts — the hardcoded .en reference and the O(n²) orphan filter — are correctness and performance concerns in the tool itself but don't affect shipped code.

apps/tradinggoose/scripts/i18n-catalog/catalog.ts warrants a second look for the default-locale hardcoding and the orphan-key filter performance. A dated changelog file under changelog/ must be added before merging to staging.

Important Files Changed

Filename Overview
apps/tradinggoose/scripts/i18n-catalog/catalog.ts Report builder: loads all locale catalogs, indexes keys, computes missing/orphaned keys and target-locale gaps. Orphan detection uses Array.includes (O(n²)) and the default-locale catalog is accessed via hardcoded .en instead of catalogs[defaultLocale].
apps/tradinggoose/scripts/i18n-catalog/scan.ts ~3500-line TypeScript AST scanner: collects import/export bindings, propagates descriptor semantics across files, and emits coverage and hardcoded-candidate records for each entry invocation. Logic is dense but well-structured; no direct correctness issues found.
apps/tradinggoose/scripts/i18n-catalog/entries.ts Next.js entry-point discovery: walks the app directory, discovers localized page/layout files, and resolves route-boundary entries. Logic correctly distinguishes localized vs. non-localized boundary entries.
apps/tradinggoose/scripts/i18n-catalog/ownership.ts Route-ownership rules table and namespace derivation helpers. Static table is straightforward; route pattern matching and specificity comparison look correct.
apps/tradinggoose/scripts/i18n-catalog/index.ts CLI façade: parses flags, delegates to scan/report builders, and formats output. Guard logic for flag combinations is correct; error paths set process.exitCode appropriately.
apps/tradinggoose/package.json Adds i18n:catalog script and updates type-check to run next typegen then point tsc at tsconfig.typecheck.json. Changes are targeted and correct.
apps/tradinggoose/tsconfig.typecheck.json Extends base tsconfig and adds .next/types/**/*.ts and explicit entry files so tsc sees generated Next route types. Minimal and correct.
apps/tradinggoose/i18n/public-copy.test.ts Adds two regression guards: landing array shape checks and cross-locale catalog shape alignment via normalizeShape. Both are straightforward additions to the existing test suite.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["bun run i18n:catalog\n--route /path | --all\n[--with-orphans] [--json]"] --> B{Flag validation}
    B -->|invalid combo| ERR["throw Error"]
    B -->|--route| C["scanCatalogProjectWithContext\nmode: 'route'"]
    B -->|--all| D["scanCatalogProjectWithContext\nmode: 'all'"]
    C --> E["resolveRouteEntries\n(entries.ts)"]
    D --> F["discoverAllModeEntries\n(entries.ts)"]
    E --> G["collectReachableFiles\n(BFS over runtime imports)"]
    F --> G
    G --> H["buildAnalysisProjectFiles\n(load TS AST per file)"]
    H --> I["populateProjectFileTypeRoots\n(propagate i18n type descriptors)"]
    I --> J["buildGlobalFunctionSemantics\n(infer translator/root descriptors)"]
    J --> K["scanFile per entry\n(scan.ts AST walker)"]
    K --> L["CatalogScanResult\ncoverage + hardcoded candidates"]
    L --> M{withOrphans?}
    M -->|yes| N["second all-mode scan\nfor globalScanResult"]
    M -->|no| O["buildCatalogReport\n(catalog.ts)"]
    N --> O
    O --> P["loadLocaleCatalogs\nen.json / es.json / zh.json"]
    P --> Q["buildCatalogIndex\n(leaf map + objectPaths)"]
    Q --> R["buildCoverageSummary\nmissing keys + used keys"]
    R --> S["buildTargetLocaleGaps\nper non-default locale"]
    S --> T["CatalogReport\nusedKeys / missingKeys / gaps\norphanedKeys / hardcodedCandidates"]
    T -->|--json| U["JSON stdout"]
    T -->|text| V["Formatted text stdout"]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A["bun run i18n:catalog\n--route /path | --all\n[--with-orphans] [--json]"] --> B{Flag validation}
    B -->|invalid combo| ERR["throw Error"]
    B -->|--route| C["scanCatalogProjectWithContext\nmode: 'route'"]
    B -->|--all| D["scanCatalogProjectWithContext\nmode: 'all'"]
    C --> E["resolveRouteEntries\n(entries.ts)"]
    D --> F["discoverAllModeEntries\n(entries.ts)"]
    E --> G["collectReachableFiles\n(BFS over runtime imports)"]
    F --> G
    G --> H["buildAnalysisProjectFiles\n(load TS AST per file)"]
    H --> I["populateProjectFileTypeRoots\n(propagate i18n type descriptors)"]
    I --> J["buildGlobalFunctionSemantics\n(infer translator/root descriptors)"]
    J --> K["scanFile per entry\n(scan.ts AST walker)"]
    K --> L["CatalogScanResult\ncoverage + hardcoded candidates"]
    L --> M{withOrphans?}
    M -->|yes| N["second all-mode scan\nfor globalScanResult"]
    M -->|no| O["buildCatalogReport\n(catalog.ts)"]
    N --> O
    O --> P["loadLocaleCatalogs\nen.json / es.json / zh.json"]
    P --> Q["buildCatalogIndex\n(leaf map + objectPaths)"]
    Q --> R["buildCoverageSummary\nmissing keys + used keys"]
    R --> S["buildTargetLocaleGaps\nper non-default locale"]
    S --> T["CatalogReport\nusedKeys / missingKeys / gaps\norphanedKeys / hardcodedCandidates"]
    T -->|--json| U["JSON stdout"]
    T -->|text| V["Formatted text stdout"]
Loading

Fix All in Codex Fix All in Claude Code Fix All in Cursor

Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
apps/tradinggoose/package.json:15
**Missing changelog entry for staging target**

This PR targets `staging` but adds no dated markdown file under `changelog/`. Per repo convention, every PR that lands on `staging` must include exactly one new dated changelog file. Please add a file such as `changelog/July-06-2026.md` documenting this branch before merging.

### Issue 2 of 3
apps/tradinggoose/scripts/i18n-catalog/catalog.ts:289-294
**Quadratic orphan-key check**`usedKeys` is a sorted array, so `.includes()` scans it linearly for every leaf in the catalog. With large catalogs (thousands of keys) this becomes O(keys × usedKeys). Convert `usedKeys` to a `Set` once before the filter.

```suggestion
  const usedKeySet = globalCoverageSummary ? new Set(globalCoverageSummary.usedKeys) : null
  const orphanedKeys = hasGlobalScan
    ? [...englishCatalogIndex.leafMap.keys()]
        .filter((pathKey) => !usedKeySet!.has(pathKey))
        .filter((pathKey) => isOwnedPath(pathKey, ownedNamespaces))
        .sort()
    : null
```

### Issue 3 of 3
apps/tradinggoose/scripts/i18n-catalog/catalog.ts:274
**Hardcoded `.en` instead of `defaultLocale`**`buildTargetLocaleGaps` correctly derives target locales dynamically via `defaultLocale`, but the main catalog index is built from `catalogs.en`. If `defaultLocale` ever changes, missing-key and orphan reports would silently compare against the wrong locale's catalog. Use `catalogs[defaultLocale]` to keep both paths aligned.

```suggestion
  const englishCatalogIndex = buildCatalogIndex(catalogs[defaultLocale])
```

Reviews (1): Last reviewed commit: "style(i18n): align catalog tooling with ..." | Re-trigger Greptile

@@ -15,12 +15,13 @@
"start": "next start",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Missing changelog entry for staging target

This PR targets staging but adds no dated markdown file under changelog/. Per repo convention, every PR that lands on staging must include exactly one new dated changelog file. Please add a file such as changelog/July-06-2026.md documenting this branch before merging.

Rule Used: Any PR targeting staging must add exactly one ne... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/tradinggoose/package.json
Line: 15

Comment:
**Missing changelog entry for staging target**

This PR targets `staging` but adds no dated markdown file under `changelog/`. Per repo convention, every PR that lands on `staging` must include exactly one new dated changelog file. Please add a file such as `changelog/July-06-2026.md` documenting this branch before merging.

**Rule Used:** Any PR targeting `staging` must add exactly one ne... ([source](greptile.json))

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Codex Fix in Claude Code Fix in Cursor

Comment on lines +289 to +294
const orphanedKeys = hasGlobalScan
? [...englishCatalogIndex.leafMap.keys()]
.filter((pathKey) => !globalCoverageSummary!.usedKeys.includes(pathKey))
.filter((pathKey) => isOwnedPath(pathKey, ownedNamespaces))
.sort()
: null

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Quadratic orphan-key checkusedKeys is a sorted array, so .includes() scans it linearly for every leaf in the catalog. With large catalogs (thousands of keys) this becomes O(keys × usedKeys). Convert usedKeys to a Set once before the filter.

Suggested change
const orphanedKeys = hasGlobalScan
? [...englishCatalogIndex.leafMap.keys()]
.filter((pathKey) => !globalCoverageSummary!.usedKeys.includes(pathKey))
.filter((pathKey) => isOwnedPath(pathKey, ownedNamespaces))
.sort()
: null
const usedKeySet = globalCoverageSummary ? new Set(globalCoverageSummary.usedKeys) : null
const orphanedKeys = hasGlobalScan
? [...englishCatalogIndex.leafMap.keys()]
.filter((pathKey) => !usedKeySet!.has(pathKey))
.filter((pathKey) => isOwnedPath(pathKey, ownedNamespaces))
.sort()
: null
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/tradinggoose/scripts/i18n-catalog/catalog.ts
Line: 289-294

Comment:
**Quadratic orphan-key check**`usedKeys` is a sorted array, so `.includes()` scans it linearly for every leaf in the catalog. With large catalogs (thousands of keys) this becomes O(keys × usedKeys). Convert `usedKeys` to a `Set` once before the filter.

```suggestion
  const usedKeySet = globalCoverageSummary ? new Set(globalCoverageSummary.usedKeys) : null
  const orphanedKeys = hasGlobalScan
    ? [...englishCatalogIndex.leafMap.keys()]
        .filter((pathKey) => !usedKeySet!.has(pathKey))
        .filter((pathKey) => isOwnedPath(pathKey, ownedNamespaces))
        .sort()
    : null
```

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex Fix in Claude Code Fix in Cursor


export function buildCatalogReport(options: BuildReportOptions): CatalogReport {
const catalogs = loadLocaleCatalogs(options.projectRoot)
const englishCatalogIndex = buildCatalogIndex(catalogs.en)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Hardcoded .en instead of defaultLocalebuildTargetLocaleGaps correctly derives target locales dynamically via defaultLocale, but the main catalog index is built from catalogs.en. If defaultLocale ever changes, missing-key and orphan reports would silently compare against the wrong locale's catalog. Use catalogs[defaultLocale] to keep both paths aligned.

Suggested change
const englishCatalogIndex = buildCatalogIndex(catalogs.en)
const englishCatalogIndex = buildCatalogIndex(catalogs[defaultLocale])
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/tradinggoose/scripts/i18n-catalog/catalog.ts
Line: 274

Comment:
**Hardcoded `.en` instead of `defaultLocale`**`buildTargetLocaleGaps` correctly derives target locales dynamically via `defaultLocale`, but the main catalog index is built from `catalogs.en`. If `defaultLocale` ever changes, missing-key and orphan reports would silently compare against the wrong locale's catalog. Use `catalogs[defaultLocale]` to keep both paths aligned.

```suggestion
  const englishCatalogIndex = buildCatalogIndex(catalogs[defaultLocale])
```

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex Fix in Claude Code Fix in Cursor

agualdron added 2 commits July 7, 2026 08:47
…163)

Extract graph reachability, runtime scanning, semantic analysis, and shared
descriptor logic from *scan.ts* into dedicated modules to reduce scanner
complexity and isolate responsibilities.

Move shared catalog test fixtures into *test-utils.ts* and replace the
monolithic *index.test.ts* suite with focused graph, runtime, semantics,
scan, and report test files.
Document the feat/i18n_tooling branch in the dated staging changelog.

Summarize the new i18n catalog tooling, type-check updates,
and test coverage noted for the branch diff against origin/staging.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant