Skip to main content
Glama
fabkho
by fabkho

Find Used-But-Undefined Translation Keys

find_undefined_keys
Idempotent

Find translation keys used in source code that are missing from the locale layers your app consumes, preventing raw keys in production. Optionally write them as empty translations.

Instructions

Find keys referenced in source code but defined in NO locale layer the using app consumes — the direction that ships raw keys to production. The inverse of find_orphan_keys. Scope-aware: each scan unit (app) is checked against the layers it consumes (summary.searchedLayersByApp), so a key defined only in a layer the using app does not consume is still undefined for that app. Known limitation: extraction is line-based and static — dynamically built keys (template literals, concatenation) cannot be verified and are reported as uncertainKeys, never as hard findings. With write, the hard findings are also added to a locale file as empty translations, which is the first half of the fix; uncertain findings are never written.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
layerNoLayer to write the undefined keys into (e.g., "root", "app-admin"). Only read together with write, and only needed when the using code resolves against more than one layer. Call discover to list the layers.
writeNoAdd every undefined key to a locale file, with an empty string as its value, in the project default locale only. Existing values are never touched, and uncertain findings are never written. The layer is the one the using code resolves against; when that is more than one layer, the run refuses and asks for a layer name. Default: false, which only reports.
localeNoReference locale to resolve key definitions in (e.g., "en", "en-US"). Defaults to the project default locale.
scanDirsNoAbsolute paths of the directories to scan for source usage. Overrides scope-aware scanning: every layer is then checked against these directories alone. Example: ["/home/user/my-app/apps/admin"].
outputFileNoAbsolute path to write the full JSON output to. Only a compact summary is returned to the caller, which is what you want for a result too large to read in one piece. Example: ".i18n-reports/undefined-keys.json"
projectDirNoAbsolute path to the project root. Defaults to I18N_PROJECT_DIR, then server cwd. Example: "/home/user/my-app".
excludeDirsNoDirectory names to skip when scanning source files. Example: ["storybook", "__tests__", "node_modules"].

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault

No arguments

Schema Changelog

Changes observed during successful MCP inspections.

  1. Changed6 schema fields changedv1.0.6
    • addedInput schema / properties / layer
      Added value: +{
      +  "description": "Layer to write the undefined keys into (e.g., \"root\", \"app-admin\"). Only read together with write, and only needed when the using code resolves against more than one layer. Call discover to list the layers.",
      +  "type": "string"
      +}
    • changedInput schema / properties / outputFile / description
      Previous value: -"Absolute path to write full JSON output. Returns only a compact summary to the caller — use this for large outputs to avoid flooding the conversation context. Example: \"/tmp/undefined-keys.json\""New value: +"Absolute path to write the full JSON output to. Only a compact summary is returned to the caller, which is what you want for a result too large to read in one piece. Example: \".i18n-reports/undefined-keys.json\""
    • changedInput schema / properties / projectDir / description
      Previous value: -"Absolute path to the Nuxt project root. Defaults to I18N_PROJECT_DIR, then server cwd. Example: \"/home/user/my-app\"."New value: +"Absolute path to the project root. Defaults to I18N_PROJECT_DIR, then server cwd. Example: \"/home/user/my-app\"."
    • changedInput schema / properties / scanDirs / description
      Previous value: -"Absolute paths to directories to scan for source code usage. Overrides scope-aware scanning: every layer counts as resolvable from these dirs. Example: [\"/home/user/my-app/apps/admin\"]."New value: +"Absolute paths of the directories to scan for source usage. Overrides scope-aware scanning: every layer is then checked against these directories alone. Example: [\"/home/user/my-app/apps/admin\"]."
    • addedInput schema / properties / write
      Added value: +{
      +  "description": "Add every undefined key to a locale file, with an empty string as its value, in the project default locale only. Existing values are never touched, and uncertain findings are never written. The layer is the one the using code resolves against; when that is more than one layer, the run refuses and asks for a layer name. Default: false, which only reports.",
      +  "type": "boolean"
      +}
    • changedOutput schema / (root)
      Previous value: -nullNew value: +{
      +  "$schema": "https://json-schema.org/draft/2020-12/schema",
      +  "anyOf": [
      +    {
      +      "additionalProperties": false,
      +      "properties": {
      +        "limitation": {
      +          "description": "What this scan cannot see, in one paragraph. Read it before acting on the uncertain findings.",
      +          "type": "string"
      +        },
      +        "summary": {
      +          "additionalProperties": false,
      +          "description": "What the scan covered and what it found. This is what comes back when the full result is diverted to a file.",
      +          "properties": {
      +            "declaredCount": {
      +              "description": "Unresolvable keys covered by a declaredNamespaces entry — defined by contract, never written.",
      +              "maximum": 9007199254740991,
      +              "minimum": -9007199254740991,
      +              "type": "integer"
      +            },
      +            "filesDeclined": {
      +              "description": "Files a syntax frontend declined; pattern matching read them instead.",
      +              "maximum": 9007199254740991,
      +              "minimum": -9007199254740991,
      +              "type": "integer"
      +            },
      +            "filesScanned": {
      +              "description": "Source files read.",
      +              "maximum": 9007199254740991,
      +              "minimum": -9007199254740991,
      +              "type": "integer"
      +            },
      +            "ignoredCount": {
      +              "description": "Unresolvable keys excluded by an orphanScan ignorePattern.",
      +              "maximum": 9007199254740991,
      +              "minimum": -9007199254740991,
      +              "type": "integer"
      +            },
      +            "locale": {
      +              "description": "Locale the key definitions were resolved in.",
      +              "type": "string"
      +            },
      +            "message": {
      +              "description": "One sentence stating what the scan found.",
      +              "type": "string"
      +            },
      +            "searchedLayersByApp": {
      +              "additionalProperties": {
      +                "description": "Layers searched for that unit's key usages.",
      +                "items": {
      +                  "type": "string"
      +                },
      +                "type": "array"
      +              },
      +              "description": "What \"undefined\" meant per scan unit: a key defined only in a layer the unit does not consume is still undefined for it.",
      +              "propertyNames": {
      +                "description": "Scan unit name.",
      +                "type": "string"
      +              },
      +              "type": "object"
      +            },
      +            "uncertainCount": {
      +              "description": "Findings static extraction could not verify.",
      +              "maximum": 9007199254740991,
      +              "minimum": -9007199254740991,
      +              "type": "integer"
      +            },
      +            "undefinedCount": {
      +              "description": "Keys that render raw at runtime, which is the counter the always-on gate reads. After a write run this counts the ones still undefined.",
      +              "maximum": 9007199254740991,
      +              "minimum": -9007199254740991,
      +              "type": "integer"
      +            },
      +            "usedKeysChecked": {
      +              "description": "Distinct statically referenced keys across every scan unit.",
      +              "maximum": 9007199254740991,
      +              "minimum": -9007199254740991,
      +              "type": "integer"
      +            },
      +            "writtenCount": {
      +              "description": "Keys extracted into a locale file. Present only alongside written.",
      +              "maximum": 9007199254740991,
      +              "minimum": -9007199254740991,
      +              "type": "integer"
      +            }
      +          },
      +          "required": [
      +            "usedKeysChecked",
      +            "undefinedCount",
      +            "uncertainCount",
      +            "ignoredCount",
      +            "declaredCount",
      +            "filesScanned",
      +            "filesDeclined",
      +            "locale",
      +            "searchedLayersByApp",
      +            "message"
      +          ],
      +          "type": "object"
      +        },
      +        "uncertainKeys": {
      +          "description": "Findings static extraction cannot verify. Never written, never counted by the gate.",
      +          "items": {
      +            "additionalProperties": false,
      +            "properties": {
      +              "app": {
      +                "description": "Scan unit the usage lives in — an app name, a layer name, or \"project-root\".",
      +                "type": "string"
      +              },
      +              "key": {
      +                "description": "The key source code calls and no consumed layer defines.",
      +                "type": "string"
      +              },
      +              "reason": {
      +                "description": "Why this is not a hard finding — a dynamically built key, an existence check, a vendor namespace.",
      +                "type": "string"
      +              },
      +              "searchedLayers": {
      +                "description": "Layers that unit can resolve keys from. All were searched.",
      +                "items": {
      +                  "type": "string"
      +                },
      +                "type": "array"
      +              },
      +              "usages": {
      +                "description": "Every call site of the key.",
      +                "items": {
      +                  "additionalProperties": false,
      +                  "properties": {
      +                    "file": {
      +                      "description": "Source file, relative to the project directory.",
      +                      "type": "string"
      +                    },
      +                    "line": {
      +                      "description": "1-based line number of the reference.",
      +                      "maximum": 9007199254740991,
      +                      "minimum": -9007199254740991,
      +                      "type": "integer"
      +                    }
      +                  },
      +                  "required": [
      +                    "file",
      +                    "line"
      +                  ],
      +                  "type": "object"
      +                },
      +                "type": "array"
      +              }
      +            },
      +            "required": [
      +              "key",
      +              "app",
      +              "searchedLayers",
      +              "usages",
      +              "reason"
      +            ],
      +            "type": "object"
      +          },
      +          "type": "array"
      +        },
      +        "undefinedKeys": {
      +          "description": "Hard findings: keys that render raw at runtime. A write run leaves them listed, because the call sites are what a reader has to visit either way.",
      +          "items": {
      +            "additionalProperties": false,
      +            "properties": {
      +              "app": {
      +                "description": "Scan unit the usage lives in — an app name, a layer name, or \"project-root\".",
      +                "type": "string"
      +              },
      +              "key": {
      +                "description": "The key source code calls and no consumed layer defines.",
      +                "type": "string"
      +              },
      +              "searchedLayers": {
      +                "description": "Layers that unit can resolve keys from. All were searched.",
      +                "items": {
      +                  "type": "string"
      +                },
      +                "type": "array"
      +              },
      +              "usages": {
      +                "description": "Every call site of the key.",
      +                "items": {
      +                  "additionalProperties": false,
      +                  "properties": {
      +                    "file": {
      +                      "description": "Source file, relative to the project directory.",
      +                      "type": "string"
      +                    },
      +                    "line": {
      +                      "description": "1-based line number of the reference.",
      +                      "maximum": 9007199254740991,
      +                      "minimum": -9007199254740991,
      +                      "type": "integer"
      +                    }
      +                  },
      +                  "required": [
      +                    "file",
      +                    "line"
      +                  ],
      +                  "type": "object"
      +                },
      +                "type": "array"
      +              }
      +            },
      +            "required": [
      +              "key",
      +              "app",
      +              "searchedLayers",
      +              "usages"
      +            ],
      +            "type": "object"
      +          },
      +          "type": "array"
      +        },
      +        "written": {
      +          "additionalProperties": false,
      +          "description": "Present only when write was asked for and the scan found something to write.",
      +          "properties": {
      +            "keys": {
      +              "description": "The keys that reached the file, alphabetically.",
      +              "items": {
      +                "type": "string"
      +              },
      +              "type": "array"
      +            },
      +            "layer": {
      +              "description": "Layer the keys were added to.",
      +              "type": "string"
      +            },
      +            "locale": {
      +              "description": "The project default locale — the only one written, and the source every other is filled from.",
      +              "type": "string"
      +            }
      +          },
      +          "required": [
      +            "layer",
      +            "locale",
      +            "keys"
      +          ],
      +          "type": "object"
      +        }
      +      },
      +      "required": [
      +        "undefinedKeys",
      +        "uncertainKeys",
      +        "limitation",
      +        "summary"
      +      ],
      +      "type": "object"
      +    },
      +    {
      +      "additionalProperties": false,
      +      "properties": {
      +        "reportFile": {
      +          "description": "Absolute path the full JSON result was written to. Read the file for the findings; the summary below is all that came back.",
      +          "type": "string"
      +        },
      +        "summary": {
      +          "additionalProperties": false,
      +          "description": "The counts from the full result, so a caller can act on them without reading the file.",
      +          "properties": {
      +            "declaredCount": {
      +              "description": "Unresolvable keys covered by a declaredNamespaces entry — defined by contract, never written.",
      +              "maximum": 9007199254740991,
      +              "minimum": -9007199254740991,
      +              "type": "integer"
      +            },
      +            "filesDeclined": {
      +              "description": "Files a syntax frontend declined; pattern matching read them instead.",
      +              "maximum": 9007199254740991,
      +              "minimum": -9007199254740991,
      +              "type": "integer"
      +            },
      +            "filesScanned": {
      +              "description": "Source files read.",
      +              "maximum": 9007199254740991,
      +              "minimum": -9007199254740991,
      +              "type": "integer"
      +            },
      +            "ignoredCount": {
      +              "description": "Unresolvable keys excluded by an orphanScan ignorePattern.",
      +              "maximum": 9007199254740991,
      +              "minimum": -9007199254740991,
      +              "type": "integer"
      +            },
      +            "locale": {
      +              "description": "Locale the key definitions were resolved in.",
      +              "type": "string"
      +            },
      +            "message": {
      +              "description": "One sentence stating what the scan found.",
      +              "type": "string"
      +            },
      +            "searchedLayersByApp": {
      +              "additionalProperties": {
      +                "description": "Layers searched for that unit's key usages.",
      +                "items": {
      +                  "type": "string"
      +                },
      +                "type": "array"
      +              },
      +              "description": "What \"undefined\" meant per scan unit: a key defined only in a layer the unit does not consume is still undefined for it.",
      +              "propertyNames": {
      +                "description": "Scan unit name.",
      +                "type": "string"
      +              },
      +              "type": "object"
      +            },
      +            "uncertainCount": {
      +              "description": "Findings static extraction could not verify.",
      +              "maximum": 9007199254740991,
      +              "minimum": -9007199254740991,
      +              "type": "integer"
      +            },
      +            "undefinedCount": {
      +              "description": "Keys that render raw at runtime, which is the counter the always-on gate reads. After a write run this counts the ones still undefined.",
      +              "maximum": 9007199254740991,
      +              "minimum": -9007199254740991,
      +              "type": "integer"
      +            },
      +            "usedKeysChecked": {
      +              "description": "Distinct statically referenced keys across every scan unit.",
      +              "maximum": 9007199254740991,
      +              "minimum": -9007199254740991,
      +              "type": "integer"
      +            },
      +            "writtenCount": {
      +              "description": "Keys extracted into a locale file. Present only alongside written.",
      +              "maximum": 9007199254740991,
      +              "minimum": -9007199254740991,
      +              "type": "integer"
      +            }
      +          },
      +          "required": [
      +            "usedKeysChecked",
      +            "undefinedCount",
      +            "uncertainCount",
      +            "ignoredCount",
      +            "declaredCount",
      +            "filesScanned",
      +            "filesDeclined",
      +            "locale",
      +            "searchedLayersByApp",
      +            "message"
      +          ],
      +          "type": "object"
      +        }
      +      },
      +      "required": [
      +        "reportFile",
      +        "summary"
      +      ],
      +      "type": "object"
      +    }
      +  ],
      +  "type": "object"
      +}
  2. Addedv1.0.3

TDQS

A4.4/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Goes well beyond the annotations (readOnly=false, idempotent=true, destructive=false): it discloses that write mode adds empty translations and is only the first half of the fix, that existing values are never touched, that uncertain findings are never written, and that the run refuses when a key resolves against multiple layers. It also discloses the static, line-based extraction limitation and the uncertainKeys reporting behavior.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loads the core definition and scoping behavior before the limitation and write behavior. Dense but nearly every clause carries distinct information; the only mild redundancy is restating the scope/write refusal already in the schema.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

An output schema exists, so return values need no explanation, and the description still covers the essential behavioral contract — scope-aware matching, the static-analysis limitation, uncertain vs hard findings, and write-side effects. Nothing an agent needs to call it correctly is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% and the inline parameter descriptions are already highly detailed (layer/write interaction, scanDirs override, defaults for locale/projectDir). The description reinforces scope-aware resolution but adds little beyond what the schema text already states, so the baseline of 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource — 'Find keys referenced in source code but defined in NO locale layer' — and immediately disambiguates from the sibling by calling itself 'the inverse of find_orphan_keys.' An agent can distinguish this from find_orphan_keys and get_missing_translations without opening any schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Clearly frames when to use it: the direction that ships raw keys to production, and explicitly names find_orphan_keys as the inverse, plus discover for listing layers. It stops short of explicit exclusions ('do not use when...'), but the direction and the named alternative give strong routing guidance.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.