restringjs
v0.2.3
Published
Live string editor for React — edit text in-context, then bake changes into code
Downloads
96
Maintainers
Readme
restringjs
Live string editor for React. Edit text in-context, then bake changes into code.
restringjs gives your team a sidebar where they can tweak every user-facing string in your app, see changes instantly, then permanently bake those edits into your source files with a single CLI command. No CMS. No runtime overhead in production.
Why
You have a marketing team that wants to change "Get Started" to "Start Free Trial." Today that's a Jira ticket, a PR, a deploy. With restringjs, they open the sidebar, make the edit, and a developer runs npx restringjs bake to commit it. Or you skip the middleman entirely and bake it yourself.
The key insight: most string changes don't need a CMS. They need a good workflow for getting text changes into source code.
Features
- Live editing sidebar with search, filtering, and section grouping
- Zero bytes in production - the sidebar tree-shakes completely when disabled
- Bake & eject - AST transforms apply overrides directly into source files, preserving formatting, comments, and quote style. Handles nested objects, arrays, and
as const - Full CLI toolkit -
bake,diff,validate,export,import,clearwith--prefixsupport for bridging DB key formats - ICU MessageFormat - syntax validation, variable chips, plural grouping with locale-aware labels
- i18next support - auto-detects
{{variable}}and$t()patterns - Visual highlight mode - overlay registered DOM elements, click to jump to the editor
- RTL-aware - inputs auto-detect text direction
- Rich text - opt-in HTML/Markdown preservation per field
- Pluggable storage - memory, localStorage, and REST adapters included (or write your own)
- Server-side rendering - Next.js App Router and Pages Router helpers
- TypeScript-first - full types, no
anyleakage
Demo
See all features in action without setting up a project:
git clone https://github.com/maki-q/restringjs.git
cd restringjs
pnpm install
pnpm demoFive pages covering basic usage, FAQ sections, i18n (ICU + i18next), rich text editing, and visual highlight mode.
Quick Start
npm install restringjs1. Wrap your app
import { RestringProvider, RestringSidebar } from 'restringjs';
import { createLocalStorageAdapter } from 'restringjs/adapters';
const adapter = createLocalStorageAdapter();
function App() {
return (
<RestringProvider enabled={process.env.NODE_ENV === 'development'} adapter={adapter}>
<YourApp />
<RestringSidebar />
</RestringProvider>
);
}2. Register strings
import { useRestring } from 'restringjs';
function Hero() {
const title = useRestring({
path: 'hero.title',
defaultValue: 'Welcome to our app',
section: 'marketing',
});
const subtitle = useRestring({
path: 'hero.subtitle',
defaultValue: 'The best way to manage your strings',
section: 'marketing',
});
return (
<section>
<h1>{title}</h1>
<p>{subtitle}</p>
</section>
);
}3. Bake changes into code
npx restringjs bake "src/**/*.tsx"Your source files now contain the edited strings. No runtime overhead. No adapter needed in production.
Visual Highlight Mode
Overlay registered DOM elements so you can see exactly which strings are editable:
import { RestringProvider, RestringSidebar, RestringHighlight } from 'restringjs';
<RestringProvider enabled adapter={adapter}>
<YourApp />
<RestringHighlight />
<RestringSidebar />
</RestringProvider>Click any highlighted element to jump to its field in the sidebar. Overlays track scroll and resize automatically.
Configure the highlight color via the provider:
<RestringProvider
enabled
adapter={adapter}
defaultHighlightMode={true}
highlightColor="#ff6b6b"
>ICU MessageFormat
restringjs understands ICU syntax out of the box:
const greeting = useRestring({
path: 'greeting',
defaultValue: 'Hello {name}, you have {count, plural, one {# message} other {# messages}}',
format: 'icu',
});The sidebar shows variable chips, validates syntax in real time, and groups plural forms with locale-aware labels.
i18next Support
const welcome = useRestring({
path: 'welcome',
defaultValue: 'Welcome {{userName}}! See $t(features.title) for details.',
format: 'i18next',
});Format detection is automatic. You can omit format and let restringjs figure it out.
Sections
Group related fields in the sidebar:
import { useRegisterSection, useRestring } from 'restringjs';
function PricingPage() {
useRegisterSection({
id: 'pricing',
label: 'Pricing Page',
order: 2,
description: 'All pricing-related copy',
});
const headline = useRestring({
path: 'pricing.headline',
defaultValue: 'Simple, transparent pricing',
section: 'pricing',
});
return <h1>{headline}</h1>;
}Adapters
import {
createMemoryAdapter,
createLocalStorageAdapter,
createRestAdapter,
} from 'restringjs/adapters';
// Ephemeral (lost on refresh)
const memory = createMemoryAdapter();
// Persists in browser localStorage
const local = createLocalStorageAdapter('my-app:overrides');
// Persists to your API
const rest = createRestAdapter('https://api.example.com/overrides', {
headers: { Authorization: 'Bearer ...' },
});Custom adapter
Implement three async methods:
import type { RestringAdapter } from 'restringjs';
const myAdapter: RestringAdapter = {
async load() {
// Return Record<string, string> of field path -> override value
const res = await fetch('/api/overrides');
return res.json();
},
async save(overrides) {
await fetch('/api/overrides', {
method: 'PUT',
body: JSON.stringify(overrides),
});
},
async clear() {
await fetch('/api/overrides', { method: 'DELETE' });
},
};Server-Side Rendering
Next.js App Router
import { createServerApply } from 'restringjs/server';
const apply = createServerApply(async () => {
// Load overrides from your database, cookie, API, etc.
return { 'hero.title': 'Server-rendered override' };
});
export default async function Page() {
const strings = await apply({
hero: { title: 'Default title', subtitle: 'Default subtitle' },
});
return <h1>{strings.hero.title}</h1>;
}Next.js Pages Router
import { withRestringOverrides, serverApply } from 'restringjs/server';
export const getServerSideProps = async () => {
const { restringOverrides } = await withRestringOverrides(() => loadOverrides())();
const strings = serverApply(defaultStrings, restringOverrides);
return { props: { strings } };
};CLI
Every command supports --help for full usage details.
bake - Write overrides into source files
AST transform via ts-morph. Preserves formatting, comments, quote style, and code structure. Handles nested objects, arrays, as const, and deeply nested paths.
# Bake overrides into source files
npx restringjs bake "src/**/*.tsx"
# Dry run - preview what would change without writing
npx restringjs bake "src/**/*.tsx" --dry-run
# Custom overrides file (default: .restringjs-overrides.json)
npx restringjs bake "src/**/*.tsx" --overrides=my-overrides.json
# Bridge DB key format to source variable names
# If your DB stores "home.title" but source has `const strings = { home: { title: "..." } }`:
npx restringjs bake "src/**/*.ts" --prefix=stringsBake reports unmatched override keys to stderr so you know if any overrides didn't find a matching path in source.
diff - Compare source strings against overrides
npx restringjs diff "src/**/*.ts" --overrides=overrides.json
npx restringjs diff "src/**/*.ts" --overrides=overrides.json --prefix=stringsPrints a colored table showing path, original value, and override value for each changed field.
validate - Check for stale or empty overrides
npx restringjs validate "src/**/*.ts" --overrides=overrides.json
npx restringjs validate "src/**/*.ts" --overrides=overrides.json --prefix=stringsFlags stale keys (overrides referencing paths that no longer exist in source) and empty values. Exits with code 1 if any issues found - useful in CI.
export - Extract all strings from source
# Print to stdout
npx restringjs export "src/**/*.ts"
# Write to file
npx restringjs export "src/**/*.ts" --output=strings.json
# Strip variable prefix from keys
npx restringjs export "src/**/*.ts" --prefix=stringsWalks source files and extracts every string literal with its dot-path. Use this to bootstrap an overrides file or audit what's editable.
import - Merge overrides into a file
npx restringjs import --overrides=new-overrides.json
npx restringjs import --overrides=new-overrides.json --target=.restringjs-overrides.jsonReads a JSON override map and merges it into the target file (default .restringjs-overrides.json).
clear - Reset override file
npx restringjs clear
npx restringjs clear --target=.restringjs-overrides.jsonWrites {} to the target file.
The --prefix flag
Many apps store override keys without the source variable name (e.g. home.title in the database) while the AST resolver produces paths that include it (e.g. strings.home.title). The --prefix flag bridges this gap across all commands that work with both source files and overrides (bake, diff, validate, export).
API Reference
Hooks
| Hook | Description |
|------|-------------|
| useRestring(config) | Register a field, return its current value (override or default) |
| useRegister(config) | Register a field, return [value, setValue] tuple |
| useRegisterSection(config) | Register a sidebar section for grouping |
| useFieldValue(path) | Read a field's current value without registering it |
| useSnapshot() | Get the full store snapshot (fields, sections, overrides, dirty state) |
Components
| Component | Description |
|-----------|-------------|
| RestringProvider | Context provider. Set enabled to control sidebar availability. |
| RestringSidebar | The editing sidebar UI. Search, filter, edit, save. |
| RestringHighlight | Visual overlay mode. Renders borders on registered DOM elements. |
Provider Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| enabled | boolean | required | Whether editing mode is active |
| adapter | RestringAdapter | memory | Storage adapter for persisting overrides |
| defaultHighlightMode | boolean | true | Whether highlight overlays start enabled |
| highlightColor | string | '#4a6cf7' | CSS color for highlight overlays and accents |
Utilities
| Function | Description |
|----------|-------------|
| applyOverrides(obj, overrides) | Apply overrides to an object immutably |
| flattenObject(obj) | Flatten nested object to dot-path keys |
| unflattenObject(flat) | Reverse of flatten |
| detectFormat(value) | Auto-detect string format (icu, i18next, plain) |
| createStore(options?) | Create a standalone store instance |
Server Exports (restringjs/server)
| Function | Description |
|----------|-------------|
| serverApply(strings, overrides) | Apply overrides server-side, returns new object |
| createServerApply(loader) | Create a reusable apply function with an async override loader |
| withRestringOverrides(loader) | Pages Router helper for getServerSideProps |
Field Config
interface FieldConfig {
path: string; // Dot-path key, e.g. "hero.title"
defaultValue: string; // Value before any overrides
section?: string; // Group in sidebar
format?: 'icu' | 'i18next' | 'plain';
richText?: boolean; // Enable HTML/Markdown editing
description?: string; // Shown in sidebar
locale?: string; // e.g. 'en', 'fr'
}How It Works
- Register strings with
useRestring(). Each gets a unique dot-path key. - Edit in the sidebar. Changes are stored via your chosen adapter (localStorage, REST, etc.).
- Bake with the CLI. ts-morph rewrites your source files, replacing default values with overrides.
- Eject if you want. Remove restringjs entirely and your strings are just hardcoded values. No lock-in.
The bake step uses AST transforms (not regex), so it preserves your formatting, comments, and code structure.
Configuration
Optional config file for CLI defaults:
// restringjs.config.ts
import { defineConfig } from 'restringjs/config';
export default defineConfig({
sources: ['src/**/*.{ts,tsx}'],
locale: 'en',
format: 'icu',
adapter: {
type: 'rest',
endpoint: 'https://api.example.com/overrides',
},
});Requirements
- React 18+ (uses
useSyncExternalStore) - TypeScript 5+ recommended
- Node.js 18+ for CLI
Roadmap to 1.0
The following are planned before a stable 1.0 release:
- Template literal support in bake - Currently, backtick strings (
`Hello ${name}`) are gracefully skipped. Plain backtick strings without interpolation (NoSubstitutionTemplateLiteral) should be replaceable. - Full ICU MessageFormat validation - Current validation is brace-matching only. A proper AST parse would catch malformed plural/select syntax before save.
- import/clear for non-file adapters -
importandclearCLI commands only support file-based targets today. localStorage and REST adapter support is planned. - CHANGELOG - Proper release-by-release changelog (started in CHANGELOG.md, will be fleshed out with each release).
If any of these are blocking your use case, open an issue.
Contributing
git clone https://github.com/maki-q/restringjs.git
cd restringjs
pnpm install
pnpm check # typecheck + lint
pnpm test # run tests
pnpm demo # start demo appLicense
If you find this useful, consider buying me a coffee :coffee:
