Skip to content

Commit 6facf2f

Browse files
docs: add plugin i18n skill
1 parent 8eeb02b commit 6facf2f

5 files changed

Lines changed: 270 additions & 3 deletions

File tree

‎.agents/skills/dsh-plugin-development/SKILL.md‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,10 @@ For an existing non-template repository, load `dsh-plugin-align` from `.agents/s
6363
1. Load `dsh-plugin-plan` from `.agents/skills/dsh-plugin-plan/SKILL.md`. Leave planning only after the plugin form, roles, dependencies, configuration, invariant decision, composition, test tiers, and distribution assumptions are explicit.
6464
2. For a new repository, load `dsh-plugin-scaffold` from `.agents/skills/dsh-plugin-scaffold/SKILL.md`. Leave scaffolding only after identity, files, dependency metadata, and the package's actual static-analysis, test, build, and pack checks pass. Skip file creation for an existing plugin, but still audit it against the scaffold exit conditions.
6565
3. Load `dsh-plugin-implement` from `.agents/skills/dsh-plugin-implement/SKILL.md`. Implement only the planned behavior, update the package contract and invariant companion, and keep all registrations owned by the plugin fiber.
66-
4. Load `dsh-plugin-compose` from `.agents/skills/dsh-plugin-compose/SKILL.md` when the package contributes a profile bundle or must be proven in an assembled DSH profile. Verify the effective rows rather than assuming the patch applied.
67-
5. Load `dsh-plugin-test` from `.agents/skills/dsh-plugin-test/SKILL.md`. Run the smallest evidence that covers the behavior; product-visible plugins require a real Loader/profile composition test in addition to hand-mounted unit tests.
68-
6. Load `dsh-plugin-release` from `.agents/skills/dsh-plugin-release/SKILL.md` for Git/npm delivery or when claiming the package is distribution-ready. Local-only work still performs its placeholder, exports, files-list, and build-artifact checks.
66+
4. Load `dsh-plugin-i18n` from `.agents/skills/dsh-plugin-i18n/SKILL.md` when the plugin owns browser UI copy. Register the feature's typed dictionaries, route copy through the locale seat, and prove fallback and disposal before composing or releasing the client face.
67+
5. Load `dsh-plugin-compose` from `.agents/skills/dsh-plugin-compose/SKILL.md` when the package contributes a profile bundle or must be proven in an assembled DSH profile. Verify the effective rows rather than assuming the patch applied.
68+
6. Load `dsh-plugin-test` from `.agents/skills/dsh-plugin-test/SKILL.md`. Run the smallest evidence that covers the behavior; product-visible plugins require a real Loader/profile composition test in addition to hand-mounted unit tests.
69+
7. Load `dsh-plugin-release` from `.agents/skills/dsh-plugin-release/SKILL.md` for Git/npm delivery or when claiming the package is distribution-ready. Local-only work still performs its placeholder, exports, files-list, and build-artifact checks.
6970

7071
Stages may be performed in one coding pass, but their exit conditions do not disappear. Planning is never skipped. Composition may be omitted only for a package that deliberately declares no bundle and is tested through its actual consumer. Publishing actions require a direct user request; release readiness never implies permission to publish, tag, push, or create a remote.
7172

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
---
2+
name: dsh-plugin-i18n
3+
description: Use when a standalone DSH plugin adds or changes browser UI copy and needs typed locale dictionaries, namespace registration, translated component props, or optional language-pack contributions. Do not use for host-only plugins or user, model, protocol, and other non-UI data.
4+
---
5+
6+
# Localize a DSH Plugin
7+
8+
This skill adds localization to a plugin that owns browser UI. It covers the ordinary plugin that ships its own `zh`/`en` copy and the separate language-pack plugin that contributes an external language. It is guidance, not permission to add a language, copy, dependency, or public UI contract that the product does not need.
9+
10+
## Read the owners first
11+
12+
Run this skill from the plugin repository root. Read `AGENTS.md`, `README.md`, `docs/dsh-plugin-contracts.md`, `package.json`, the actual browser entry, its slot contracts, the focused component tests, and the applicable `cordis.patch.yml`. If the package has no browser face or no product-visible copy, stop: do not add a locale dependency for host, model, user, wire, or internal data.
13+
14+
Keep every source and documentation input below the repository root. Do not read a DSH checkout or another plugin as an implicit source. Use installed package declarations and this repository's manifest to inspect host APIs. Preserve the plugin's function or service Loader form.
15+
16+
Record these facts before editing:
17+
18+
| Area | Record |
19+
|---|---|
20+
| Browser owner | client entry, components, slots, and whether the package is static or dynamic |
21+
| Copy inventory | visible text, labels, aria names, titles, placeholders, status text, and formatters |
22+
| Namespace | stable package-owned namespace and its key owner |
23+
| Services | Cordis `locale` service and other actual dependencies |
24+
| Languages | built-in `zh`/`en`, plus any separately supplied external language |
25+
| Tests | component, lifecycle, composition, and snapshot owners |
26+
27+
## Keep data and copy separate
28+
29+
Localize product-visible UI copy, including:
30+
31+
- text rendered to users;
32+
- button and link labels;
33+
- `aria-label`, `title`, placeholder, empty-state, loading, and error copy;
34+
- visible units or status formatters that the plugin owns;
35+
- labels passed to a zero-Cordis primitive.
36+
37+
Do not translate user input, model output, provider names, protocol fields, tool names, stable ids, error codes, CSS tokens, or values used for matching and branching. Internal behavior switches on discriminants and stable ids, never on translated strings. A failure code may remain a code while a separately owned UI message translates its presentation.
38+
39+
Every visible string has one owner. Do not hide English or Chinese fallback text in a component, primitive, hook, store, injected callback, or slot registration. A Cordis-free primitive receives complete localized label props; a feature plugin owns the dictionary and supplies those props.
40+
41+
## Choose one namespace per feature
42+
43+
Give the plugin a stable namespace such as `vendor.feature` or `settings.vendor-feature`. The namespace is the key passed to `ctx.locale.register()`, `ctx.locale.bind()`, and a slot registration's `locale` field.
44+
45+
Keep feature copy out of `common`. Use `common` only for vocabulary deliberately shared by several features. Do not create a namespace per component when the copy belongs to one feature owner. Do not let two plugins own the same `(namespace, locale)` pair; registration rejects the duplicate instead of silently overwriting it.
46+
47+
## Define typed dictionaries
48+
49+
Put the ordinary plugin dictionaries in the browser source owner, commonly `src/client/locales.ts`:
50+
51+
```ts
52+
/** Feature namespace dictionaries. */
53+
export const zh = {
54+
'panel.title': '功能面板',
55+
'panel.save': '保存',
56+
'panel.count': '{count} 个项目',
57+
} satisfies Record<string, string>
58+
59+
export type FeatureKey = keyof typeof zh
60+
61+
export const en = {
62+
'panel.title': 'Feature Panel',
63+
'panel.save': 'Save',
64+
'panel.count': '{count} items',
65+
} satisfies Record<FeatureKey, string>
66+
```
67+
68+
The `zh` object supplies the key union. The `en` constraint makes a missing or extra key a compile-time error. Keep dictionaries flat and use stable semantic keys, not English sentences as keys. Values may contain `{name}` placeholders; the component supplies their values to `t(key, params)`.
69+
70+
Declare the namespace in the module that owns the browser plugin's public client contract:
71+
72+
```ts
73+
import type {} from '@deepseek-ai/dsh-client-locale/client'
74+
import type {} from '@deepseek-ai/dsh-client-ui-slots'
75+
import { en, zh, type FeatureKey } from './locales.ts'
76+
77+
declare module '@deepseek-ai/dsh-client-ui-slots' {
78+
interface LocaleNamespaceMap {
79+
'vendor.feature': FeatureKey
80+
}
81+
}
82+
```
83+
84+
The type-only locale import supplies the `ctx.locale` Context augmentation. The `LocaleNamespaceMap` augmentation supplies the typed `register`, `bind`, and component locale prop. Do not add a runtime import merely to obtain those types.
85+
86+
## Register ordinary plugin copy
87+
88+
Register the built-in dictionaries in the plugin's `apply` function and make the returned disposer fiber-owned:
89+
90+
```ts
91+
const NS = 'vendor.feature' as const
92+
93+
export const inject = ['locale', 'slots']
94+
95+
export function apply(ctx: ClientContext): void {
96+
ctx.effect(
97+
() => ctx.locale.register(NS, { zh, en }),
98+
'vendor.feature: dictionaries',
99+
)
100+
101+
// Register the plugin's actual slot contribution here, with `locale: NS`.
102+
}
103+
```
104+
105+
`ctx.locale.register(NS, { zh, en })` is the typed built-in form. It registers one complete dictionary for every built-in language and checks its keys against the namespace's `LocaleNamespaceMap` entry. The call returns an idempotent disposer; use `ctx.effect()` rather than a module-level registration.
106+
107+
The `locale` in the exported Cordis `inject` list is a service name. If this is a dynamic browser package, also declare the locale package in the package's client dependency metadata and in the peer/development dependency sections required by the repository's client-package contract. `dsh.client.inject` package-name edges and Cordis `inject` service names are different declarations; keep both accurate when both are required.
108+
109+
## Pass translation through slots
110+
111+
Registering a dictionary does not make `t` appear in every component. A slot contribution opts into the framework translation seat:
112+
113+
```ts
114+
ctx.slots.register({
115+
name: 'vendor.feature.panel',
116+
locale: NS,
117+
}, FeaturePanel)
118+
```
119+
120+
Derive component props from the slot framework instead of hand-writing the locale member:
121+
122+
```tsx
123+
import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
124+
125+
export type FeaturePanelProps =
126+
PropsRuntime<'vendor.feature.panel'> & PropsLocale<'vendor.feature'>
127+
128+
export function FeaturePanel({ t }: FeaturePanelProps) {
129+
return (
130+
<section aria-label={t('panel.title')}>
131+
<h2>{t('panel.title')}</h2>
132+
<button type="button">{t('panel.save')}</button>
133+
<p>{t('panel.count', { count: 3 })}</p>
134+
</section>
135+
)
136+
}
137+
```
138+
139+
Use the four prop shares required by the client contract: owner/runtime data, render slots, declared store state/actions, and injected values. Add `PropsLocale<N>` as the locale share when the registration declares `locale: N`. Components do not call `ctx`, access a service class, create a subscription, or receive a bare locale observable. The framework hook updates `t` when the active locale or dictionary revision changes.
140+
141+
If a child component is not itself a slot registrant, pass localized strings or a localized callback as ordinary props from its owner. Do not create a second translation hook or a fallback dictionary inside the child.
142+
143+
## Register an external language separately
144+
145+
A normal feature plugin usually supplies only `zh` and `en`. A language-pack plugin may add a language definition and the feature's dictionary as separate effects:
146+
147+
```ts
148+
export const inject = ['locale']
149+
150+
export function apply(ctx: ClientContext): void {
151+
ctx.effect(
152+
() => ctx.locale.addLanguage({
153+
id: 'ja',
154+
label: '日本語',
155+
fallback: 'en',
156+
}),
157+
'ja: language',
158+
)
159+
160+
ctx.effect(
161+
() => ctx.locale.register('vendor.feature', 'ja', {
162+
'panel.title': '機能パネル',
163+
'panel.save': '保存',
164+
'panel.count': '{count} 件',
165+
}),
166+
'ja: vendor.feature dictionary',
167+
)
168+
}
169+
```
170+
171+
`addLanguage()` registers only the selectable language definition (`id`, `label`, and `fallback`); it does not register translations. `register(ns, locale, dict)` registers one language dictionary. The external id must be an ASCII BCP 47-style tag such as `ja`, `pt-BR`, or `zh-Hant`. The fallback chain must terminate at `en`; malformed ids, duplicate language definitions, unknown fallback targets, and cycles fail when the language is registered.
172+
173+
The language definition and dictionary may register in either order. A dictionary can exist before its language definition or before the feature plugin that consumes it. If the feature plugin is absent, the dictionary is dormant. If it later registers its `zh`/`en` dictionaries, those entries coexist. If both plugins register the same `(namespace, locale)`, the second registration fails; agree on one owner rather than relying on order.
174+
175+
Do not use `addLanguage()` to predeclare a feature's translations. If the plugin does not own a new selectable language, do not call it at all. A language pack that only calls `addLanguage()` adds a language selector entry but provides no feature copy.
176+
177+
## Fallback and missing keys
178+
179+
Lookup first walks the active language's fallback chain in the requested namespace, then repeats that chain in `common`, and finally returns the key itself. Therefore an external language may provide only the keys it translates:
180+
181+
```ts
182+
ctx.locale.register('vendor.feature', 'ja', {
183+
'panel.title': '機能パネル',
184+
})
185+
```
186+
187+
If `panel.save` is missing in Japanese, the lookup continues to the declared fallback dictionaries. Do not add component-side `?? 'Save'` or `if (locale === 'ja')` branches; those bypass the registry's fallback and make disposal or late registration inconsistent.
188+
189+
Locale registration and dictionary registration are independent from feature activation. A late dictionary registration changes the locale revision so mounted slots can render it without a remount. Registering a dictionary does not itself change the active locale or emit the active-locale change event.
190+
191+
## Lifecycle, ownership, and disposal
192+
193+
Every locale definition and dictionary registration must be owned by the plugin fiber through `ctx.effect()`. Test the observable cleanup after disposing the plugin fiber:
194+
195+
- the language pack is removed from the selectable locale list;
196+
- dictionaries owned by the plugin no longer resolve;
197+
- an active removed language returns to the available browser/default locale;
198+
- a saved unavailable preference remains pending until its definition returns;
199+
- a later registration can claim a `(namespace, locale)` pair after the prior owner is disposed.
200+
201+
Do not manually call another plugin's disposer, mutate the locale registry's internal maps, or register dictionaries at module evaluation time.
202+
203+
## Dependency and package rules
204+
205+
For a browser plugin, inspect the current client package contract before changing the manifest:
206+
207+
1. Add the locale package as a runtime peer when the host supplies the locale service, and as a development dependency when source typechecking or tests import it.
208+
2. Add a dynamic client package-name edge only when the bundle's module graph requires it; do not use client metadata to replace Cordis service injection.
209+
3. Keep the browser entry and locale dictionary in the package's published `files` list and ensure the built client entry exports/types remain complete.
210+
4. Keep host-only dependencies and browser-only dependencies on their actual build faces; never pull a browser locale runtime into a host entry just to share types.
211+
5. Update the profile patch only when the client package must be composed into a profile. A locale dictionary does not by itself require a new host patch row.
212+
213+
If a language-pack plugin needs a target feature's key type, prefer a type-only import from the target's public client contract and a declared development dependency. If that dependency is unavailable, use a deliberately untyped `register(ns, locale, dict)` call only after recording the loss of compile-time key checking; do not add a runtime dependency or a host-source path solely for translation metadata.
214+
215+
## Tests and evidence
216+
217+
Map each changed copy path to an assertion before editing. At minimum, select the applicable evidence:
218+
219+
| Behavior | Evidence |
220+
|---|---|
221+
| Dictionary key parity | `zh` key source plus `en satisfies Record<FeatureKey, string>`; add a schema/dictionary test when the package has one |
222+
| Typed `t` calls | Component test renders the feature and exercises a namespace key and placeholder |
223+
| Locale switching | Test the component or real client composition under `zh` and `en`, asserting visible text and accessibility labels |
224+
| Registration | Mount through the real client/Loader path and observe the dictionary or slot contribution |
225+
| Disposal | Dispose the plugin fiber and observe the dictionary/registration disappears |
226+
| External language | Test language definition, fallback, late dictionary registration, duplicate ownership, and removal when the plugin owns these paths |
227+
| Product-visible output | Use the package's real assembled composition and update a keyless snapshot when stable user-visible output changes |
228+
| Distribution | Build the client face, import the built entry, and inspect the packed file list |
229+
230+
Do not test localization by inspecting private maps, render counts, or the translation function's identity. Assert rendered text, labels, locale snapshots, registry entries, and real removal. Do not add snapshots merely because dictionaries exist; add or update them when the assembled product output changes.
231+
232+
## Documentation and verification
233+
234+
Update the plugin's `README.md` and any bilingual counterpart when localization changes the public setup, supported languages, or user-visible behavior. Document the namespace and registration owner once; do not paste the entire dictionary into a README. Keep configuration JSDoc and profile composition metadata synchronized if a locale-related config field exists. Follow the repository's local documentation and translation-pairing rules; do not invoke an automated translation tool unless the user requested it.
235+
236+
Run the narrowest checks for the changed face, then the package baseline:
237+
238+
```sh
239+
pnpm run lint
240+
pnpm test
241+
pnpm run build
242+
pnpm pack --dry-run --json
243+
```
244+
245+
For a browser-facing plugin, also run the repository's configured GUI, real-composition, built-artifact, and snapshot checks when those surfaces exist. Report exact commands, passed tests, changed snapshots, skipped credential-dependent cases, and any pre-existing failure separately. Never claim an i18n change is complete because the dictionary compiles alone: the slot receives the `locale` declaration, the visible copy renders through `t`, and the registration disposes correctly.
246+
247+
## Exit condition
248+
249+
The localization work is complete only when:
250+
251+
- the namespace and dictionary owner are explicit;
252+
- every product-visible string in the changed feature uses the locale seat or localized props;
253+
- `zh`/`en` keys are type-checked and external language ownership is unambiguous;
254+
- `addLanguage()` is used only for a new selectable language, never as a dictionary shortcut;
255+
- registrations are fiber-owned and disposal is observed;
256+
- absent consumer plugins, late installation, fallback, and duplicate ownership behave as documented;
257+
- the manifest, client bundle metadata, README, tests, and snapshots are synchronized where applicable;
258+
- the exact selected checks pass, with unrelated failures reported rather than hidden.
259+
260+
Return a handoff naming the namespace, dictionary files, locale/service dependencies, slot registrations, external languages, disposal evidence, visible output or snapshots, package metadata, and exact commands run.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
interface:
2+
display_name: "Localize DSH Plugin"
3+
short_description: "Add typed locale dictionaries to a client plugin"
4+
default_prompt: "Use $dsh-plugin-i18n to localize this DSH plugin's browser UI, register owned dictionaries, wire the locale seat, and verify fallback and disposal."

0 commit comments

Comments
 (0)