@dropshot/i18n-inline-editor
v0.2.0
Published
Framework-neutral React editor for messages rendered on a page
Keywords
Readme
@dropshot/i18n-inline-editor
A framework-neutral React editor for reviewing messages directly on a rendered screen during development. It identifies the supplied messages on the page, lets a user edit their translations, validates message placeholders, and reports the resulting changes through callbacks.
npm install @dropshot/i18n-inline-editorImport the component and its stylesheet where the editor is rendered:
import {InlineMessageEditor} from '@dropshot/i18n-inline-editor';
import '@dropshot/i18n-inline-editor/styles.css';
<InlineMessageEditor
messages={messages}
locale={locale}
sourceLocale={sourceLocale}
onPreview={setPreviewChanges}
/>Each message has an id, source, and translation. onPreview receives the pending changes after
each edit, so the consumer can show them on the page.
The editor shows a single control: a small globe button. Pressing it turns editing on and off, and its colour reports the state — grey when off, blue when on, amber once an edit is pending. There is no panel, and nothing is persisted; this release exists to see edited copy on the running screen.
Consumer responsibilities
The consumer owns everything outside the screen: use onPreview to apply the pending changes. The
component neither reads nor writes a catalog, API, or storage layer, and it does not save.
The consumer also owns environment gating. Render the editor only in the environments, routes, and access conditions where development editing is allowed. The package does not select environments or provide authorization.
Automatic IDs for Next.js and next-intl
Next.js 16 applications using next-intl 4 message extraction can add exact message IDs to rendered DOM elements during local development. Wrap the config returned by createNextIntlPlugin:
import {withI18nInlineEditor} from '@dropshot/i18n-inline-editor/next';
import createNextIntlPlugin from 'next-intl/plugin';
const withNextIntl = createNextIntlPlugin({
experimental: {
extract: {},
messages: {
path: './messages',
locales: ['ko', 'en'],
sourceLocale: 'ko',
},
},
});
// The app decides which environments may edit copy, and passes the same value to the provider.
export const inlineEditorEnabled =
process.env.NODE_ENV === 'development' && process.env.I18N_INLINE_EDITOR === 'true';
export default withI18nInlineEditor(withNextIntl(nextConfig), {enabled: inlineEditorEnabled});The package never reads the environment. Which environments, routes, and people may edit copy is the app's policy, and only the app knows it. Pass the same value to the build wrapper and to the provider — if they disagree, the editor appears on a screen whose messages carry no build IDs.
The adapter runs immediately after next-intl's extraction transform and reuses its generated message ID. It marks unambiguous intrinsic JSX usages with data-i18n-id; translated aria-label, title, alt, and placeholder attributes receive independent attribute markers. Indirect values, custom component props, and elements containing multiple direct messages remain available to the editor's text fallback instead of receiving a potentially incorrect ID.
The editor reports exact, fallback, and unlinked counts. If the adapter is missing or its supported Next.js/next-intl integration changes, it reports that the build plugin is inactive instead of silently claiming exact matches. The config adapter is a no-op unless both development gates are enabled.
Wiring a next-intl app in one step
@dropshot/i18n-inline-editor/next-intl bundles the runtime wiring so an application does not
reimplement catalog flattening, preview state, saving, or editor copy. Wrap the layout children
with it in place of NextIntlClientProvider:
import {I18nInlineEditorProvider} from '@dropshot/i18n-inline-editor/next-intl';
export default async function RootLayout({children}: {children: ReactNode}) {
return (
<html lang={await getLocale()}>
<body>
<I18nInlineEditorProvider enabled={inlineEditorEnabled} sourceLocale="ko">
{children}
</I18nInlineEditorProvider>
</body>
</html>
);
}The server entry reads both catalogs with getMessages and imports the client editor only when
enabled is true, so a production build never pulls the editor into its client bundle. Interface copy defaults to Korean;
pass labels to change it.
Pass locales to place every locale side by side. Without it the editor shows the source text and the
translation for the screen being viewed — which are the same value while viewing the source language,
leaving nothing to compare. Only the locale on screen is editable: the page was not rendered in the
other locales, so an edit there could not be seen. Already-read catalogs are not fetched again.
exclude is optional. It drops top-level keys that did not come from the extracted catalog —
formatting values and other hand-written settings merged into the same message tree. Leaving them in
is harmless: they are never rendered, so no frame is drawn for them. Use it to keep values that are
not translated copy out of the editable set.
Two behaviours follow from how next-intl works, and are worth stating before the editor is handed to anyone:
- Only messages rendered by client components update live. Preview replaces the messages held by the client provider, and a message rendered in a server component is already fixed in the server output, so it keeps its original text until the page is requested again.
- One generated ID can appear in several namespaces. Extraction derives the ID from the source
text alone, and the DOM marker carries only that ID, so identical Korean copy in two namespaces is
one editable entry and saving updates every place it appears. Keys that share a name but hold
different source text split the other way: their identity is the path, so
index.titleandmetadata.titlestay separate entries and are matched by rendered text rather than a marker.
Requires next-intl as a peer dependency. The getRequestConfig callback must honour the locale
argument, otherwise the source catalog cannot be read alongside the translated one.
Customization
Provide labels to replace the built-in interface text, formatDiagnostic to replace validation
diagnostics, trigger to replace the globe inside the button, and localeLabel or
sourceLocaleLabel to control locale names shown in the editor.
Match diagnostics go to the console when the editor opens: how many messages were linked by build ID, how many by rendered text, and how many the build plugin marked but could not link. They are there for whoever wires the editor up, not for whoever edits the copy.
License
MIT
