messages
Learn how Duck Gen scans @duckgen tags and generates strongly-typed i18n dictionaries and message registries.
Overview
The messages extension scans your source files for exported constants tagged with @duckgen
JSDoc comments. It reads the keys from those constants and generates TypeScript types that
describe your message registry, giving you compile-time safety for i18n dictionaries,
error codes, and any other string-keyed data you define on the server.
Loading diagram...
What it scans
Duck Gen looks for exported const declarations that:
- Have a JSDoc comment containing a
@duckgentag. - Are either an array or an object marked with
as const.
// Array source — values become message keys
/**
* @duckgen messages auth
*/
export const AuthMessages = [
'AUTH_SIGNIN_SUCCESS',
'AUTH_SIGNIN_FAILED',
'AUTH_USERNAME_INVALID',
] as const
// Object source — keys become message keys
/**
* @duckgen messages payments
*/
export const PaymentMessages = {
PAYMENT_SUCCESS: 'PAYMENT_SUCCESS',
PAYMENT_DECLINED: 'PAYMENT_DECLINED',
PAYMENT_INSUFFICIENT_FUNDS: 'PAYMENT_INSUFFICIENT_FUNDS',
} as constBoth formats produce the same result: a set of typed message keys grouped under a group name.
Tag format
The @duckgen JSDoc tag supports several formats. All of these are valid:
| Tag | Group key |
|---|---|
@duckgen | Uses the const name (e.g. AuthMessages) |
@duckgen messages | Uses the const name |
@duckgen message | Uses the const name |
@duckgen auth | Uses auth as the group key |
@duckgen messages auth | Uses auth as the group key |
@duckgen message auth | Uses auth as the group key |
When to use a custom group key: Use a custom group key when you want a short, clean name for your i18n scope. Without one, the const variable name is used, which may be longer than you want.
// Group key = "auth" (explicit)
/** @duckgen messages auth */
export const AuthMessages = [ ... ] as const
// Group key = "AuthMessages" (inferred from const name)
/** @duckgen */
export const AuthMessages = [ ... ] as constGroup key rules
- Each group key must be unique across your entire project.
- If two constants share the same group key, the duplicate is skipped with a warning.
- If two constants share the same variable name, the duplicate is warned and skipped.
- Group keys are case-sensitive:
authandAuthare different groups.
The as const requirement
You must use as const on your arrays and objects. Without it, TypeScript widens the
types to string[] or Record<string, string>, and Duck Gen cannot extract the literal
message keys.
// Without as const — keys become just `string`
export const Messages = ['MSG_ONE', 'MSG_TWO']
// With as const — keys are literal types 'MSG_ONE' | 'MSG_TWO'
export const Messages = ['MSG_ONE', 'MSG_TWO'] as constArray vs object sources
Array source
Use arrays when your message keys are simple string identifiers:
/** @duckgen messages auth */
export const AuthMessages = [
'AUTH_SIGNIN_SUCCESS',
'AUTH_SIGNIN_FAILED',
'AUTH_USERNAME_INVALID',
'AUTH_PASSWORD_INVALID',
] as constThe array values become the message keys. The generated type maps each key to a string
value (the translation text).
Object source
Use objects when you want to associate message keys with status codes or other metadata:
/** @duckgen messages auth */
export const AuthMessages = {
AUTH_SIGNIN_SUCCESS: 200,
AUTH_SIGNIN_FAILED: 500,
AUTH_USERNAME_INVALID: 401,
AUTH_PASSWORD_INVALID: 401,
} as constThe object keys become the message keys. The values can be anything. Duck Gen only uses the keys.
Mixing shared constants
You can spread shared constants into your arrays:
export const ZodMessages = {
ZOD_EXPECTED_STRING: 'ZOD_EXPECTED_STRING',
ZOD_TOO_LONG: 'ZOD_TOO_LONG',
ZOD_TOO_SHORT: 'ZOD_TOO_SHORT',
} as constimport { ZodMessages } from '~/common/constants'
/** @duckgen messages auth */
export const AuthMessages = [
'AUTH_SIGNIN_SUCCESS',
'AUTH_SIGNIN_FAILED',
...Object.values(ZodMessages),
] as constThis way, your auth i18n dictionary includes both auth-specific and shared validation messages.
Generated types
The messages extension generates a .d.ts file with several types. Here is what each one
does and how to use it:
DuckgenMessageSources
A type registry mapping group keys to their source types:
type DuckgenMessageSources = {
auth: typeof AuthMessages
payments: typeof PaymentMessages
}DuckgenMessageGroup
A union of all group keys:
type DuckgenMessageGroup = 'auth' | 'payments'DuckgenMessageKey
Extract message keys for a specific group or all groups:
// Keys for a specific group
type AuthKey = DuckgenMessageKey<'auth'>
// => 'AUTH_SIGNIN_SUCCESS' | 'AUTH_SIGNIN_FAILED' | ...
// All keys across all groups
type AllKeys = DuckgenMessageKey
// => 'AUTH_SIGNIN_SUCCESS' | ... | 'PAYMENT_SUCCESS' | ...DuckgenMessageDictionary
A record mapping message keys to string translations for a group:
type AuthDictionary = DuckgenMessageDictionary<'auth'>
// => { AUTH_SIGNIN_SUCCESS: string; AUTH_SIGNIN_FAILED: string; ... }DuckgenMessageDictionaryByGroup
All groups mapped to their dictionaries:
type AllDicts = DuckgenMessageDictionaryByGroup
// => { auth: { AUTH_SIGNIN_SUCCESS: string; ... }; payments: { PAYMENT_SUCCESS: string; ... } }DuckGenI18nMessages
An alias of DuckgenMessageDictionaryByGroup. Use whichever name reads better in your code.
DuckgenI18n
Maps language codes to a dictionary for one or more groups:
type AuthI18n = DuckgenI18n<'en' | 'ar', 'auth'>
// => { en: { AUTH_SIGNIN_SUCCESS: string; ... }; ar: { AUTH_SIGNIN_SUCCESS: string; ... } }DuckgenI18nByGroup
Maps language codes to all group dictionaries:
type FullI18n = DuckgenI18nByGroup<'en' | 'ar'>
// => { en: { auth: { ... }; payments: { ... } }; ar: { auth: { ... }; payments: { ... } } }DuckgenScopedI18nByGroup
Wraps all groups under a server scope. Useful when you have both server-generated and
client-only messages:
type ScopedI18n = DuckgenScopedI18nByGroup<'en' | 'ar', DuckGenI18nMessages>
// => { en: { server: { AuthMessages: { ... }; PaymentMessages: { ... } } }; ar: { ... } }Real-world i18n example
Here is a complete example of using generated message types to build a type-safe i18n system:
Step 1: Define messages on the server
/** @duckgen messages auth */
export const AuthMessages = [
'AUTH_SIGNIN_SUCCESS',
'AUTH_SIGNIN_FAILED',
'AUTH_USERNAME_INVALID',
'AUTH_PASSWORD_INVALID',
] as constStep 2: Run Duck Gen
bunx duck-genStep 3: Build your i18n object on the client
import type { DuckgenScopedI18nByGroup, DuckGenI18nMessages } from '@gentleduck/gen/nestjs'
type SupportedLang = 'en' | 'ar'
type I18n = DuckgenScopedI18nByGroup<SupportedLang, DuckGenI18nMessages>
export const i18n: I18n = {
en: {
server: {
AuthMessages: {
AUTH_SIGNIN_SUCCESS: 'Signed in successfully',
AUTH_SIGNIN_FAILED: 'Sign in failed',
AUTH_USERNAME_INVALID: 'Invalid username',
AUTH_PASSWORD_INVALID: 'Invalid password',
},
},
},
ar: {
server: {
AuthMessages: {
AUTH_SIGNIN_SUCCESS: 'تم تسجيل الدخول بنجاح',
AUTH_SIGNIN_FAILED: 'فشل تسجيل الدخول',
AUTH_USERNAME_INVALID: 'اسم المستخدم غير صحيح',
AUTH_PASSWORD_INVALID: 'كلمة المرور غير صحيحة',
},
},
},
}TypeScript will error if you:
- Misspell a message key.
- Forget to add a translation for a key.
- Add a key that doesn't exist in the server constants.
Step 4: Use in your UI
import { i18n } from './i18n'
function showSigninError(lang: 'en' | 'ar', messageKey: string) {
const text = i18n[lang].server.AuthMessages[messageKey]
toast.error(text)
}Multiple message groups
You can have as many message groups as you need. Each module can define its own:
/** @duckgen messages auth */
export const AuthMessages = ['AUTH_SIGNIN_SUCCESS', 'AUTH_SIGNIN_FAILED'] as const/** @duckgen messages users */
export const UserMessages = ['USER_CREATED', 'USER_NOT_FOUND', 'USER_UPDATED'] as const/** @duckgen messages payments */
export const PaymentMessages = ['PAYMENT_SUCCESS', 'PAYMENT_DECLINED'] as constAll three groups appear in the generated types, and you can access them independently or together.
Troubleshooting
| Problem | Solution |
|---|---|
Keys are just string | Add as const to your array or object. |
| Group not appearing | Make sure the constant is exported and has a @duckgen JSDoc tag. |
| Duplicate group warning | Two constants use the same group key. Rename one. |
| Duplicate const name | Two files export a constant with the same name. Rename one. |
| Spread values missing | Duck Gen reads the literal values. If you spread a dynamic expression, those values may not be captured. Use as const on the source too. |
Next steps
- Generated types reference: full type API for messages.
- Templates: complete example with messages, controllers, and Duck Query.
- Duck Query: pair your types with a type-safe HTTP client.