Package Exports
- restringjs
- restringjs/adapters
- restringjs/cli
- restringjs/config
- restringjs/server
Readme
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 and comments
- 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
# Bake overrides into source files (AST transform, preserves formatting)
npx restringjs bake "src/**/*.tsx"
# Dry run - preview what would change without writing
npx restringjs bake "src/**/*.tsx" --dry-run
# Use a custom overrides file (default: .restringjs-overrides.json)
npx restringjs bake "src/**/*.tsx" --overrides=my-overrides.json
# Show diffs between source defaults and current overrides
npx restringjs diff
# Check for stale overrides (keys that no longer exist in source)
npx restringjs validate
# Export current overrides to JSON
npx restringjs export > overrides.json
# Import overrides from JSON
npx restringjs import < overrides.json
# Clear all stored overrides
npx restringjs clearAPI 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
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 ☕