Skip to main content
Search...

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:

  1. Have a JSDoc comment containing a @duckgen tag.
  2. 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 const

Both 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:

TagGroup key
@duckgenUses the const name (e.g. AuthMessages)
@duckgen messagesUses the const name
@duckgen messageUses the const name
@duckgen authUses auth as the group key
@duckgen messages authUses auth as the group key
@duckgen message authUses 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 const

Group 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: auth and Auth are 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 const

Array 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 const

The 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 const

The 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:

src/common/constants.ts
export const ZodMessages = {
  ZOD_EXPECTED_STRING: 'ZOD_EXPECTED_STRING',
  ZOD_TOO_LONG: 'ZOD_TOO_LONG',
  ZOD_TOO_SHORT: 'ZOD_TOO_SHORT',
} as const
src/modules/auth/auth.constants.ts
import { ZodMessages } from '~/common/constants'
 
/** @duckgen messages auth */
export const AuthMessages = [
  'AUTH_SIGNIN_SUCCESS',
  'AUTH_SIGNIN_FAILED',
  ...Object.values(ZodMessages),
] as const

This 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

src/modules/auth/auth.constants.ts
/** @duckgen messages auth */
export const AuthMessages = [
  'AUTH_SIGNIN_SUCCESS',
  'AUTH_SIGNIN_FAILED',
  'AUTH_USERNAME_INVALID',
  'AUTH_PASSWORD_INVALID',
] as const

Step 2: Run Duck Gen

bunx duck-gen

Step 3: Build your i18n object on the client

client/i18n.ts
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

client/auth.ts
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:

src/modules/auth/auth.constants.ts
/** @duckgen messages auth */
export const AuthMessages = ['AUTH_SIGNIN_SUCCESS', 'AUTH_SIGNIN_FAILED'] as const
src/modules/users/users.constants.ts
/** @duckgen messages users */
export const UserMessages = ['USER_CREATED', 'USER_NOT_FOUND', 'USER_UPDATED'] as const
src/modules/payments/payments.constants.ts
/** @duckgen messages payments */
export const PaymentMessages = ['PAYMENT_SUCCESS', 'PAYMENT_DECLINED'] as const

All three groups appear in the generated types, and you can access them independently or together.

Troubleshooting

ProblemSolution
Keys are just stringAdd as const to your array or object.
Group not appearingMake sure the constant is exported and has a @duckgen JSDoc tag.
Duplicate group warningTwo constants use the same group key. Rename one.
Duplicate const nameTwo files export a constant with the same name. Rename one.
Spread values missingDuck 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