Two-phase normalize flow: extract hardcoded strings from source code into .contentrain/ (Phase 1) and patch source files with content references (Phase 2). Use when normalizing, extracting content, or replacing hardcoded strings.
Installs into .claude/skills of the current project.
Are you the author of Contentrain Normalize?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/contentrain-contentrain-normalize)
---
name: contentrain-normalize
description: "Two-phase normalize flow: extract hardcoded strings from source code into .contentrain/ (Phase 1) and patch source files with content references (Phase 2). Use when normalizing, extracting content, or replacing hardcoded strings."
metadata:
author: Contentrain
version: "1.0.0"
---
# Contentrain Normalize
Normalize converts a codebase with hardcoded strings into a Contentrain-managed content architecture. It runs in two independent phases, each producing a separate branch for review.
- **Phase 1 (Extraction):** Pull content from source code into `.contentrain/` structure. Source files are NOT modified.
- **Phase 2 (Reuse):** Patch source files to replace hardcoded strings with content references. Requires completed extraction.
Phase 1 alone is valuable: content becomes manageable in Studio, translatable, and publishable without touching source code.
---
## MUST Rules
- MUST scan before extract (`contentrain_scan` → `contentrain_apply`)
- MUST `dry_run: true` before `dry_run: false` for every `contentrain_apply` call
- MUST merge Phase 1 branch before starting Phase 2
- MUST run `npx contentrain generate` after Phase 2 completes
- MUST NOT reuse without scoped model or domain (whole-project patching is blocked)
- MUST NOT patch `.contentrain/` files via reuse (content files are read-only for reuse)
- MUST NOT exceed 100 patches per `contentrain_apply` call
## Transport Requirements
Normalize (`contentrain_scan` and `contentrain_apply`) requires **local
disk access** — AST scanners walk the source tree and patch files in
place. It runs only on a `LocalProvider` (stdio transport, or an HTTP
transport configured with a `LocalProvider`).
Remote providers (`GitHubProvider`, `GitLabProvider`, future
`BitbucketProvider`) expose `astScan: false`, `sourceRead: false`, and
`sourceWrite: false`. Calling these tools over a remote provider
returns a uniform capability error:
```json
{
"error": "contentrain_scan requires local filesystem access.",
"capability_required": "astScan",
"hint": "This tool is unavailable when MCP is driven by a remote provider. Use a LocalProvider or the stdio transport."
}
```
If the agent is driving a remote-only MCP session, normalize must run
in a separate local-checkout session before the extracted content
branch is pushed.
---
## Two-Phase Architecture
| Aspect | Phase 1: Extraction | Phase 2: Reuse |
|--------|-------------------|----------------|
| Purpose | Pull content from source to `.contentrain/` | Patch source files with content references |
| Scope | Full project scan | Per model or per domain |
| Source files modified | No | Yes |
| Branch pattern | `cr/normalize/extract/{domain}/{timestamp}` | `cr/normalize/reuse/{model}/{locale}/{timestamp}` |
| Prerequisite | Initialized `.contentrain/` | Completed extraction (content exists in `.contentrain/`) |
| Workflow mode | Always `review` | Always `review` |
| Standalone value | Yes -- content is manageable in Studio immediately | Depends on Phase 1 |
---
## Agent vs MCP Responsibilities
### Agent (Intelligence Layer)
- **Decide what is content vs code.** Semantic judgment requiring context understanding.
- **Assign domain grouping.** Determine which domain each piece of content belongs to.
- **Determine model structure.** Group related content into models, choose the right kind.
- **Create replacement expressions.** Stack-specific, requires framework knowledge (Vue/Nuxt, React/Next, Svelte, Astro, SDK).
- **Filter false positives** from scan candidates.
- **Evaluate and group** candidates into logical models.
### MCP (Deterministic Infrastructure)
- **Scan the file system** (build graph, find candidates).
- **Read and write files** in `.contentrain/`.
- **Patch source files** with exact string replacement (agent provides the replacement expression).
- **Create branches, commit, validate** all changes.
- **Enforce guardrails** (file limits, type restrictions, dry-run requirement).
MCP is framework-agnostic. It does NOT know how `{t('key')}` differs from `{{ $t('key') }}`. The agent provides all stack-specific logic.
---
## Guardrails
| Guardrail | Limit | Rationale |
|-----------|-------|-----------|
| Allowed source file types | `.vue`, `.tsx`, `.jsx`, `.ts`, `.js`, `.mjs`, `.astro`, `.svelte` | Only scan files that can contain UI content |
| Max files per scan | 500 | Prevent scanning entire monorepos |
| Max files per apply | 100 | Keep diffs reviewable |
| Dry-run before apply | **MANDATORY** | Preview all changes before executing |
| Workflow mode | Always `review` | Normalize changes are never auto-merged |
| Reuse scope | Per model or per domain | No whole-project reuse in one operation |
| Reuse prerequisite | Extraction must be complete | Content must exist in `.contentrain/` before patching source |
Attempting to apply without a prior dry-run will be rejected. Exceeding file limits will truncate results with a warning.
---
## Phase 1: Extraction (Step-by-Step)
### 1. Check Project State
Call `contentrain_status` to confirm the project is initialized and note supported locales. Call `contentrain_describe_format` to understand the four storage formats (dictionary, collection, singleton, document) before deciding model structure.
### 2. Build the Project Graph
Call `contentrain_scan(mode: "graph")` to build the import/component dependency graph. Use this to understand which components belong to which pages, identify shared vs page-specific components, and prioritize files.
### 3. Find Candidates
Call `contentrain_scan(mode: "candidates")` to find hardcoded strings. The scan returns candidates with file paths, line numbers, string values, and surrounding context.
### 4. Evaluate Candidates (Agent Intelligence)
- **Filter false positives:** Remove CSS values, technical identifiers, import paths, variable names, config values, log messages, test strings. See [What Is Content](references/what-is-content.md) for full heuristics.
- **Assign domains:** Group candidates by domain (e.g., `marketing`, `blog`, `ui`, `system`).
- **Determine model types:** Choose dictionary, singleton, collection, or document kind for each group.
- **Structure fields:** Define field names and assign candidate strings to fields.
### 5. Preview Extraction
Call `contentrain_apply(mode: "extract", dry_run: true)` to generate a preview. Review the dry-run output: verify model definitions, content assignments, and check for missed or misclassified candidates. Show the preview to the user.
### 6. Execute Extraction
After user approval, call `contentrain_apply(mode: "extract", dry_run: false)`. This creates model definitions and content files in `.contentrain/` on a `cr/normalize/extract/{timestamp}` branch. Source files are NOT modified.
### 7. Validate and Submit
Call `contentrain_validate` to check schema compliance, i18n completeness, and duplicate entries. Call `contentrain_submit` to push the branch for review. Normalize operations always use `review` workflow mode.
Tell the user: "Phase 1 complete. Content is now in Contentrain and can be managed, translated, and published from Studio. When ready, proceed with Phase 2 to update source files."
### Step 5: User Review
Two paths — agent chooses based on context:
**Path A — Browser Review (recommended for visual diff):**
1. Ensure `contentrain serve` is running (or ask user to start it)
2. Summarize the dry_run output to the user
3. Direct user to http://localhost:3333/normalize
4. Explain: "On this page you can review the extraction plan and Approve or Reject it."
5. Wait for user decision
**Path B — Terminal Review:**
1. Show dry_run output to user
2. Ask for explicit approval
3. If approved: `contentrain_apply(mode: 'extract', dry_run: false)` → execute
4. `contentrain_validate` → `contentrain_submit`
5. `contentrain_merge(branch: 'cr/normalize/extract/...', confirm: true)` to merge locally
### Step 6: Merge Phase 1 Branch
Phase 2 CANNOT start until Phase 1 branch is merged. Three options:
- **Browser:** localhost:3333/branches → click Merge
- **MCP Tool:** `contentrain_merge(branch: 'cr/normalize/extract/...', confirm: true)`
- **Git platform:** Create PR → review → merge
Verify with `contentrain_status` — check that extract branch is no longer in unmerged list.
### Step 7: Phase 2 Prerequisite Check
Before starting reuse:
1. Call `contentrain_status` and verify extract branch is merged
2. If not merged → inform user and wait
3. If merged → proceed to Phase 2 reuse
### Step 8: SDK Regeneration (after Phase 2)
After all reuse operations are complete, source files now use `#contentrain` imports.
The SDK client MUST be regenerated:
```bash
npx contentrain generate
```
Without this, `#contentrain` imports will fail at build time.
See: **contentrain-generate** skill for details.
---
## Phase 2: Reuse (Step-by-Step)
Only start after Phase 1 is reviewed and merged.
### 1. Select Scope
List the extracted models and ask the user which model or domain to process first. Process one model or domain at a time to keep diffs small and reviewable.
### 2. Determine Replacement Expressions (Agent Intelligence)
Based on the project's tech stack, determine the correct replacement pattern. See [Reuse Details](references/reuse.md) for the full replacement expressions table by stack. Also determine any necessary import statements for patched files.
### 3. Preview Reuse
Call `contentrain_apply(mode: "reuse", scope: { model: "<model-id>" }, patches: [...], dry_run: true)`. Review: verify each replacement is correct, import statements will be added, no non-content strings are being replaced, component structure is preserved.
### 4. Execute Reuse
After user confirmation, call `contentrain_apply(mode: "reuse", scope: { model: "<model-id>" }, patches: [...], dry_run: false)`. This patches source files and creates a `cr/normalize/reuse/{model}/{timestamp}` branch.
### 5. Validate and Submit
Call `contentrain_validate` to verify source files parse correctly, content references resolve, and no strings were missed or double-replaced. Call `contentrain_submit` to push the branch for review.
### 6. Repeat
Ask the user which model or domain to process next. Repeat steps 1-5 for each remaining model until all extracted content is referenced in source code.
---
## Domain and Model Grouping Guidelines
### Domain Assignment
| Content Location | Suggested Domain |
|------------------|-----------------|
| Landing page, marketing sections | `marketing` |
| Blog, articles, posts | `blog` |
| Navigation, footer, header | `ui` |
| Error messages, validation | `system` |
| Product pages, e-commerce | `product` |
| Documentation, help | `docs` |
| User-facing app strings | `app` |
### Model Kind Selection
| Content Pattern | Kind | Rationale |
|----------------|------|-----------|
| One set of fields per page section | `singleton` | Hero, features, pricing header |
| Multiple items of same type | `collection` | Team members, FAQs, testimonials |
| Long-form with metadata | `document` | Blog posts, documentation pages |
| Key-value UI strings | `dictionary` | Error messages, button labels, form labels |
---
## Common Mistakes
| Mistake | Correct Approach |
|---------|-----------------|
| Extracting CSS values as content | Only extract user-visible text |
| Creating one model per component | Group related content into shared models |
| Skipping dry-run | ALWAYS preview before apply |
| Auto-merging normalize changes | Normalize ALWAYS uses review mode |
| Reusing before extraction is merged | Wait for extraction review and merge first |
| Processing all models in one reuse | Scope reuse to one model/domain at a time |
| Ignoring project graph | Use graph output to understand component relationships |
| Hardcoding replacement patterns | Detect the project's i18n stack and use its conventions |
| Extracting strings with runtime variables | Split parameterized strings: extract the static part, leave interpolation in code |
---
## References
- [Extraction Details](references/extraction.md) -- Phase 1 extraction rules, parameters, and examples
- [Reuse Details](references/reuse.md) -- Phase 2 replacement expressions by stack and patching rules
- [What Is Content](references/what-is-content.md) -- Heuristics for identifying content vs code strings
## Framework Guides
Read the guide for the detected stack during Phase 2, after
[Reuse Details](references/reuse.md) has given you the general replacement
rules. Each guide covers that framework's i18n conventions, import style, and
component patterns. Load only the one that matches the project.
| Stack | Guide |
|-------|-------|
| Vue | [vue.md](../../frameworks/vue.md) |
| Nuxt | [nuxt.md](../../frameworks/nuxt.md) |
| Next.js | [next.md](../../frameworks/next.md) |
| React | [react.md](../../frameworks/react.md) |
| Astro | [astro.md](../../frameworks/astro.md) |
| SvelteKit | [sveltekit.md](../../frameworks/sveltekit.md) |
| Node | [node.md](../../frameworks/node.md) |
| Expo | [expo.md](../../frameworks/expo.md) |
| React Native | [react-native.md](../../frameworks/react-native.md) |
## Related Skills
- **contentrain-serve** — Browser-based review UI for extraction approval
- **contentrain-generate** — SDK client regeneration (required after reuse)
- **contentrain-sdk** — @contentrain/query usage in source code
- **contentrain** — Core architecture and full MCP tool catalog