service.dictionary.ts

<service>.dictionary.ts labels, in every language, what a service module shows: its endpoints, its errors and its other phrases. Open it when you add an endpoint, throw a new error, or show a new line of text.
A model dictionary starts by naming fields. A service module has none, so its dictionary starts one stage later, at the endpoints, and the first line says so: serviceDictionary, not modelDictionary.
Words used on this page
label
The name a person reads, one entry per language: fn(["Disconnect App", "앱 연결 끊기"]).
.desc()
A longer sentence beside a label. An AI agent picks a tool by reading it.
key
The dotted path code reads a text by, such as oauth.consentTitle.
language tuple
One string per language, in the order serviceDictionary(["en", "ko"]) lists them.
Three stages
.endpoint<XEndpoint>((fn) => ({}))
One entry per signal endpoint: a label, a .desc(), and an .arg() for every argument.
.error({})
Every key the service throws as new Err("<service>.error.<key>"). Korean ends in 다.
.translate({})
Every other phrase, neither an endpoint nor an error. It is read by the bare key under the module.
  • Every stage is optional. Write the ones the module has something for: _security writes only .endpoint(), and _localFile has no .translate().
  • Any order works; one order is the house style. Each stage returns the same builder, but write endpoint → error → translate, the order a reader looks for them in.
  • The array is the language list. serviceDictionary(["en", "ko"]) fixes the order every tuple in the file follows.

Naming The Endpoints

Pass the signal's endpoint class to .endpoint() as a type argument, and give each endpoint a label and a .desc(). This is the whole file of _localFile, a module with one endpoint:
libs/util/lib/_localFile/localFile.dictionary.ts
  • The keys follow the endpoint class. The callback must return one entry per endpoint, so a renamed or added endpoint breaks the dictionary at compile time, not at the first render.
  • Import the class with import type. A dictionary is a shared contract file, and a value import would pull the signal's runtime graph in behind it.
  • Two parts are not optional: the .desc() beside the label, and the error key localFile.service.ts throws by name.
Who reads which text
People read the label; a model reads the description. Each piece shows up in these places:
Text
API explorer
OpenAPI
MCP
Endpoint
label
✓
✓
✓
The API explorer heading, the OpenAPI summary and the MCP tool title.
.desc()
✓
✓
✓
The description an agent picks a tool by. The API explorer shows it under the label.
Argument, in .arg()
label
✓
Shown beside the identifier in the API explorer, and nowhere else.
.desc()
✓
✓
✓
The argument's description in the MCP input schema and on OpenAPI path and query parameters.
✓Shown thereNot used

Naming Every Argument

.arg() names each argument the endpoint declares, including skip, limit and sort when a custom endpoint takes them. Leave one out and the dictionary does not compile:
libs/shared/lib/_oauth/oauth.dictionary.ts
  • Say where the value comes from. The description does not say what a session ID is; it says where the caller gets one: the connected-apps list.
  • That sentence is worth more than the type. It is the argument's description in the MCP input schema. Without it an agent has only the identifier's spelling, and guesses.
  • The label is for people. "Session ID" appears beside sessionId in the API explorer.

Errors And Phrases

An error key is the other half of a throw. The service writes new Err("oauth.error.notSignedIn"), and .error() is the only place that key becomes a sentence a person can read.
.translate() holds every other phrase the module shows. Both stages take plain language tuples, with no t() and no .desc():
libs/shared/lib/_oauth/oauth.dictionary.ts
  • Err accepts only registered keys. A typo, or a key missing from .error(), is a type error. A key with no text in the reader's language or the default one shows up as the raw key.
  • A brace pair is a slot the caller fills. l("oauth.connectedAt", { at }) fills Connected {at}, and new Err(key, { days }) fills an error the same way.
  • Nothing checks that the value was passed. A missing one leaves {at} in the text as written, so give the slot an obvious name.
Tone of voice
The ending follows who the sentence speaks to. Both conventions in the file above are deliberate:
.error()
Korean ends in 다.: a statement of what went wrong, not an apology.
.translate()
Plain 다. only for a bare statement; a line addressed to the user ends in 습니다 (consentScope).
label
English in Title Case, Korean as the plain domain term.

Reading A Key Back

Every key sits under the service's own name, and the stage decides what comes next. Endpoint labels get a signal segment so they stay clear of the phrases; phrases get none.
<service>.signal.<endpoint>
The endpoint label from .endpoint(). Read it with l().
<service>.signal.<endpoint>.desc
Its .desc(). Read it with l(), adding .desc to the label's key.
<service>.signal.<endpoint>.arg.<arg>
An argument label from .arg(). Read it with l().
<service>.error.<key>
An error from .error(). new Err() throws it on the server; msg.error() shows it on the client.
<service>.<key>
A phrase from .translate(). Read it with l().
The OAuth consent page reads its phrases this way, on the server. Markup is trimmed here:
libs/shared/page/oauth/consent/_index.tsx
  • usePage() works on the server too. The consent page is a server component, so its labels add no client boundary.
  • A thrown error needs no reading code. When a store action fails with an Err, the store toasts its text in the reader's language. For a client-side check, call msg.error("oauth.error.notSignedIn") and return.
  • One-off text belongs to the screen. A phrase used once, on one screen, is l.trans({ en, ko }) in that component. A key only one component reads is a key somebody keeps in sync for nothing.
Common mistakes
Instead of
↳ Write
import { OauthEndpoint } from "./oauth.signal"
import type. A value import pulls the signal's runtime graph into the dictionary.
An endpoint label with no .desc()
Write one. An agent picks a tool by its description.
An argument description that repeats the name: "The session ID"
Say where the caller gets the value: "from the connected-apps list".
A .translate() key only one component reads
l.trans({ en, ko }) inside that component.
Read next

Released under the MIT License

Connect your AI to these docs

MCPhttps://akanjs.com/mcp
Copyright © 2026 Akan.js All rights reserved.System managed bybassman