ui-strings 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -6
- package/dist/cli.js +30 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,9 +15,13 @@
|
|
|
15
15
|
|
|
16
16
|
# UI Strings Scanner
|
|
17
17
|
|
|
18
|
-
UI Strings Scanner lists every string that could reach the user in a React (JSX/TSX) codebase
|
|
18
|
+
UI Strings Scanner lists every string that could reach the user in a React (JSX/TSX) codebase. It parses the AST with [ts-morph](https://ts-morph.com/) instead of running the code, so one command covers any React project without a build step or a dev server. Detection relies on JSX semantics, so the scanner is React-only.
|
|
19
19
|
|
|
20
|
-
Use it to audit hardcoded copy, review tone and wording, catch untranslated strings, or take stock before adopting i18n.
|
|
20
|
+
Use it to audit hardcoded copy, review tone and wording, catch untranslated strings, or take stock before adopting i18n. The scanner inventories strings and leaves translation-key management to your i18n tooling.
|
|
21
|
+
|
|
22
|
+
<img src="https://raw.githubusercontent.com/junseinagao/ui-strings-scanner/main/assets/report-page-view.png" alt="HTML report: every UI string grouped by route, with search, surface filters, and stats" />
|
|
23
|
+
|
|
24
|
+
<p align="center"><i>The HTML report for <a href="https://github.com/shadcn-ui/ui">shadcn/ui</a>: 27,000+ strings from 3,550 files, grouped by route and laid out like the page.</i></p>
|
|
21
25
|
|
|
22
26
|
## Quick Start
|
|
23
27
|
|
|
@@ -42,14 +46,28 @@ Scanning: /path/to/my-app/src/**/*.{ts,tsx,js,jsx}
|
|
|
42
46
|
→ /path/to/my-app/ui-strings-report/ui-strings.html
|
|
43
47
|
```
|
|
44
48
|
|
|
49
|
+
## Review copy, then fix it with your AI agent
|
|
50
|
+
|
|
51
|
+
The HTML report doubles as a copy-review tool. Spot a string that needs work, click ✎, and type the new copy. The report shows old → new and keeps your edits across reloads.
|
|
52
|
+
|
|
53
|
+
<img src="https://raw.githubusercontent.com/junseinagao/ui-strings-scanner/main/assets/report-inline-edit.png" alt="Inline editing in the report: the old string is struck through, the replacement shown next to it, with an edits bar at the bottom" />
|
|
54
|
+
|
|
55
|
+
Once your pass is done, **Copy fix prompt** turns every edit into one prompt with `file:line` locations. Paste it into Claude Code, Cursor, Codex, or any coding agent, and the agent rewrites the exact literals you marked.
|
|
56
|
+
|
|
57
|
+
<img src="https://raw.githubusercontent.com/junseinagao/ui-strings-scanner/main/assets/report-fix-prompt.png" alt="Fix Prompt modal: a generated prompt listing each edit as file path, line number, current text, and replacement text" />
|
|
58
|
+
|
|
59
|
+
**Table view** shows one row per string with surface, kind, context, and location columns. Rows copy out as TSV for spreadsheet work.
|
|
60
|
+
|
|
61
|
+
<img src="https://raw.githubusercontent.com/junseinagao/ui-strings-scanner/main/assets/report-table-view.png" alt="Table view: one row per string with surface, kind, context, and file:line location columns" />
|
|
62
|
+
|
|
45
63
|
## Features
|
|
46
64
|
|
|
47
65
|
- **Sentence-level extraction** - `<p>Hello <b>world</b>,<br />welcome</p>` comes out as one string. Embedded expressions become `{count}` placeholders.
|
|
48
66
|
- **Condition tracking** - Strings inside `cond ? A : B` or `cond && X` carry their condition, so you know when they show.
|
|
49
67
|
- **Symbol resolution** - An option array rendered with `.map()` links back to its declaration, across import chains and `@/` aliases.
|
|
50
|
-
- **Surface classification** - Every string
|
|
51
|
-
- **Noise filtering** - Class names, import paths, and directives never make the list.
|
|
52
|
-
- **Next.js aware** - Entries group by App Router route, and `metadata`
|
|
68
|
+
- **Surface classification** - Every string carries a tag for how it reaches the user: `visible` (rendered as-is), `interactive` (toasts, validation, API messages), `a11y` (`aria-*`, `alt`, `title`), `meta` (Next.js metadata), or `internal` (console/throw).
|
|
69
|
+
- **Noise filtering** - Class names, import paths, and directives never make the list. Detection combines a string's position (attribute, object key, call argument) with its shape, and treats non-Latin text as copy in any position, so mixed-language projects work.
|
|
70
|
+
- **Next.js aware** - Entries group by App Router route, and `metadata` lands under `meta`.
|
|
53
71
|
- **Fix prompts** - Edit strings in the HTML viewer, then copy all edits as a ready-to-run prompt for an AI coding agent.
|
|
54
72
|
|
|
55
73
|
## Usage
|
|
@@ -79,7 +97,7 @@ The scanner reads the target project's `tsconfig.json` when present and resolves
|
|
|
79
97
|
|
|
80
98
|
## Limitations
|
|
81
99
|
|
|
82
|
-
|
|
100
|
+
Strings built at runtime, such as API responses and computed values, stay invisible to static analysis. The scanner records copy passed across components at its declaration and resolves render sites only for the array `.map()` pattern. Unknown copy attributes fall back to lexical detection.
|
|
83
101
|
|
|
84
102
|
## Development
|
|
85
103
|
|
package/dist/cli.js
CHANGED
|
@@ -503,17 +503,30 @@ var surfaceFor = (classified, file) => {
|
|
|
503
503
|
}
|
|
504
504
|
return "visible";
|
|
505
505
|
};
|
|
506
|
-
var
|
|
507
|
-
const
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
506
|
+
var sharedPrefixLength = (files) => {
|
|
507
|
+
const first = files[0];
|
|
508
|
+
if (first === undefined) {
|
|
509
|
+
return 0;
|
|
510
|
+
}
|
|
511
|
+
let shared = first.length - 1;
|
|
512
|
+
for (const parts of files) {
|
|
513
|
+
const limit = Math.min(shared, parts.length - 1);
|
|
514
|
+
let i = 0;
|
|
515
|
+
while (i < limit && parts[i] === first[i]) {
|
|
516
|
+
i += 1;
|
|
517
|
+
}
|
|
518
|
+
shared = i;
|
|
512
519
|
}
|
|
513
|
-
const
|
|
514
|
-
|
|
515
|
-
|
|
520
|
+
const boundary = first.slice(0, shared).findIndex((segment) => segment === "src" || segment === "app");
|
|
521
|
+
return boundary === -1 ? shared : boundary;
|
|
522
|
+
};
|
|
523
|
+
var groupFor = (parts, hasAppDir) => {
|
|
524
|
+
const rest = parts[0] === "src" ? parts.slice(1) : parts;
|
|
525
|
+
if (hasAppDir && rest[0] === "app") {
|
|
526
|
+
const segments = rest.slice(1, -1).filter((segment) => segment !== "_dependencies" && !(segment.startsWith("(") && segment.endsWith(")")));
|
|
527
|
+
return `/${segments.slice(0, 2).join("/")}`;
|
|
516
528
|
}
|
|
529
|
+
const dirs = rest.slice(0, -1);
|
|
517
530
|
return dirs.slice(0, 2).join("/") || "(root)";
|
|
518
531
|
};
|
|
519
532
|
var DEFAULT_EXCLUDES = [
|
|
@@ -536,7 +549,13 @@ var scanProject = (options) => {
|
|
|
536
549
|
`${projectDir}/${srcGlob}`,
|
|
537
550
|
...excludes.map((glob) => `!${projectDir}/${glob}`)
|
|
538
551
|
]);
|
|
539
|
-
const
|
|
552
|
+
const relativeParts = sourceFiles.map((file) => relative(projectDir, file.getFilePath()).split(sep));
|
|
553
|
+
const prefixLength = sharedPrefixLength(relativeParts);
|
|
554
|
+
const hasAppDir = relativeParts.some((parts) => {
|
|
555
|
+
const stripped = parts.slice(prefixLength);
|
|
556
|
+
const rest = stripped[0] === "src" ? stripped.slice(1) : stripped;
|
|
557
|
+
return rest[0] === "app" && rest.length > 1;
|
|
558
|
+
});
|
|
540
559
|
const renderSites = new Map;
|
|
541
560
|
for (const sourceFile of sourceFiles) {
|
|
542
561
|
const relativeFile = relative(projectDir, sourceFile.getFilePath());
|
|
@@ -572,7 +591,7 @@ var scanProject = (options) => {
|
|
|
572
591
|
const entries = [];
|
|
573
592
|
for (const sourceFile of sourceFiles) {
|
|
574
593
|
const relativeFile = relative(projectDir, sourceFile.getFilePath());
|
|
575
|
-
const group = groupFor(relativeFile, hasAppDir);
|
|
594
|
+
const group = groupFor(relativeFile.split(sep).slice(prefixLength), hasAppDir);
|
|
576
595
|
const consumed = new Set;
|
|
577
596
|
const nodeKey = (node) => `${node.getPos()}:${node.getEnd()}`;
|
|
578
597
|
const push = (text, node, classified) => {
|