L2 v0.7.0 source

i18n-watch

Bir edit'in, bir ya da birden fazla locale dosyasında olmayan translation key'lerini kullandığını modele söyleyen bir Claude Code Mod'u. Not Edit'in sonucuyla birlikte gelir, yani model key'leri aynı turda ekler. Varsayılan olarak hiçbir şey durmaz; deny modunda bir key eksikken commit, push ve merge durur.

Ne yapar

  1. Mod Edit ve Write tool'larını hook'lar. Bir kaynak dosyada (.ts, .tsx, .js, .jsx, .mjs, .cjs, .vue, .svelte, .astro, .php, .py, .rb, .erb, .haml, .slim) başarılı bir çağrıdan sonra edit'in eklediği translation çağrılarını okur: new_string içinde olup old_string içinde olmayanları, ya da bir Write'ın her çağrısını.

  2. Okunan çağrılar, tırnaklı ilk argümanı olan t, $t, i18n.t, __, trans, trans_choice, @lang, _, gettext ve ngettext; ayrıca this., vm., i18n., $i18n., I18n. ve i18n.global. sonrası biçimleri. Değişken argüman, template literal ve Rails lazy key (t('.title')) atlanır.

  3. Locale dosyalarını session dizininin şu dizinleri altında, en fazla 4 seviye derinlikte ve 200 dosya olarak okur: locales, lang, i18n, translations, locale, config/locales, resources/lang, src/locales, src/i18n, public/locales. Session dizini, session'ın başladığı dizindir ve başlangıçta bir kere okunur, çünkü bir Bash cd session'ın kendi dizinini taşır. Edit edilen dosya da o dizine göre adlandırılır, yani proje içindeki bir path proje kökünden yazılır.

    Format Örnek path Key'ler
    JSON (i18next, vue-i18n, Laravel) locales/tr.json, locales/tr/checkout.json, lang/tr.json iç içe key'ler noktalı path olarak; item_one aynı zamanda item'i tanımlar
    PHP array (Laravel) lang/tr/messages.php messages.key, iç içe array'ler noktalı path olarak
    YAML (Rails, Symfony) config/locales/tr.yml, translations/messages.tr.yaml noktalı path'ler; bir Rails üst key'i (tr:) dışarıda bırakılır
    gettext locale/tr/LC_MESSAGES/django.po her msgid

    Dil bir dizinden (tr/, en-US/) ya da dosya adından (tr.json, messages.tr.yaml) gelir. Bir namespace dosyasındaki key aynı zamanda ns.key ve ns:key olarak da sayılır.

  4. Bir key, bir dilde yoksa ya da hiçbir dilde yoksa eksiktir. Her key çağrıldığı satırla adlandırılır, böylece açabilirsiniz. Model bu notu Edit'in sonucundan sonra okur:

    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.
    

    En fazla 10 key adlandırılır, kalanı sayılır.

  5. Aynı anda transcript'e bir satır yazılır, böylece modele ne söylendiğini görürsünüz. Bu satır yalnız key'leri taşır, talimat cümlesi olmadan:

    i18n-watch: keys the locale files lack: checkout.total:42 (missing in tr, de) · checkout.vat:58 (missing in every locale)
    

    Not ve satır ayrı iki kanaldır: model satırı hiç okumaz, siz notu hiç okumazsınız.

  6. sidebar açıkken bu key'ler oraya gider, key başına bir satır olarak, stream'inde bir entry halinde, ve transcript temiz kalır. Entry, yenileri pane'den itene kadar durur. Sidebar kapalıyken ya da o mod kurulu değilken transcript satırı yukarıdaki gibi yazılır.

  7. Bir bulgu hiçbir zaman hatırlanmış bir cevap değildir. Tuttuğu key'ler bir iddiadır ve her ölçüm kaynak dosyayı diskten yeniden okur ve artık çağırmadığı key'leri düşürür. Yani bir bulgu iki yoldan kapanır ve ikisi de her Edit ve Write'tan sonra, ve bir guarded git komutundan önce ölçülür:

    • her locale key'leri kazandı;
    • kod onları çağırmayı bıraktı, çünkü edit string'i sildi, başkasıyla değiştirdi ya da başka bir dosyaya taşıdı. Silinmiş bir dosya da bulgusunu kapatır.

    Entry temizlenir ve yeni bir satır hangisi olduğunu söyler:

    i18n-watch: every locale now has the keys src/Cart.vue lacked: checkout.total · checkout.vat
    i18n-watch: index.php no longer uses: Unauthorized Access
    

    Sidebar kapalıyken aynı metin tek bir transcript satırıdır. Model bunun hiçbirini okumaz: bulgu kendi işiyle kapandı, bir not yalnız az önce yaptığını tekrar ederdi. Var olan ama okunamayan bir dosya bulgusunu açık tutar, çünkü okunamayan bir dosya hiçbir şeyi kanıtlamaz.

  8. Modelin kapatmadığı bir bulgu her ana döngü turunun sonunda yeniden ölçülür ve kalan, bir sonraki prompt'la modele tek bir not olarak ulaşır:

    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.
    

    Tur başına bir not, prompt başına değil. Bu olmasa bulgu bir kere, edit anında söylenir ve sonra model onu unutmuşken pane'de dururdu. Siz yeni bir şey okumazsınız: pane zaten aynı bulguyu taşıyor.

  9. deny modunda mod ayrıca, bir dosya locale dosyalarının tutmadığı key'leri kullandığı sürece git commit, git push ve git merge komutlarını durdurur. Bir git commit yalnız kendi dosyaları için cevap verir: mod index'i okur (git diff --cached --name-only) ve index açık dosyaların hiçbirini tutmuyorsa commit'in çalışmasına izin verir, size kaçının hâlâ durduğunu söyleyen bir satırla. Bir push ve bir merge okunacak index tutmaz, yani orada her bulgu durur. Kaçış yolu yok; gate'i yalnız kişi /i18n-watch mode note ile kapatır. note modu varsayılandır ve hiçbir şeyi durdurmaz, ama bulguları bir git komutunda yine de ölçer, böylece çözülmüş bir bulgu pane'de kalmaz.

Locale dosyaları, bir turda key ekleyen ilk edit'te ve bir locale dosyasının Edit ya da Write'ından sonra okunur. Bu dizinleri olmayan bir proje hiçbir şey almaz. Okunamayan ya da parse edilemeyen bir locale dosyası atlanır ve session başına bir kere log'lanır.

Canlı kontrolde model, locales/en.json ve locales/tr.json olan bir projenin dosyasına t('cart.total') ekledi, notu Edit'ten sonra okudu ve kelimesi kelimesine alıntıladı.

Komut

/i18n-watch                 on ya da off, mod ve hâlâ key'i eksik olan dosyalar
/i18n-watch on | off        varsayılan on
/i18n-watch mode note       yalnız not; varsayılan
/i18n-watch mode deny       bir key eksikken commit, push ve merge de durur

Kurulum

claude plugin marketplace add KilimcininKorOglu/claude-code-mods
claude plugin install i18n-watch@kilimcininkoroglu-mods

Function hook'lar early access. Flag olmadan hiçbir şey yüklenmez. Flag'i kalıcı yapmak için ~/.claude/settings.json dosyasına ekleyin:

{ "env": { "CLAUDE_CODE_ENABLE_FUNCTION_HOOKS": "1" } }

Kurulumdan sonra

  1. Claude Code'u yeniden başlatın.

Nereye uzanır

Claude Code 2.1.278 üzerinde claude plugin validate ile doğrulandı:

❯ ./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 stagedPaths), $.session.cwd, $.sidebar.clear (via dropEntry), $.sidebar.set (via toPerson), $.store.get, $.store.set (via runCommand, setMode), $.ui.log (via catalogOf, gate, toPerson)

Reach L2, index'i okumak için git çalıştırır.

1. Okur:     her Edit ve Write çağrısının metnini; Bash komut metnini; raporlanan her kaynak dosyayı yeniden; session dizini altındaki locale dizinlerini ve dosyalarını
2. Çalıştırır: deny modunda guarded bir komutta git rev-parse --show-toplevel ve git diff --cached --name-only, commit'in hangi dosyaları tuttuğunu okumak için
3. Gönderir: eksik key kullanan bir edit'ten sonra modele bir not, bulgu dururken sonraki prompt'la bir tane daha ve transcript'e bir satır; makineden hiçbir şey çıkmaz
4. Saklar:   $.store içinde on/off ayarını ve modu; locale key'leri bir tur boyunca bellekte yaşar
5. Düşman girdi: locale dosyaları yalnız veri olarak parse edilir (JSON.parse ve satır regex'leri), hiçbir zaman çalıştırılmaz; PHP dosyaları execute edilmez

Sınırlar

  • Yalnız session dizini altındaki locale dizinleri okunur. Locale'leri apps/web/src/locales içinde olan bir monorepo, session repository kökünde başladığında görülmez.
  • Bir Laravel PHP dosyası satır satır okunur, çalıştırılmaz: tek satırda açılıp kapanan bir array, hesaplanmış bir key ve include edilen bir array görülmez.
  • YAML indent ile okunur. Anchor'lar, alias'lar ve flow mapping'ler ({a: b}) izlenmez.
  • Çalışma anında kurulan bir key (t(name), t(`a.${b}`)) kontrol edilmez.
  • Bir dil kodu, isteğe bağlı bir region ya da script ile iki harftir (tr, pt_BR, zh-Hant); fil gibi üç harfli bir kod tanınmaz.
  • Edit'in yalnız taşıdığı bir key (old_string içinde de vardı) kontrol edilmez, Bash üzerinden yapılan bir edit de öyle.
  • deny modunun kaçış yolu yoktur. Bir bulgu düzeltilemediğinde kişi gate'i /i18n-watch mode note ile kapatır.
  • Gate komut metnini okur. git commit'i gizleyen bir script ya da alias üzerinden atılan commit durdurulmaz.
  • Bir git commit -a, bir -am ve -- sonrası pathspec taşıyan bir commit index'e göre daraltılmaz, çünkü bunlar index'in henüz tutmadığı dosyaları commit eder. Onlar için her açık bulgu durur.
  • Index, komut çalışmadan önce okunur. Okuma ile çalışma arasında dosyaları değişen bir commit (bu arada başka bir process stage ediyorsa) okuma anındaki index'e göre ölçülür.
  • Bir bulgu dosyanın yaptığı çağrılarla kapanır, locale dosyalarının kendi kullanımıyla değil. Kodun çağırmayı bıraktığı bir key, locale dosyaları hâlâ eksik olsa bile bulgudan düşer, çünkü artık onu hiçbir şey çağırmıyor.

Geliştirme

make install     # eslint, typescript-eslint, typescript
make lint        # complexity limiti 10, üstünde build'i düşürür
make typecheck   # /plugin-types ile üretilen .claude/types/ gerekir
make validate
make test        # claude plugin test

i18n-watch

A Claude Code Mod that 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

  1. 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 in new_string that old_string does not have, or every call of a Write.

  2. The calls read are t, $t, i18n.t, __, trans, trans_choice, @lang, _, gettext and ngettext with a quoted first argument, also after this., vm., i18n., $i18n., I18n. and i18n.global.. A variable argument, a template literal and a Rails lazy key (t('.title')) are skipped.

  3. It reads the locale files under these directories of the session directory, at most 4 levels deep and 200 files: 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 Bash cd moves the session's own directory. The edited file is named against that directory too, so a path inside the project is written from the project root.

    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_one also defines item
    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 each msgid

    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 as ns.key and ns:key.

  4. 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.

  5. The same moment writes one line to the transcript, so you see what the model was told. The line holds the keys alone, without the instruction:

    i18n-watch: keys 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.

  6. While the sidebar is open, those keys go there instead, one line per key, as an entry in its stream, and 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.

  7. 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, and both are measured after each Edit and Write and again 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 new 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 Access
    

    With 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.

  8. 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.

  9. In deny mode the mod also stops git commit, git push and git merge while a file still uses keys the locale files lack. A git commit answers for its own files alone: the mod reads the index (git diff --cached --name-only) and lets the commit run when it holds none of the open files, with one line to you naming how many still stand. A push and a merge hold no index to read, so every finding stands there. There is no bypass; only the person turns the gate off with /i18n-watch mode note. note mode 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 adds a key, 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. Nothing loads without the flag. To keep it on, add this to ~/.claude/settings.json:

{ "env": { "CLAUDE_CODE_ENABLE_FUNCTION_HOOKS": "1" } }

After installing

  1. Restart Claude Code.

What it can reach

Validated with claude plugin validate on Claude Code 2.1.278:

❯ ./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 stagedPaths), $.session.cwd, $.sidebar.clear (via dropEntry), $.sidebar.set (via toPerson), $.store.get, $.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 and git diff --cached --name-only, 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/locales is 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 as fil is not recognised.
  • A key the edit only moves (it was in old_string too) is not checked, and neither is an edit through Bash.
  • The deny mode has no bypass. When a finding cannot be fixed, the person turns 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 commit is not stopped.
  • A git commit -a, a -am and 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, fails the build above it
make typecheck   # needs .claude/types/ from /plugin-types
make validate
make test        # claude plugin test