Skip to main content

messages

Learn how Duck Gen scans @duckgen tags and generates strongly-typed i18n dictionaries and message registries.

Overview

The messages extension scans source files for exported constants tagged with a @duckgen JSDoc comment. It reads the keys from those constants and emits TypeScript types that describe the message registry — compile-time safety for i18n dictionaries, error codes, or any string-keyed data declared on the server.

Loading diagram...

What it scans

Duck Gen picks up exported const declarations that:

  1. Have a JSDoc comment with a @duckgen tag.
  2. Are an array or object 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
// 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: typed message keys grouped under a name.

Tag format

Valid forms of the @duckgen JSDoc tag:

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 pass a custom group key: Pass a custom key for a short scope name. Without one, the const variable name is used, which is often longer.

// 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 = "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 project-wide.
  • Two constants with the same group key — the duplicate is skipped with a warning.
  • Two constants with the same variable name — the duplicate is skipped with a warning.
  • Group keys are case-sensitive: auth and Auth are different groups.

The as const requirement

as const is required on the array or object. Without it, TypeScript widens the types to string[] or Record<string, string> and Duck Gen can't read the literal 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
// 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 the keys are plain identifiers:

/** @duckgen messages auth */
export const AuthMessages = [
  'AUTH_SIGNIN_SUCCESS',
  'AUTH_SIGNIN_FAILED',
  'AUTH_USERNAME_INVALID',
  'AUTH_PASSWORD_INVALID',
] as const
/** @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 (the translation text).

Object source

Use objects to attach status codes or other metadata to each key:

/** @duckgen messages auth */
export const AuthMessages = {
  AUTH_SIGNIN_SUCCESS: 200,
  AUTH_SIGNIN_FAILED: 500,
  AUTH_USERNAME_INVALID: 401,
  AUTH_PASSWORD_INVALID: 401,
} as const
/** @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. Values can be anything — Duck Gen only uses the keys.

Mixing shared constants

Spread shared constants into an array:

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/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
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

The auth i18n dictionary now covers both auth-specific and shared validation messages.

Generated types

The messages extension writes a .d.ts file with these types:

DuckgenMessageSources

A type registry mapping group keys to their source types:

type DuckgenMessageSources = {
  auth: typeof AuthMessages
  payments: typeof PaymentMessages
}
type DuckgenMessageSources = {
  auth: typeof AuthMessages
  payments: typeof PaymentMessages
}

DuckgenMessageGroup

A union of all group keys:

type DuckgenMessageGroup = 'auth' | 'payments'
type DuckgenMessageGroup = 'auth' | 'payments'

DuckgenMessageKey

Message keys for a group, or across 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' | ...
// 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 a group's message keys to string translations:

type AuthDictionary = DuckgenMessageDictionary<'auth'>
// => { AUTH_SIGNIN_SUCCESS: string; AUTH_SIGNIN_FAILED: string; ... }
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; ... } }
type AllDicts = DuckgenMessageDictionaryByGroup
// => { auth: { AUTH_SIGNIN_SUCCESS: string; ... }; payments: { PAYMENT_SUCCESS: string; ... } }

DuckGenI18nMessages

Alias for DuckgenMessageDictionaryByGroup. Pick whichever name reads better.

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; ... } }
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: { ... } } }
type FullI18n = DuckgenI18nByGroup<'en' | 'ar'>
// => { en: { auth: { ... }; payments: { ... } }; ar: { auth: { ... }; payments: { ... } } }

DuckgenScopedI18nByGroup

Wraps all groups under a server scope. Use this when the app mixes server-generated and client-only messages:

type ScopedI18n = DuckgenScopedI18nByGroup<'en' | 'ar', DuckGenI18nMessages>
// => { en: { server: { AuthMessages: { ... }; PaymentMessages: { ... } } }; ar: { ... } }
type ScopedI18n = DuckgenScopedI18nByGroup<'en' | 'ar', DuckGenI18nMessages>
// => { en: { server: { AuthMessages: { ... }; PaymentMessages: { ... } } }; ar: { ... } }

Real-world i18n example

A type-safe i18n system built on generated message types:

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
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
bunx duck-gen

Step 3: Build the 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: 'كلمة المرور غير صحيحة',
      },
    },
  },
}
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 errors when you:

  • Misspell a message key.
  • Miss a translation for a key.
  • Add a key that isn't in the server constants.

Step 4: Use in the 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)
}
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

Define as many groups as you need. Each module gets its own:

src/modules/auth/auth.constants.ts
/** @duckgen messages auth */
export const AuthMessages = ['AUTH_SIGNIN_SUCCESS', 'AUTH_SIGNIN_FAILED'] as const
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/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
src/modules/payments/payments.constants.ts
/** @duckgen messages payments */
export const PaymentMessages = ['PAYMENT_SUCCESS', 'PAYMENT_DECLINED'] as const

All three groups land in the generated types, accessible individually or together.

Troubleshooting

ProblemSolution
Keys are just stringAdd as const to the array or object.
Group missingThe constant must be exported and have a @duckgen JSDoc tag.
Duplicate group warningTwo constants share the same group key — rename one.
Duplicate const nameTwo files export the same name — rename one.
Spread values missingDuck Gen reads literal values only. Spreading a dynamic expression may lose them. Put as const on the source too.

Next steps