i18n-key-inspector

See which translation key produced any text on the page.
2,338 1
Install
npm install i18n-key-inspector
Latest Version:0.2.2
Vue:3
License:MIT
Last Updated:Sep 6, 2026
Links: GitHub  ·  npm
Maintainer: ignsm

i18n-key-inspector

Point at any text on the page. See the translation key that made it.

The library writes an invisible prefix in front of every message, then reads that prefix back from the DOM. The key travels inside the string. It survives v-html, attributes, and a section that hydrates ten seconds after the first paint.

Searching the DOM for the rendered text cannot do that. It cannot tell two equal strings apart, and it never sees copy that renders later.

Use this library in development only. Do not ship it to production.

Install

npm install --save-dev i18n-key-inspector

Use it with vue-i18n

import { startInspector } from 'i18n-key-inspector'
import { createVueI18nAdapter } from 'i18n-key-inspector/vue-i18n'

startInspector(createVueI18nAdapter(i18n.global))

Press Alt three times to turn the mode on. Option on a Mac. Then hold the same key. Every translated string lights up, and the one under the pointer shows its key. Hold the key and click to copy it. The tooltip says Copied for a moment, then shows the key again. Three presses turn the mode off.

Until you press it, the inspector leaves the page alone. It rewrites no catalogue, and it marks nothing.

Change the shortcut:

startInspector(adapter, {
  activation: { key: 'Shift', presses: 2, windowMs: 500 },
})

startInspector(adapter, { activation: null }) // start at once, no shortcut

startInspector returns { inspector, active, toggle, stop }, so you can drive it from your own devtools panel. inspector.keyAt(element) gives you the key for your own UI. The key follows changes to translated text and attributes. When that translation disappears, its key goes away too.

An element can hold several translations. Its last direct text node with a translation takes precedence. Otherwise, its first translated attribute in DOM order supplies the key.

Use it in Nuxt

Put it in a client plugin, and keep it out of production.

// plugins/i18n-inspector.client.ts
export default defineNuxtPlugin(async (nuxtApp) => {
  if (!import.meta.dev) return

  const { startInspector } = await import('i18n-key-inspector')
  const { createVueI18nAdapter } = await import('i18n-key-inspector/vue-i18n')

  startInspector(createVueI18nAdapter(nuxtApp.$i18n), {
    skipGroups: ['seo'],
    formatKey: (key) => key.replace(/\./g, '::'),
  })
})

The dynamic import keeps the package out of the production bundle. The formatKey above prints a double-colon key, ready to paste into a search.

Bring your own UI

startInspector builds the inspector and a small overlay. Skip the overlay when you want your own.

import { Inspector } from 'i18n-key-inspector'

const inspector = new Inspector(createVueI18nAdapter(i18n.global))
inspector.start()

document.addEventListener('mouseover', (event) => {
  const target = event.target
  if (target instanceof Element) console.log(inspector.keyAt(target))
})

Options

new Inspector(adapter, {
  keyAttribute: 'data-i18n-key',
  toolSelector: '[data-i18n-inspector-ui]',
  skipGroups: ['seo'],
  skipNestedGroups: ['meta'],
  listSeparators: [',,'],
})
Option Default Purpose
activation { key: 'Alt', presses: 3, windowMs: 600 } Shortcut that turns the mode on. null starts at once
modifier 'Alt' Key to hold to see the keys
formatKey identity Turns a key into the text the tooltip shows
onCopy null Runs after a copy, for your own toast
keyAttribute data-i18n-key Attribute that holds the key
toolSelector [data-i18n-inspector-ui] Your own overlay. The inspector then ignores it
skipGroups [] Top-level groups to leave alone
skipNestedGroups [] Group names one level in to leave alone
pluralSeparator '|' Separator that the i18n library reads as a plural
listSeparators [] Separators that your app splits a message on
reapplyDelayMs 400 Delay before the next pass, after new content
selfInflictedMs 600 Window in which a mutation counts as our own
hydrationDelayMs 1500 One extra pass after start, for late hydration

A section that hydrates on scroll adopts the server text in place, so nothing mutates for the observer to see. The inspector watches scrolling for that reason, and marks again after it settles.

Which strings to skip

Skip copy that reaches only <title> and <meta>. Nobody can point at it. A marker in a title also travels to the analytics pageview. Use skipGroups for a top-level seo group. Use skipNestedGroups for a meta group that sits beside the strings of each page.

Copy that your app splits

A plural needs no configuration. The | in one note | {n} notes belongs to vue-i18n, and both branches get a marker.

A list that your own code splits does need configuration. Your FAQ questions can sit in one message that a ,, joins. Pass listSeparators: [',,']. Every item then carries the key, not only the first one.

A real array in the catalogue needs nothing. The walk names each item by its index, features.0 and features.1. This is more exact than any separator.

Messages that your app parses as JSON

A message can hold a JSON array of strings. Each item gets a marker inside the array, so JSON.parse still works and each item carries the key.

A message can also hold a JSON record. The inspector leaves it alone. The values of a record can be icon names and other identifiers.

Requirements

The package ships ESM only. Node 20.19 or later, and a bundler that reads the exports map. There is no require build, and none is planned.

The vue-i18n adapter works with vue-i18n 9, 10, and 11. CI runs the adapter tests against each of them.

The overlay draws in a shadow root. It writes no style onto an element of your app, and draws its boxes on top instead. Restyling a native control makes Safari drop its own appearance and resize it, which moves the page.

Building your own UI instead? Mark it with data-i18n-inspector-ui, or set toolSelector, so the inspector does not read it as new copy from your app.

Limits

  • A compiled message with no literal text gets no marker. A message such as {n} notes carries its marker in the text after the interpolation.
  • A component can call t() once, outside a reactive scope. It keeps the text that it read at build time. Those strings stay untagged until the framework builds the component again.
  • Edit a locale file while the inspector runs, and the new text appears for a moment. The next pass restores the snapshot from start(). Stop the inspector and start it again to pick the edit up.
  • A component can hold a string that t() gave it and render it later. The table reads a marker of the pass before last, so the element reports no key while one pass stands between the two. The next pass reads the key again.
  • Stop the inspector, load another catalogue, and start it again. A string that a component still holds from the earlier run can name the key that took its place. Render the component again, and it reports the right key.
  • The inspector puts its marker in front of the text. Code of the app that reads a translated string therefore sees the marker. A check such as value.trim().startsWith('[') fails for a message such as [TODO] Copy. A message that holds a JSON array keeps its brackets and still parses.
  • The inspector strips its markers from the page, so it remembers which text belongs to which key. Text of the app that reads the same as a translation therefore keeps that key. A {{ label }} that renders Active from the catalogue and then Active from an API still reports the catalogue key.

Write an adapter

Any i18n library works. You need three things. Read a catalogue, write it back, and make the framework render again.

import type { CatalogueAdapter } from 'i18n-key-inspector'

const adapter: CatalogueAdapter = {
  currentLocale: () => 'en',
  loadedLocales: () => ['en', 'es'],
  getCatalogue: (locale) => catalogues[locale],
  setCatalogue: (locale, catalogue) => install(locale, catalogue),
}

One key table covers every loaded locale. The app renders a missing string from the fallback catalogue. The inspector still reports the key that the string came from, not a neighbour key.

setCatalogue must replace the catalogue and make the framework render again. This is one call in vue-i18n. Other libraries need more work. i18next, for example, does not re-render a React tree from addResourceBundle alone.

Development

pnpm install
pnpm play      # a demo page on http://localhost:5173
pnpm verify    # format, comments, types, tests, build

pnpm verify runs the formatter, the comment checker, the type checker, the tests, and the build. See CONTRIBUTING.md.

License

MIT

Related Packages

@blax-software/i18n

Per-page, all-locale translation sources merged into vue-i18n messages, plus a C...

253 0
vite-plugin-vue-inspector

Jump to local IDE source code when clicking Vue elements in the browser.

46,387,406 809
@json-render/devtools-vue

Vue adapter for @json-render/devtools. Drop-in component.

4,135 18,761
@modernice/vue-i18n-modules

"Message modules" extension for vue-i18n

2,564

Version History

Version Released Vue License
0.2.2 ^3.5.41 MIT
0.2.1 ^3.5.41 MIT
0.2.0 ^3.5.41 MIT
0.1.1 ^3.5.41 MIT
0.1.0 ^3.5.41 MIT