Skip to content

Localization ​

Validation error messages can be localized for different languages.

Issue Registry ​

All issue keys are automatically registered in GGIssueRegistry when created. Use this to generate translation files:

typescript
import {GGIssueRegistry} from "@grest-ts/schema"

// Get all registered issue keys as JSON
const allIssues = GGIssueRegistry.toJSON();
// {
//   string: {
//     type: { $value: { code: "string.type", message: "Value must be a string" } },
//     tooShort: { $value: { code: "string.tooShort", message: "Minimum {min} characters", params: { min: "Minimum length" } } }
//   },
//   number: {
//     type: { $value: { code: "number.type", message: "Value must be a number" } }
//   },
//   ...
// }

// Use this to generate translation files for your i18n system

Static Localizer ​

For simple setups, set a static localizer function on GGIssueKey:

typescript
import {GGIssueKey, ValidationIssueJson} from "@grest-ts/schema"

const translations: Record<string, Record<string, string>> = {
    de: {
        "string.type": "Wert muss eine Zeichenkette sein",
        "number.type": "Wert muss eine Zahl sein",
        "string.tooShort": "Mindestens {min} Zeichen erforderlich"
    },
    fr: {
        "string.type": "La valeur doit etre une chaine",
        "number.type": "La valeur doit etre un nombre"
    }
};

// Set localizer - mutates the issue JSON in place
GGIssueKey.setLocalizer((issue: ValidationIssueJson) => {
    const lang = getCurrentLanguage(); // Your language detection logic
    const translated = translations[lang]?.[issue.code];
    if (translated) {
        // Interpolate params into translated message
        issue.message = translated.replace(/\{(\w+)\}/g, (_, key) =>
            String((issue.params as any)?.[key] ?? `{${key}}`)
        );
        issue.usedLanguage = lang;
    }
});

Async Context Localization with GGLocator ​

For request-scoped localization (different users, different languages), integrate with @grest-ts/locator:

typescript
import {GGIssueKey, ValidationIssueJson} from "@grest-ts/schema"
import {GGLocatorKey} from "@grest-ts/locator"
import {GGIntl} from "@grest-ts/intl"

// Define a locator key for the intl service
const IntlKey = new GGLocatorKey<GGIntl>("GG_INTL");

// Set localizer that reads from async context
GGIssueKey.setLocalizer((issue: ValidationIssueJson) => {
    const intl = IntlKey.tryGet();
    if (intl) {
        const translated = intl.t(`validation.${issue.code}`, issue.params);
        if (translated) {
            issue.message = translated;
            issue.usedLanguage = intl.locale;
        }
    }
});

// In your request handler, bind the intl service to async context
async function handleRequest(req: Request) {
    const userLocale = req.headers["accept-language"] || "en";
    const intl = new GGIntl(userLocale, translations);

    await IntlKey.run(intl, async () => {
        // All validation errors in this context will use userLocale
        const result = IsUser.safeParse(req.body, true);
        if (!result.success) {
            return {errors: result.issues.toJSON()}; // Localized messages
        }
    });
}

ValidationIssueJson Structure ​

Localized issues include language metadata:

typescript
interface ValidationIssueJson {
    path: string;           // Field path (e.g., "user.email")
    code: string;           // Issue code (e.g., "string.tooShort")
    message: string;        // Localized message with params interpolated
    params?: object;        // Original params (e.g., { min: 8 })
    usedLanguage?: string;  // Language actually used for translation
    expectedLanguage?: string; // Language that was requested
}