name: i18n-extract
description: "Use when extracting user-facing strings, moving literals into _locales/en-US.ts, and rewriting source to t(...), Trans, or dt(...) translation keys."
disable-model-invocation: true
user-invocable: true
i18n Extract Skill
This skill is user-invoked only. Run it only when the user explicitly invokes /i18n-extract.
Extract user-facing strings from source files into locale files, then update the source to use translation keys.
This workflow updates en-US.ts only unless the user explicitly asks to update other locales too.
Usage
/i18n-extract [file_or_directory] [options]
Input Contract
Required input:
- a target file or directory under
src/
Optional input:
- whether to update non-English locale files afterward
- whether to prefer reuse over new key creation when both are plausible
Defaults:
- update
en-US.tsonly - keep scope limited to the requested file or directory
- stop and ask when reuse vs new-key choice is genuinely ambiguous
Locale File Locations
Main app:
- Path:
src/entrypoints/_locales/{locale}.ts - Namespace:
"common"
Plugins:
- Path:
src/plugins/{plugin-path}/_locales/{locale}.ts - Namespace: read the exported
namespaceconstant fromsrc/plugins/{plugin-path}/_locales/index.ts
en-US Locale File Format
import type { LanguageMessages } from "@complexity/i18n";
export default {
keyName: "Simple string",
nested: {
key: "Nested value",
withParam: "Hello {name}",
withComponent: "Click <0>here</0> to continue",
},
} as const satisfies LanguageMessages;
Non-English locale files are a separate follow-up step and typically mirror the same shape using the local Translations type from _locales/index.ts.
File Inspection Order
Inspect files in this order. Do not guess before checking.
- Target source file(s)
- Closest target locale namespace file:
_locales/index.ts - Target
en-US.ts - Neighboring source usage of existing keys in the same feature
- Only then decide reuse vs new key, and formatter choice
Extraction Rules
- DO NOT import
torTrans- globally available (auto-imported) - Extract to
en-USfirst - English is source of truth - Use the real namespace - never infer plugin namespace from folder name; always read
_locales/index.ts - Use full key path - format:
namespace.section.key - Reuse existing keys when semantics match - search nearby locale sections before adding a new key
- Prefer nested semantic keys - base keys on UI role, not raw English text
- Use
t(...)for plain text - Only use
Transwhen the translated string contains component placeholders like<0>...</0> - Preserve placeholders exactly - convert runtime values into
{name}/{count:number}/{date}style placeholders - Do not extract non-visible DOM attributes - skip HTML
aria-label,alt, and DOMtitleattributes - Do extract visible component props - props like
title,label,description,placeholder,message,contentmay be visible UI text depending on the component - Do not update non-English locale files during extraction unless the user explicitly asks for that follow-up work
Key Naming Rules
- Prefer the existing subtree in the target locale file if one already matches the feature.
- Use stable semantic names such as
dialog.confirm.title,filters.sort.newest,buttons.save. - Do not create keys directly from the literal English text.
- Do not create flat one-off keys when a nested group already exists.
- Reuse an existing key only when the meaning matches, not just the raw text.
What To Extract
Extract visible user-facing strings from:
- JSX text nodes
- string literals rendered in components
- visible component props such as
label,description,placeholder,message,content,text - menu items, dialog content, toast titles/descriptions, button labels, filter labels, section headings
- strings inside arrays or objects when they are rendered to users
What To Skip
Skip:
- HTML
aria-label,alt, and DOMtitleattributes - internal identifiers, enum values, test text, analytics/event names, CSS class names
- URLs, file paths, selectors, storage keys, query parameter names
- strings already passed to
t(...)orTrans
Formatter Selection Rules
Choose the smallest correct mechanism:
| Case | Locale entry | Source rewrite |
|---|---|---|
| Static text | plain string | t(...) |
| Simple placeholders | plain string with {name} |
t(..., { name }) |
| Number/date/list formatting | plain string with {count:number}, {date:date}, {items:list} |
t(..., values) |
| Plural grammar | dt(...) with {count:plural} rules |
t(..., { count }) |
| Enum-dependent wording | dt(...) with {name:enum} rules |
t(..., { name }) |
| Embedded JSX/components | string with <0>...</0> placeholders |
<Trans ... /> |
Use plain strings for
- static copy
- simple runtime variables like
{name} - number/date/list formatting such as
{count:number},{date:date},{items:list}
Use dt(...) for
pluralvariationsenumvariations- only when locale-specific grammar actually depends on the variable
Do not use dt(...) for
- plain
{name}interpolation {count:number}/{date:date}/{items:list}formatting- JSX/component placeholders handled by
Trans
Transformation Rules
Plain strings
<Button>Save</Button>
Becomes something like:
<Button>{t("common.buttons.save")}</Button>
Interpolated values
`Expires ${date}`
Becomes locale text like:
expires: "Expires {date}"
and source like:
t("plugin-command-menu.common.expires", { date })
Plural grammar
limited: dt("{count:plural} left", {
plural: {
count: {
one: "1 use",
other: "{?} uses",
},
},
})
and source like:
t("plugin-model-selectors.languageModelSelector.usesLeft.limited", { count })
Enum grammar
description: dt("Click to view {name:enum}", {
enum: {
name: {
markdown: "content",
mermaid: "diagram",
html: "web page",
},
},
})
and source like:
t("plugin-artifacts.placeholder.description", { name: artifactType })
Component placeholders
<p>
Click <a href={url}>here</a> to continue
</p>
Becomes locale text like:
continueMessage: "Click <0>here</0> to continue"
and source like:
<Trans
tKey="common.continueMessage"
components={[<a href={url} />]}
/>
Duplicate And Reuse Strategy
- Search the target
en-US.tsfile for an existing nearby subtree first. - Search the rest of the same locale file for the same English text.
- Reuse an existing key only if the semantic meaning and grammatical role match.
- Prefer local namespace reuse over cross-namespace reuse.
- Do not move plugin-specific text into
commonjust because the English text looks generic. - If no good match exists, add a new nested key in the closest feature subtree.
Anti-Patterns
Wrong namespace guess
Bad:
t("plugin-language-model-selector.tooltip")
Good:
t("plugin-model-selectors.languageModelSelector.tooltip")
Wrong dt(...) usage for simple formatting
Bad:
countLabel: dt("Files ({count:number})", {})
Good:
countLabel: "Files ({count:number})"
Wrong Trans usage for plain text
Bad:
<Trans tKey="common.buttons.save" />
Good:
{t("common.buttons.save")}
Wrong skip of visible component props
Bad:
- skipping
<SettingsItem title="Debug" description="Include in bug reports" />
Good:
- inspect the component semantics first; extract if the prop is visible UI text
Stop Conditions
Stop and ask the user instead of guessing when:
- the target path is missing, invalid, or outside
src/ - no matching locale tree or
_locales/index.tscan be found - the string is built so dynamically that safe placeholder conversion is unclear
- reuse vs new-key choice would materially change meaning
- it is unclear whether a prop like
titleis a DOM attribute or visible component text after inspecting the component usage - extraction would require broad restructuring rather than a local i18n rewrite
- the requested directory scope is large enough to create risky bulk churn
- the user appears to want translation of non-English locale files as part of the same step
When you stop, report:
- exact file
- exact string or pattern
- why it is ambiguous
- the smallest decision the user needs to make
Required Workflow
- Determine whether the target file belongs to
commonor a plugin locale tree. - Open the target
_locales/index.tsand read the exportednamespace. - Open the target
en-US.tslocale file. - Search the locale file for an existing matching section or reusable key.
- Decide the correct formatter: plain string,
dt(...), orTrans. - Add new English strings only when no good existing key matches.
- Rewrite source code to use
t(...)orTrans. - Ensure no manual
t/Transimports were added. - Leave non-English locale files untouched unless the user explicitly requested them.
- Validate the result with typecheck and lint.
Output Contract
On completion, report these sections:
- Files changed
- Keys added
- Keys reused
- Strings skipped and why
- Follow-up needed, if any
If no changes were made, say explicitly:
- no extractable strings found, or
- stopped due to ambiguity and waiting for user input
Validation Checklist
- Correct locale file updated
- Correct namespace used
- No guessed namespace from folder name
- No manual
torTransimports - Placeholders preserved
dt(...)used for plural/enum cases that need grammar rules- Plain strings kept for simple number/date/list formatting
Transused only for component placeholders- Non-visible DOM attributes skipped
- Visible component props not skipped by mistake
- Non-English locale files untouched unless requested
tscpassespnpm lintpasses