KilimcininKorOglu/claude-code-mods/tree/main/plugins/i18n-watch
i18n-watch
After an edit that calls translation keys, names the keys the JSON, Laravel PHP, YAML or gettext locale files lack, and the languages that lack them.
About this mod
i18n-watch
The model adds t('checkout.total') to a component, and the key exists in no locale file, or in English only. The page then shows the raw key, or English on the Turkish page, and nobody notices until a user does. This mod tells the model when an edit uses translation keys that one or more locale files lack. The note comes with the Edit's result, so the model adds the keys in the same turn. By default nothing is stopped; in deny mode a commit, a push and a merge stop while a key is missing.
What it does
-
The mod hooks the Edit and Write tools. After a successful call on a source file (
.ts,.tsx,.js,.jsx,.mjs,.cjs,.vue,.svelte,.astro,.php,.py,.rb,.erb,.haml,.slim), it reads the translation calls the edit added: those innew_stringthatold_stringdoes not have, or every call of a Write. -
The calls read are
t,$t,i18n.t,__,trans,trans_choice,@lang,_,gettextandngettextwith a quoted first argument, also afterthis.,vm.,i18n.,$i18n.,I18n.andi18n.global.. A variable argument, a template literal and a Rails lazy key (t('.title')) are skipped. -
It reads the locale files under these directories of the session directory, at most 4 levels deep and 200 files, skipping
node_modulesand any file over 2 MB:locales,lang,i18n,translations,locale,config/locales,resources/lang,src/locales,src/i18n,public/locales. The session directory is the one the session started in, read once at its start, because a Bashcdmoves the session's own directory. The edited file is named against the git repository the session started in, so a session opened inapps/webnames a file ofapps/apiasapps/api/x.ts. Outside a git repository the file is named against the session directory. That root is read once at the session's start too.| Format | Example path | Keys | |---|---|---| | JSON (i18next, vue-i18n, Laravel) |
locales/tr.json,locales/tr/checkout.json,lang/tr.json| nested keys as dotted paths;item_onealso definesitem| | PHP array (Laravel) |lang/tr/messages.php|messages.key, nested arrays as dotted paths | | YAML (Rails, Symfony) |config/locales/tr.yml,translations/messages.tr.yaml| dotted paths; a Rails top key (tr:) is left out | | gettext |locale/tr/LC_MESSAGES/django.po| eachmsgid|The language comes from a directory (
tr/,en-US/) or from the file name (tr.json,messages.tr.yaml). A key in a namespace file also counts asns.keyandns:key. -
A key is missing when a language lacks it, or when no language has it. Each key is named with the line it is called on, so you can open it. The model reads this note after the Edit's result:
i18n-watch: this edit uses translation keys the locale files lack: checkout.total:42 (missing in tr, de) · checkout.vat:58 (missing in every locale). Add them to each locale file.At most 10 keys are named, the rest counted. Only the keys this edit added are reported; a key reported before stays in the finding without being said again.
-
At the same moment one line reaches the transcript, so you see what the model was told. The line holds the file and its keys, without the instruction. The file is named because the model saw the edit and you did not:
i18n-watch: keys src/Cart.vue uses that the locale files lack: checkout.total:42 (missing in tr, de) · checkout.vat:58 (missing in every locale)The note and the line are separate channels: the model never reads the line, and you never read the note.
-
While the sidebar is open, those keys go there instead, as an entry in its stream: the file first, then one line per key, with the key red, its line number faint,
every localered and a list of some locales yellow. The transcript stays clean. The entry stays until newer ones push it off the pane. With the sidebar closed, or without that mod installed, the transcript line is written as above. -
A finding is never a remembered answer. The keys it holds are a claim, and every measure reads the source file from disk again and drops the keys it no longer calls. So a finding closes two ways, measured after each Edit and Write, at the end of each main-loop turn, and before a guarded git command:
- every locale gained the keys;
- the code stopped calling them, because the edit deleted the string, replaced it with another one or moved it to another file. A file that is gone closes its finding too.
The entry is cleared and a green one says which of the two it was:
i18n-watch: every locale now has the keys src/Cart.vue lacked: checkout.total · checkout.vat i18n-watch: index.php no longer uses: Unauthorized AccessWith the sidebar closed the same text is one transcript line. The model reads nothing of this: the finding closed by its own work, so a note would only repeat what it just did. A file that is there and cannot be read keeps its finding, because an unread file proves nothing.
-
A finding the model did not close is measured again at the end of each main-loop turn, and what is left reaches the model as one note with its next prompt:
i18n-watch: 1 file(s) still use translation keys the locale files lack: index.php (Unauthorized Access:42). Add the keys to every locale file, or take the calls out.One note per turn, not one per prompt. Without this the finding would be said once, at the edit, and then stand in the pane while the model forgot it. You read nothing new: the pane already carries the same finding.
-
In
denymode the mod also stopsgit commit,git pushandgit mergewhile a file still uses keys the locale files lack; a command with--dry-run,--helpor-his not stopped. Agit commitanswers for its own files alone: the mod reads the index (git diff --cached --name-only -z) and lets the commit run when it holds none of the open files, with one line to you naming how many still stand. Apushand amergehold no index to read, so every finding stands there. There is no bypass; only you turn the gate off, with/i18n-watch mode note.notemode is the default and stops nothing, but it measures the findings at a git command all the same, so a settled one does not stay in the pane.
The locale files are read at the first edit of a turn that needs them, and again after an Edit or Write of a locale file. A project without these directories gets nothing. A locale file that cannot be read or parsed is skipped and logged once per session.
In the live check the model added t('cart.total') to a file of a project with locales/en.json and locales/tr.json, read the note after the Edit, and quoted it word for word.
Command
/i18n-watch on or off, the mode, and the files still missing keys
/i18n-watch on | off on by default
/i18n-watch mode note note only; the default
/i18n-watch mode deny a commit, a push and a merge also stop while a key is missing
Install
claude plugin marketplace add KilimcininKorOglu/claude-code-mods
claude plugin install i18n-watch@kilimcininkoroglu-mods
Function hooks are early access. Claude Code 2.1.288 and later load them by default, so there is nothing to switch on.
After installing
- Restart Claude Code.
What it can reach
Validated with claude plugin validate on Claude Code 2.1.283:
❯ ./register.ts hooks: session.start, command.run{command=i18n-watch}, turn.start, tool.call{tool=Bash}, turn.complete, prompt.submit, tool.call{tool=Edit}, tool.call{tool=Write}
❯ ./register.ts calls: $.command.register, $.fs.exists (via isDir, usedNow), $.fs.list (via walkLocales), $.fs.read (via loadCatalog, usedNow), $.fs.stat (via isDir), $.process.run (via shownRootOf, stagedPaths), $.session.cwd, $.sidebar.clear (via dropEntry), $.sidebar.set (via toPerson), $.store.get (via readSettings), $.store.set (via runCommand, setMode), $.ui.log (via catalogOf, gate, toPerson)
Reach L2, it runs git to read the index.
1. Reads: the text of each Edit and Write call; the Bash command text; each reported source file again; the locale directories under the session directory and their files
2. Runs: git rev-parse --show-toplevel once at the session's start, to name files against the repository root; git rev-parse --show-toplevel and git diff --cached --name-only -z, at a guarded command in deny mode, to read which files the commit holds
3. Sends: a note to the model after an edit that uses missing keys, one more with the next prompt while a finding stands, and one line to the transcript; nothing leaves the machine
4. Persists: in $.store, the on/off setting and the mode; the locale keys live in memory for one turn
5. Hostile input: locale files are only parsed as data (JSON.parse and line regexes), never run; PHP files are not executed
Limits
- Only the locale directories under the session directory are read. A monorepo whose locales sit in
apps/web/src/localesis not seen when the session starts at the repository root. - A Laravel PHP file is read line by line, not run: an array opened and closed on one line, a computed key and an
included array are not seen. - YAML is read by indent. Anchors, aliases and flow mappings (
{a: b}) are not followed. - A key built at run time (
t(name),t(`a.${b}`)) is not checked. - A language code is two letters with an optional region or script (
tr,pt_BR,zh-Hant); a three-letter code such asfilis not recognised. - A key the edit only moves (it was in
old_stringtoo) is not checked, and neither is an edit through Bash. - The
denymode has no bypass. When a finding cannot be fixed, you turn the gate off with/i18n-watch mode note. - The gate reads the command text. A commit through a script or an alias that hides
git commitis not stopped. - A
git commit -a, a-amand a commit with a pathspec after--are not narrowed to the index, because they commit files the index does not hold yet. Every open finding stands for those. - The index is read before the command runs. A commit whose files change between the read and the run (another process staging meanwhile) is measured against what the index held at the read.
- A finding is closed by the calls the file makes, not by the locale files' own use. A key the code stopped calling is dropped from the finding even when the locale files still lack it, because nothing calls it any more.
Development
make install # eslint, typescript-eslint, typescript
make lint # complexity limit 10, the build fails above it
make typecheck # needs .claude/types/ from /plugin-types
make validate
make test # claude plugin test
Installation
Check the author's README for the marketplace and plugin name first. Commands may change as the repository evolves.
claude plugin marketplace add KilimcininKorOglu/claude-code-mods claude plugin install i18n-watch
Original text / README
i18n-watch
The model adds t('checkout.total') to a component, and the key exists in no locale file, or in English only. The page then shows the raw key, or English on the Turkish page, and nobody notices until a user does. This mod tells the model when an edit uses translation keys that one or more locale files lack. The note comes with the Edit's result, so the model adds the keys in the same turn. By default nothing is stopped; in deny mode a commit, a push and a merge stop while a key is missing.
What it does
-
The mod hooks the Edit and Write tools. After a successful call on a source file (
.ts,.tsx,.js,.jsx,.mjs,.cjs,.vue,.svelte,.astro,.php,.py,.rb,.erb,.haml,.slim), it reads the translation calls the edit added: those innew_stringthatold_stringdoes not have, or every call of a Write. -
The calls read are
t,$t,i18n.t,__,trans,trans_choice,@lang,_,gettextandngettextwith a quoted first argument, also afterthis.,vm.,i18n.,$i18n.,I18n.andi18n.global.. A variable argument, a template literal and a Rails lazy key (t('.title')) are skipped. -
It reads the locale files under these directories of the session directory, at most 4 levels deep and 200 files, skipping
node_modulesand any file over 2 MB:locales,lang,i18n,translations,locale,config/locales,resources/lang,src/locales,src/i18n,public/locales. The session directory is the one the session started in, read once at its start, because a Bashcdmoves the session's own directory. The edited file is named against the git repository the session started in, so a session opened inapps/webnames a file ofapps/apiasapps/api/x.ts. Outside a git repository the file is named against the session directory. That root is read once at the session's start too.| Format | Example path | Keys | |---|---|---| | JSON (i18next, vue-i18n, Laravel) |
locales/tr.json,locales/tr/checkout.json,lang/tr.json| nested keys as dotted paths;item_onealso definesitem| | PHP array (Laravel) |lang/tr/messages.php|messages.key, nested arrays as dotted paths | | YAML (Rails, Symfony) |config/locales/tr.yml,translations/messages.tr.yaml| dotted paths; a Rails top key (tr:) is left out | | gettext |locale/tr/LC_MESSAGES/django.po| eachmsgid|The language comes from a directory (
tr/,en-US/) or from the file name (tr.json,messages.tr.yaml). A key in a namespace file also counts asns.keyandns:key. -
A key is missing when a language lacks it, or when no language has it. Each key is named with the line it is called on, so you can open it. The model reads this note after the Edit's result:
i18n-watch: this edit uses translation keys the locale files lack: checkout.total:42 (missing in tr, de) · checkout.vat:58 (missing in every locale). Add them to each locale file.At most 10 keys are named, the rest counted. Only the keys this edit added are reported; a key reported before stays in the finding without being said again.
-
At the same moment one line reaches the transcript, so you see what the model was told. The line holds the file and its keys, without the instruction. The file is named because the model saw the edit and you did not:
i18n-watch: keys src/Cart.vue uses that the locale files lack: checkout.total:42 (missing in tr, de) · checkout.vat:58 (missing in every locale)The note and the line are separate channels: the model never reads the line, and you never read the note.
-
While the sidebar is open, those keys go there instead, as an entry in its stream: the file first, then one line per key, with the key red, its line number faint,
every localered and a list of some locales yellow. The transcript stays clean. The entry stays until newer ones push it off the pane. With the sidebar closed, or without that mod installed, the transcript line is written as above. -
A finding is never a remembered answer. The keys it holds are a claim, and every measure reads the source file from disk again and drops the keys it no longer calls. So a finding closes two ways, measured after each Edit and Write, at the end of each main-loop turn, and before a guarded git command:
- every locale gained the keys;
- the code stopped calling them, because the edit deleted the string, replaced it with another one or moved it to another file. A file that is gone closes its finding too.
The entry is cleared and a green one says which of the two it was:
i18n-watch: every locale now has the keys src/Cart.vue lacked: checkout.total · checkout.vat i18n-watch: index.php no longer uses: Unauthorized AccessWith the sidebar closed the same text is one transcript line. The model reads nothing of this: the finding closed by its own work, so a note would only repeat what it just did. A file that is there and cannot be read keeps its finding, because an unread file proves nothing.
-
A finding the model did not close is measured again at the end of each main-loop turn, and what is left reaches the model as one note with its next prompt:
i18n-watch: 1 file(s) still use translation keys the locale files lack: index.php (Unauthorized Access:42). Add the keys to every locale file, or take the calls out.One note per turn, not one per prompt. Without this the finding would be said once, at the edit, and then stand in the pane while the model forgot it. You read nothing new: the pane already carries the same finding.
-
In
denymode the mod also stopsgit commit,git pushandgit mergewhile a file still uses keys the locale files lack; a command with--dry-run,--helpor-his not stopped. Agit commitanswers for its own files alone: the mod reads the index (git diff --cached --name-only -z) and lets the commit run when it holds none of the open files, with one line to you naming how many still stand. Apushand amergehold no index to read, so every finding stands there. There is no bypass; only you turn the gate off, with/i18n-watch mode note.notemode is the default and stops nothing, but it measures the findings at a git command all the same, so a settled one does not stay in the pane.
The locale files are read at the first edit of a turn that needs them, and again after an Edit or Write of a locale file. A project without these directories gets nothing. A locale file that cannot be read or parsed is skipped and logged once per session.
In the live check the model added t('cart.total') to a file of a project with locales/en.json and locales/tr.json, read the note after the Edit, and quoted it word for word.
Command
/i18n-watch on or off, the mode, and the files still missing keys
/i18n-watch on | off on by default
/i18n-watch mode note note only; the default
/i18n-watch mode deny a commit, a push and a merge also stop while a key is missing
Install
claude plugin marketplace add KilimcininKorOglu/claude-code-mods
claude plugin install i18n-watch@kilimcininkoroglu-mods
Function hooks are early access. Claude Code 2.1.288 and later load them by default, so there is nothing to switch on.
After installing
- Restart Claude Code.
What it can reach
Validated with claude plugin validate on Claude Code 2.1.283:
❯ ./register.ts hooks: session.start, command.run{command=i18n-watch}, turn.start, tool.call{tool=Bash}, turn.complete, prompt.submit, tool.call{tool=Edit}, tool.call{tool=Write}
❯ ./register.ts calls: $.command.register, $.fs.exists (via isDir, usedNow), $.fs.list (via walkLocales), $.fs.read (via loadCatalog, usedNow), $.fs.stat (via isDir), $.process.run (via shownRootOf, stagedPaths), $.session.cwd, $.sidebar.clear (via dropEntry), $.sidebar.set (via toPerson), $.store.get (via readSettings), $.store.set (via runCommand, setMode), $.ui.log (via catalogOf, gate, toPerson)
Reach L2, it runs git to read the index.
1. Reads: the text of each Edit and Write call; the Bash command text; each reported source file again; the locale directories under the session directory and their files
2. Runs: git rev-parse --show-toplevel once at the session's start, to name files against the repository root; git rev-parse --show-toplevel and git diff --cached --name-only -z, at a guarded command in deny mode, to read which files the commit holds
3. Sends: a note to the model after an edit that uses missing keys, one more with the next prompt while a finding stands, and one line to the transcript; nothing leaves the machine
4. Persists: in $.store, the on/off setting and the mode; the locale keys live in memory for one turn
5. Hostile input: locale files are only parsed as data (JSON.parse and line regexes), never run; PHP files are not executed
Limits
- Only the locale directories under the session directory are read. A monorepo whose locales sit in
apps/web/src/localesis not seen when the session starts at the repository root. - A Laravel PHP file is read line by line, not run: an array opened and closed on one line, a computed key and an
included array are not seen. - YAML is read by indent. Anchors, aliases and flow mappings (
{a: b}) are not followed. - A key built at run time (
t(name),t(`a.${b}`)) is not checked. - A language code is two letters with an optional region or script (
tr,pt_BR,zh-Hant); a three-letter code such asfilis not recognised. - A key the edit only moves (it was in
old_stringtoo) is not checked, and neither is an edit through Bash. - The
denymode has no bypass. When a finding cannot be fixed, you turn the gate off with/i18n-watch mode note. - The gate reads the command text. A commit through a script or an alias that hides
git commitis not stopped. - A
git commit -a, a-amand a commit with a pathspec after--are not narrowed to the index, because they commit files the index does not hold yet. Every open finding stands for those. - The index is read before the command runs. A commit whose files change between the read and the run (another process staging meanwhile) is measured against what the index held at the read.
- A finding is closed by the calls the file makes, not by the locale files' own use. A key the code stopped calling is dropped from the finding even when the locale files still lack it, because nothing calls it any more.
Development
make install # eslint, typescript-eslint, typescript
make lint # complexity limit 10, the build fails above it
make typecheck # needs .claude/types/ from /plugin-types
make validate
make test # claude plugin test

