Translations and permissions

What Register.messages and Register.permissions do to translate, useTranslate and usePermissions.

Register types more than resources. Its messages key types every translate call in the app, and its permissions key types what an auth provider hands back. Registering your app is where you write the two keys and what each one falls back to when you leave it off. This page is what they do to a call.

Typed keys and params

A catalog is a MessageCatalog: keys mapped to a MessageValue, which is a string or a PluralMessage (the { one, other } form keyed by Intl.PluralRules categories, other required). MessageKey<TCatalog> is a catalog's own keys, ExtractParams<T> reads the {placeholders} out of a template string, and TranslateParams<MessageValue> turns them into the params a call must pass. TranslateParamValues is the loose Record<string, string | number> an untyped call takes.

defineMessages pins a catalog's literal keys and literal text, and createI18n over it returns CheloneI18n<TCatalog>. On that instance translate takes only the keys the catalog holds:

src/i18n.ts
import { 
function createI18n<const TCatalog extends MessageCatalog>(options: {
    locale: string;
    messages: Record<string, TCatalog>;
    fallbackLocale?: string;
    onMissingKey?: (key: string, locale: string, fallback: string | undefined) => void;
}): CheloneI18n<TCatalog>
createI18n
, function defineMessages<const T extends MessageCatalog>(catalog: T): T
Identity helper pinning literal keys AND literal template strings (for param inference).
defineMessages
} from "@chelone/core";
const
const en: {
    readonly "app.greeting": "Hello {name}";
    readonly "app.save": "Save";
    readonly "app.selected": {
        readonly one: "{count} item selected";
        readonly other: "{count} items selected";
    };
}
en
=
defineMessages<{
    readonly "app.greeting": "Hello {name}";
    readonly "app.save": "Save";
    readonly "app.selected": {
        readonly one: "{count} item selected";
        readonly other: "{count} items selected";
    };
}>(catalog: {
    readonly "app.greeting": "Hello {name}";
    readonly "app.save": "Save";
    readonly "app.selected": {
        readonly one: "{count} item selected";
        readonly other: "{count} items selected";
    };
}): {
    readonly "app.greeting": "Hello {name}";
    readonly "app.save": "Save";
    readonly "app.selected": {
        readonly one: "{count} item selected";
        readonly other: "{count} items selected";
    };
}
Identity helper pinning literal keys AND literal template strings (for param inference).
defineMessages
({
"app.greeting": "Hello {name}", "app.save": "Save", "app.selected": { one: "{count} item selected"one: "{count} item selected", other: "{count} items selected"other: "{count} items selected", }, }); const
const i18n: CheloneI18n<{
    readonly "app.greeting": "Hello {name}";
    readonly "app.save": "Save";
    readonly "app.selected": {
        readonly one: "{count} item selected";
        readonly other: "{count} items selected";
    };
}>
i18n
=
createI18n<{
    readonly "app.greeting": "Hello {name}";
    readonly "app.save": "Save";
    readonly "app.selected": {
        readonly one: "{count} item selected";
        readonly other: "{count} items selected";
    };
}>(options: {
    locale: string;
    messages: Record<string, {
        readonly "app.greeting": "Hello {name}";
        readonly "app.save": "Save";
        readonly "app.selected": {
            readonly one: "{count} item selected";
            readonly other: "{count} items selected";
        };
    }>;
    fallbackLocale?: string;
    onMissingKey?: (key: string, locale: string, fallback: string | undefined) => void;
}): CheloneI18n<...>
createI18n
({ locale: stringlocale: "en",
messages: Record<string, {
    readonly "app.greeting": "Hello {name}";
    readonly "app.save": "Save";
    readonly "app.selected": {
        readonly one: "{count} item selected";
        readonly other: "{count} items selected";
    };
}>
messages
: {
en: {
    readonly "app.greeting": "Hello {name}";
    readonly "app.save": "Save";
    readonly "app.selected": {
        readonly one: "{count} item selected";
        readonly other: "{count} items selected";
    };
}
en
} });
const
const text: string
text
=
const i18n: CheloneI18n<{
    readonly "app.greeting": "Hello {name}";
    readonly "app.save": "Save";
    readonly "app.selected": {
        readonly one: "{count} item selected";
        readonly other: "{count} items selected";
    };
}>
i18n
.
CheloneI18n<{ readonly "app.greeting": "Hello {name}"; readonly "app.save": "Save"; readonly "app.selected": { readonly one: "{count} item selected"; readonly other: "{count} items selected"; }; }>.translate<"app.greeting">(key: "app.greeting", params: {
    name: string | number;
}): string
Params tuple, three-way: no placeholders → no params; type-erased or heterogeneous catalogs (where `undefined` joins the union, e.g. `CheloneI18n<MessageCatalog>`) → optional params; placeholders known → required params.
translate
("app.greeting", { name: string | numbername: "Ann" });

Hover text and it is string: a typed catalog buys you the call, not the return. These four fail:

src/i18n.ts
import { 
function createI18n<const TCatalog extends MessageCatalog>(options: {
    locale: string;
    messages: Record<string, TCatalog>;
    fallbackLocale?: string;
    onMissingKey?: (key: string, locale: string, fallback: string | undefined) => void;
}): CheloneI18n<TCatalog>
createI18n
, function defineMessages<const T extends MessageCatalog>(catalog: T): T
Identity helper pinning literal keys AND literal template strings (for param inference).
defineMessages
} from "@chelone/core";
const
const en: {
    readonly "app.greeting": "Hello {name}";
    readonly "app.save": "Save";
    readonly "app.selected": {
        readonly one: "{count} item selected";
        readonly other: "{count} items selected";
    };
}
en
=
defineMessages<{
    readonly "app.greeting": "Hello {name}";
    readonly "app.save": "Save";
    readonly "app.selected": {
        readonly one: "{count} item selected";
        readonly other: "{count} items selected";
    };
}>(catalog: {
    readonly "app.greeting": "Hello {name}";
    readonly "app.save": "Save";
    readonly "app.selected": {
        readonly one: "{count} item selected";
        readonly other: "{count} items selected";
    };
}): {
    readonly "app.greeting": "Hello {name}";
    readonly "app.save": "Save";
    readonly "app.selected": {
        readonly one: "{count} item selected";
        readonly other: "{count} items selected";
    };
}
Identity helper pinning literal keys AND literal template strings (for param inference).
defineMessages
({
"app.greeting": "Hello {name}", "app.save": "Save", "app.selected": { one: "{count} item selected"one: "{count} item selected", other: "{count} items selected"other: "{count} items selected", }, }); const
const i18n: CheloneI18n<{
    readonly "app.greeting": "Hello {name}";
    readonly "app.save": "Save";
    readonly "app.selected": {
        readonly one: "{count} item selected";
        readonly other: "{count} items selected";
    };
}>
i18n
=
createI18n<{
    readonly "app.greeting": "Hello {name}";
    readonly "app.save": "Save";
    readonly "app.selected": {
        readonly one: "{count} item selected";
        readonly other: "{count} items selected";
    };
}>(options: {
    locale: string;
    messages: Record<string, {
        readonly "app.greeting": "Hello {name}";
        readonly "app.save": "Save";
        readonly "app.selected": {
            readonly one: "{count} item selected";
            readonly other: "{count} items selected";
        };
    }>;
    fallbackLocale?: string;
    onMissingKey?: (key: string, locale: string, fallback: string | undefined) => void;
}): CheloneI18n<...>
createI18n
({ locale: stringlocale: "en",
messages: Record<string, {
    readonly "app.greeting": "Hello {name}";
    readonly "app.save": "Save";
    readonly "app.selected": {
        readonly one: "{count} item selected";
        readonly other: "{count} items selected";
    };
}>
messages
: {
en: {
    readonly "app.greeting": "Hello {name}";
    readonly "app.save": "Save";
    readonly "app.selected": {
        readonly one: "{count} item selected";
        readonly other: "{count} items selected";
    };
}
en
} });
const i18n: CheloneI18n<{
    readonly "app.greeting": "Hello {name}";
    readonly "app.save": "Save";
    readonly "app.selected": {
        readonly one: "{count} item selected";
        readonly other: "{count} items selected";
    };
}>
i18n
.translate("app.greeting");
Expected 2 arguments, but got 1.
const i18n: CheloneI18n<{
    readonly "app.greeting": "Hello {name}";
    readonly "app.save": "Save";
    readonly "app.selected": {
        readonly one: "{count} item selected";
        readonly other: "{count} items selected";
    };
}>
i18n
.
CheloneI18n<{ readonly "app.greeting": "Hello {name}"; readonly "app.save": "Save"; readonly "app.selected": { readonly one: "{count} item selected"; readonly other: "{count} items selected"; }; }>.translate<"app.greeting">(key: "app.greeting", params: {
    name: string | number;
}): string
Params tuple, three-way: no placeholders → no params; type-erased or heterogeneous catalogs (where `undefined` joins the union, e.g. `CheloneI18n<MessageCatalog>`) → optional params; placeholders known → required params.
translate
("app.greeting", { nme: "Ann" });
Object literal may only specify known properties, but 'nme' does not exist in type '{ name: string | number; }'. Did you mean to write 'name'?
const i18n: CheloneI18n<{
    readonly "app.greeting": "Hello {name}";
    readonly "app.save": "Save";
    readonly "app.selected": {
        readonly one: "{count} item selected";
        readonly other: "{count} items selected";
    };
}>
i18n
.
CheloneI18n<{ readonly "app.greeting": "Hello {name}"; readonly "app.save": "Save"; readonly "app.selected": { readonly one: "{count} item selected"; readonly other: "{count} items selected"; }; }>.translate<MessageKey<{
    readonly "app.greeting": "Hello {name}";
    readonly "app.save": "Save";
    readonly "app.selected": {
        readonly one: "{count} item selected";
        readonly other: "{count} items selected";
    };
}>>(key: MessageKey<{
    readonly "app.greeting": "Hello {name}";
    readonly "app.save": "Save";
    readonly "app.selected": {
        readonly one: "{count} item selected";
        readonly other: "{count} items selected";
    };
}>, params?: {
    name: string | number;
} | (OtherPlaceholders<"{count} items selected"> & ({
    count: number;
} | {
    smart_count: number;
})) | undefined): string
Params tuple, three-way: no placeholders → no params; type-erased or heterogeneous catalogs (where `undefined` joins the union, e.g. `CheloneI18n<MessageCatalog>`) → optional params; placeholders known → required params.
translate
("app.helo");
Argument of type '"app.helo"' is not assignable to parameter of type 'MessageKey<{ readonly "app.greeting": "Hello {name}"; readonly "app.save": "Save"; readonly "app.selected": { readonly one: "{count} item selected"; readonly other: "{count} items selected"; }; }>'.
const i18n: CheloneI18n<{
    readonly "app.greeting": "Hello {name}";
    readonly "app.save": "Save";
    readonly "app.selected": {
        readonly one: "{count} item selected";
        readonly other: "{count} items selected";
    };
}>
i18n
.translate("app.selected");
Expected 2 arguments, but got 1.
  • translate("app.greeting") leaves out name, which Hello {name} needs.
  • { nme: "Ann" } is a misspelt placeholder, not a free-form param.
  • "app.helo" is a key no catalog holds.
  • translate("app.selected") leaves out the count a plural needs. A plural takes a number under count or under smart_count, and whichever name carries it also fills {count} and {smart_count} in the text.

_ is the default text for a key no catalog holds. CheloneI18n types a call's params as exactly the placeholders its entry names, so _ joins them on the registered translate below, where a call may name a key the catalog lacks. Translation pins what a provider owes all of this at runtime.

Registering the catalog

Register.messages gives two types. RegisteredMessages is the registered catalog, and RegisteredTranslate is its translate. useTranslate() returns RegisteredTranslate in both adapters, so one augmentation types every call in the app. The block itself is in Registering your app, and the hooks that read the registry are in Typed hooks.

import { function defineMessages<const T extends MessageCatalog>(catalog: T): T
Identity helper pinning literal keys AND literal template strings (for param inference).
defineMessages
, type
type RegisteredTranslate = {
    <K extends "app.greeting" | (string & {}), const TParams extends ParamsFor<{
        readonly "app.greeting": "Hello {name}";
    }, K, TParams>>(key: K, params: TParams): string;
    <K extends "app.greeting" | (string & {})>(key: K, ...params: K extends "app.greeting" ? undefined extends CallParams<K extends "app.greeting" ? {
        readonly "app.greeting": "Hello {name}";
    }[K] : never> ? [params?: Exclude<CallParams<K extends "app.greeting" ? {
        readonly "app.greeting": "Hello {name}";
    }[K] : never>, CallParams<K extends "app.greeting" ? {
        readonly "app.greeting": "Hello {name}";
    }[K] : never> & undefined> | undefined] : [params: ...] : [params?: ...]): string;
}
`translate` typed against the app's registered catalog (see {@link RegisteredMessages } ).
RegisteredTranslate
} from "@chelone/core";
const
const en: {
    readonly "app.greeting": "Hello {name}";
}
en
=
defineMessages<{
    readonly "app.greeting": "Hello {name}";
}>(catalog: {
    readonly "app.greeting": "Hello {name}";
}): {
    readonly "app.greeting": "Hello {name}";
}
Identity helper pinning literal keys AND literal template strings (for param inference).
defineMessages
({ "app.greeting": "Hello {name}" });
declare module "@chelone/core" { interface Register {
Register.messages: {
    readonly "app.greeting": "Hello {name}";
}
messages
: typeof
const en: {
    readonly "app.greeting": "Hello {name}";
}
en
;
} } type
type Translate = {
    <K extends "app.greeting" | (string & {}), const TParams extends ParamsFor<{
        readonly "app.greeting": "Hello {name}";
    }, K, TParams>>(key: K, params: TParams): string;
    <K extends "app.greeting" | (string & {})>(key: K, ...params: K extends "app.greeting" ? undefined extends CallParams<K extends "app.greeting" ? {
        readonly "app.greeting": "Hello {name}";
    }[K] : never> ? [params?: Exclude<CallParams<K extends "app.greeting" ? {
        readonly "app.greeting": "Hello {name}";
    }[K] : never>, CallParams<K extends "app.greeting" ? {
        readonly "app.greeting": "Hello {name}";
    }[K] : never> & undefined> | undefined] : [params: ...] : [params?: ...]): string;
}
Translate
=
type RegisteredTranslate = {
    <K extends "app.greeting" | (string & {}), const TParams extends ParamsFor<{
        readonly "app.greeting": "Hello {name}";
    }, K, TParams>>(key: K, params: TParams): string;
    <K extends "app.greeting" | (string & {})>(key: K, ...params: K extends "app.greeting" ? undefined extends CallParams<K extends "app.greeting" ? {
        readonly "app.greeting": "Hello {name}";
    }[K] : never> ? [params?: Exclude<CallParams<K extends "app.greeting" ? {
        readonly "app.greeting": "Hello {name}";
    }[K] : never>, CallParams<K extends "app.greeting" ? {
        readonly "app.greeting": "Hello {name}";
    }[K] : never> & undefined> | undefined] : [params: ...] : [params?: ...]): string;
}
`translate` typed against the app's registered catalog (see {@link RegisteredMessages } ).
RegisteredTranslate
;

RegisteredTranslate is lenient where CheloneI18n is strict, and that is deliberate. The components label themselves with keys such as resources.posts.name beside a _ default, and the registry copies those calls into apps whose catalog holds only some of the chelone.* keys. A hook that rejected a key the registered catalog lacks would break every app that mounts a partial catalog, so any string stays a key and only the params a known entry names are checked:

import { function defineMessages<const T extends MessageCatalog>(catalog: T): T
Identity helper pinning literal keys AND literal template strings (for param inference).
defineMessages
, type
type RegisteredTranslate = {
    <K extends "app.greeting" | (string & {}), const TParams extends ParamsFor<{
        readonly "app.greeting": "Hello {name}";
    }, K, TParams>>(key: K, params: TParams): string;
    <K extends "app.greeting" | (string & {})>(key: K, ...params: K extends "app.greeting" ? undefined extends CallParams<K extends "app.greeting" ? {
        readonly "app.greeting": "Hello {name}";
    }[K] : never> ? [params?: Exclude<CallParams<K extends "app.greeting" ? {
        readonly "app.greeting": "Hello {name}";
    }[K] : never>, CallParams<K extends "app.greeting" ? {
        readonly "app.greeting": "Hello {name}";
    }[K] : never> & undefined> | undefined] : [params: ...] : [params?: ...]): string;
}
`translate` typed against the app's registered catalog (see {@link RegisteredMessages } ).
RegisteredTranslate
} from "@chelone/core";
const
const en: {
    readonly "app.greeting": "Hello {name}";
}
en
=
defineMessages<{
    readonly "app.greeting": "Hello {name}";
}>(catalog: {
    readonly "app.greeting": "Hello {name}";
}): {
    readonly "app.greeting": "Hello {name}";
}
Identity helper pinning literal keys AND literal template strings (for param inference).
defineMessages
({ "app.greeting": "Hello {name}" });
declare module "@chelone/core" { interface Register {
Register.messages: {
    readonly "app.greeting": "Hello {name}";
}
messages
: typeof
const en: {
    readonly "app.greeting": "Hello {name}";
}
en
;
} } declare const const translate: RegisteredTranslatetranslate:
type RegisteredTranslate = {
    <K extends "app.greeting" | (string & {}), const TParams extends ParamsFor<{
        readonly "app.greeting": "Hello {name}";
    }, K, TParams>>(key: K, params: TParams): string;
    <K extends "app.greeting" | (string & {})>(key: K, ...params: K extends "app.greeting" ? undefined extends CallParams<K extends "app.greeting" ? {
        readonly "app.greeting": "Hello {name}";
    }[K] : never> ? [params?: Exclude<CallParams<K extends "app.greeting" ? {
        readonly "app.greeting": "Hello {name}";
    }[K] : never>, CallParams<K extends "app.greeting" ? {
        readonly "app.greeting": "Hello {name}";
    }[K] : never> & undefined> | undefined] : [params: ...] : [params?: ...]): string;
}
`translate` typed against the app's registered catalog (see {@link RegisteredMessages } ).
RegisteredTranslate
;
translate("app.greeting");
Expected 2 arguments, but got 1.
translate("app.greeting", { nme: nevernme: "Ann" });
No overload matches this call. Overload 1 of 2, '(key: "app.greeting", params: ParamsFor<{ readonly "app.greeting": "Hello {name}"; }, "app.greeting", { readonly nme: "Ann"; }>): string', gave the following error. Type 'string' is not assignable to type 'never'. Overload 2 of 2, '(key: "app.greeting", params: never): string', gave the following error. Argument of type '{ nme: string; }' is not assignable to parameter of type 'never'.
const translate: <"app.helo", {
    readonly _: "Hello {name}";
    readonly name: "Ann";
}>(key: "app.helo", params: {
    readonly _: "Hello {name}";
    readonly name: "Ann";
}) => string (+1 overload)
translate
("app.helo", { _: "Hello {name}"_: "Hello {name}", name: "Ann"name: "Ann" });

The third call compiles. Its key is one the catalog lacks, so the provider answers it with the _ default, and a key the catalog does not name takes any TranslateParamValues.

TranslateFor<TCatalog> is the same type over any catalog you name, with nothing registered. asRegisteredTranslate is the one place that bridges the erased instance: <Admin i18n> holds an AnyCheloneI18n, whose translate takes any key, and reading it as RegisteredTranslate narrows what a call may pass, which is how neither adapter writes a cast.

Register the catalog you actually mount. The typing is a compile-time claim about the instance <Admin> holds, and a key that instance lacks still comes back as the key or its _ default at runtime.

Literal text

translate returns string. An app that needs the text itself, for a lookup table or a discriminated check, can ask for it instead. literalMessages: true beside messages on Register flips the return, HasLiteralMessages reads the flag off a register shape, and RegisteredLiteralMessages is the flag as core reads it for your app.

Over two catalogs with nothing registered:

import { function defineMessages<const T extends MessageCatalog>(catalog: T): T
Identity helper pinning literal keys AND literal template strings (for param inference).
defineMessages
, type
type TranslateFor<TCatalog extends MessageCatalog, TRegister extends Register = Register> = {
    <K extends TranslatableKey<TCatalog>, const TParams extends ParamsFor<TCatalog, K, TParams>>(key: K, params: TParams): TranslateText<TCatalog, K, TParams, TRegister>;
    <K extends TranslatableKey<TCatalog>>(key: K, ...params: NoParamsFor<TCatalog, K>): TranslateText<...>;
}
`translate` typed against a catalog: a key the catalog holds autocompletes and must be passed its placeholders; any other key still compiles with {@link TranslateParamValues } . Two signatures, because the params argument has to be a naked type parameter for a literal `{ name: "Ann" }` to survive inference, and a naked parameter cannot be required for one key and optional for another. The first takes the params and keeps their literal types; the second takes none, and names `never` for a key whose entry interpolates something.
TranslateFor
} from "@chelone/core";
const
const en: {
    readonly "app.save": "Save";
    readonly "app.greeting": "Hello {name}";
}
en
=
defineMessages<{
    readonly "app.save": "Save";
    readonly "app.greeting": "Hello {name}";
}>(catalog: {
    readonly "app.save": "Save";
    readonly "app.greeting": "Hello {name}";
}): {
    readonly "app.save": "Save";
    readonly "app.greeting": "Hello {name}";
}
Identity helper pinning literal keys AND literal template strings (for param inference).
defineMessages
({
"app.save": "Save", "app.greeting": "Hello {name}", }); const
const fr: {
    readonly "app.save": "Enregistrer";
    readonly "app.greeting": "Bonjour {name}";
}
fr
=
defineMessages<{
    readonly "app.save": "Enregistrer";
    readonly "app.greeting": "Bonjour {name}";
}>(catalog: {
    readonly "app.save": "Enregistrer";
    readonly "app.greeting": "Bonjour {name}";
}): {
    readonly "app.save": "Enregistrer";
    readonly "app.greeting": "Bonjour {name}";
}
Identity helper pinning literal keys AND literal template strings (for param inference).
defineMessages
({
"app.save": "Enregistrer", "app.greeting": "Bonjour {name}", }); declare const
const translate: TranslateFor<{
    readonly "app.save": "Save";
    readonly "app.greeting": "Hello {name}";
} | {
    readonly "app.save": "Enregistrer";
    readonly "app.greeting": "Bonjour {name}";
}, {
    literalMessages: true;
}>
translate
:
type TranslateFor<TCatalog extends MessageCatalog, TRegister extends Register = Register> = {
    <K extends TranslatableKey<TCatalog>, const TParams extends ParamsFor<TCatalog, K, TParams>>(key: K, params: TParams): TranslateText<TCatalog, K, TParams, TRegister>;
    <K extends TranslatableKey<TCatalog>>(key: K, ...params: NoParamsFor<TCatalog, K>): TranslateText<...>;
}
`translate` typed against a catalog: a key the catalog holds autocompletes and must be passed its placeholders; any other key still compiles with {@link TranslateParamValues } . Two signatures, because the params argument has to be a naked type parameter for a literal `{ name: "Ann" }` to survive inference, and a naked parameter cannot be required for one key and optional for another. The first takes the params and keeps their literal types; the second takes none, and names `never` for a key whose entry interpolates something.
TranslateFor
<
typeof
const en: {
    readonly "app.save": "Save";
    readonly "app.greeting": "Hello {name}";
}
en
| typeof
const fr: {
    readonly "app.save": "Enregistrer";
    readonly "app.greeting": "Bonjour {name}";
}
fr
,
{ literalMessages: trueliteralMessages: true } >; const
const save: "Save" | "Enregistrer"
save
=
const translate: <"app.save">(key: "app.save", params?: {
    _?: string;
} | undefined) => "Save" | "Enregistrer" (+1 overload)
translate
("app.save");
const
const greeting: "Hello Ann" | "Bonjour Ann"
greeting
=
const translate: <"app.greeting", {
    readonly name: "Ann";
}>(key: "app.greeting", params: {
    readonly name: "Ann";
}) => "Hello Ann" | "Bonjour Ann" (+1 overload)
translate
("app.greeting", { name: "Ann"name: "Ann" });
  • A {placeholder} is interpolated in the type from the param you pass. name: "Ann" gives "Hello Ann" | "Bonjour Ann", and name: string gives `Hello ${string}` | `Bonjour ${string}`. Params keep their literal types, so a call needs no as const.
  • A plural returns every form of every locale at once, whatever count is. Which form a count picks depends on the locale's Intl.PluralRules, so the type cannot name one.
  • A key no catalog holds returns string. Each member of a key union is asked on its own, so translate(isPost ? "resources.posts.name" : "resources.comments.name") keeps string for the member no catalog holds.
  • A catalog imported from JSON returns string. TypeScript types a JSON import's values as string, so no literal reaches the type.

The flag is off by default because a literal union is not a string you can reassign:

import { function defineMessages<const T extends MessageCatalog>(catalog: T): T
Identity helper pinning literal keys AND literal template strings (for param inference).
defineMessages
, type
type TranslateFor<TCatalog extends MessageCatalog, TRegister extends Register = Register> = {
    <K extends TranslatableKey<TCatalog>, const TParams extends ParamsFor<TCatalog, K, TParams>>(key: K, params: TParams): TranslateText<TCatalog, K, TParams, TRegister>;
    <K extends TranslatableKey<TCatalog>>(key: K, ...params: NoParamsFor<TCatalog, K>): TranslateText<...>;
}
`translate` typed against a catalog: a key the catalog holds autocompletes and must be passed its placeholders; any other key still compiles with {@link TranslateParamValues } . Two signatures, because the params argument has to be a naked type parameter for a literal `{ name: "Ann" }` to survive inference, and a naked parameter cannot be required for one key and optional for another. The first takes the params and keeps their literal types; the second takes none, and names `never` for a key whose entry interpolates something.
TranslateFor
} from "@chelone/core";
const
const en: {
    readonly "app.save": "Save";
    readonly "app.cancel": "Cancel";
    readonly "app.selected": {
        readonly one: "{count} item selected";
        readonly other: "{count} items selected";
    };
}
en
=
defineMessages<{
    readonly "app.save": "Save";
    readonly "app.cancel": "Cancel";
    readonly "app.selected": {
        readonly one: "{count} item selected";
        readonly other: "{count} items selected";
    };
}>(catalog: {
    readonly "app.save": "Save";
    readonly "app.cancel": "Cancel";
    readonly "app.selected": {
        readonly one: "{count} item selected";
        readonly other: "{count} items selected";
    };
}): {
    readonly "app.save": "Save";
    readonly "app.cancel": "Cancel";
    readonly "app.selected": {
        readonly one: "{count} item selected";
        readonly other: "{count} items selected";
    };
}
Identity helper pinning literal keys AND literal template strings (for param inference).
defineMessages
({
"app.save": "Save", "app.cancel": "Cancel", "app.selected": { one: "{count} item selected"one: "{count} item selected", other: "{count} items selected"other: "{count} items selected", }, }); declare const
const translate: TranslateFor<{
    readonly "app.save": "Save";
    readonly "app.cancel": "Cancel";
    readonly "app.selected": {
        readonly one: "{count} item selected";
        readonly other: "{count} items selected";
    };
}, {
    literalMessages: true;
}>
translate
:
type TranslateFor<TCatalog extends MessageCatalog, TRegister extends Register = Register> = {
    <K extends TranslatableKey<TCatalog>, const TParams extends ParamsFor<TCatalog, K, TParams>>(key: K, params: TParams): TranslateText<TCatalog, K, TParams, TRegister>;
    <K extends TranslatableKey<TCatalog>>(key: K, ...params: NoParamsFor<TCatalog, K>): TranslateText<...>;
}
`translate` typed against a catalog: a key the catalog holds autocompletes and must be passed its placeholders; any other key still compiles with {@link TranslateParamValues } . Two signatures, because the params argument has to be a naked type parameter for a literal `{ name: "Ann" }` to survive inference, and a naked parameter cannot be required for one key and optional for another. The first takes the params and keeps their literal types; the second takes none, and names `never` for a key whose entry interpolates something.
TranslateFor
<typeof
const en: {
    readonly "app.save": "Save";
    readonly "app.cancel": "Cancel";
    readonly "app.selected": {
        readonly one: "{count} item selected";
        readonly other: "{count} items selected";
    };
}
en
, { literalMessages: trueliteralMessages: true }>;
let let label: "Save"label =
const translate: <"app.save">(key: "app.save", params?: {
    _?: string;
} | undefined) => "Save" (+1 overload)
translate
("app.save");
label =
const translate: <"app.cancel">(key: "app.cancel", params?: {
    _?: string;
} | undefined) => "Cancel" (+1 overload)
translate
("app.cancel");
Type '"Cancel"' is not assignable to type '"Save"'.
const
const many: "3 item selected" | "3 items selected"
many
=
const translate: <"app.selected", {
    readonly count: 3;
}>(key: "app.selected", params: {
    readonly count: 3;
}) => "3 item selected" | "3 items selected" (+1 overload)
translate
("app.selected", { count: 3count: 3 });

Checking a locale

defineLocale<TBase>() checks a locale catalog against a base catalog, usually englishMessages, and keeps the catalog's own literal type. LocaleMessages<TBase> is the checked shape, if you want to name it. A key English has and the locale missed fails:

import { function defineLocale<TBase extends MessageCatalog>(): <const T extends LocaleMessages<TBase> & Record<Exclude<keyof T, MessageKey<TBase>> | PlaceholderGaps<TBase, T>, never>>(catalog: T) => T
Identity helper that checks a locale catalog against a base catalog — usually `englishMessages` — and keeps the catalog's own literal type, so `defineLocale<typeof en>()(fr)` is `typeof fr` and not a widened map. It checks types only: a missing key, an extra key, a string where English has a plural (or the reverse), and a string or plural `other` that drops a placeholder English has all fail typecheck. Placeholders may come in any order, and a plural may leave out a category English fills. ```ts const fr = defineLocale<typeof englishMessages>()({ "chelone.page.create": "Créer {name}", // …every other English key }); ```
defineLocale
, function defineMessages<const T extends MessageCatalog>(catalog: T): T
Identity helper pinning literal keys AND literal template strings (for param inference).
defineMessages
} from "@chelone/core";
const
const en: {
    readonly "app.save": "Save";
    readonly "app.greeting": "Hello {name}";
    readonly "app.delete": "Delete";
}
en
=
defineMessages<{
    readonly "app.save": "Save";
    readonly "app.greeting": "Hello {name}";
    readonly "app.delete": "Delete";
}>(catalog: {
    readonly "app.save": "Save";
    readonly "app.greeting": "Hello {name}";
    readonly "app.delete": "Delete";
}): {
    readonly "app.save": "Save";
    readonly "app.greeting": "Hello {name}";
    readonly "app.delete": "Delete";
}
Identity helper pinning literal keys AND literal template strings (for param inference).
defineMessages
({
"app.save": "Save", "app.greeting": "Hello {name}", "app.delete": "Delete", }); const
const fr: LocaleMessages<{
    readonly "app.save": "Save";
    readonly "app.greeting": "Hello {name}";
    readonly "app.delete": "Delete";
}> & Record<never, never>
fr
=
defineLocale<{
    readonly "app.save": "Save";
    readonly "app.greeting": "Hello {name}";
    readonly "app.delete": "Delete";
}>(): <const T extends LocaleMessages<{
    readonly "app.save": "Save";
    readonly "app.greeting": "Hello {name}";
    readonly "app.delete": "Delete";
}> & Record<Exclude<keyof T, MessageKey<{
    readonly "app.save": "Save";
    readonly "app.greeting": "Hello {name}";
    readonly "app.delete": "Delete";
}>> | PlaceholderGaps<{
    readonly "app.save": "Save";
    readonly "app.greeting": "Hello {name}";
    readonly "app.delete": "Delete";
}, T>, never>>(catalog: T) => T
Identity helper that checks a locale catalog against a base catalog — usually `englishMessages` — and keeps the catalog's own literal type, so `defineLocale<typeof en>()(fr)` is `typeof fr` and not a widened map. It checks types only: a missing key, an extra key, a string where English has a plural (or the reverse), and a string or plural `other` that drops a placeholder English has all fail typecheck. Placeholders may come in any order, and a plural may leave out a category English fills. ```ts const fr = defineLocale<typeof englishMessages>()({ "chelone.page.create": "Créer {name}", // …every other English key }); ```
defineLocale
<typeof
const en: {
    readonly "app.save": "Save";
    readonly "app.greeting": "Hello {name}";
    readonly "app.delete": "Delete";
}
en
>()({
Argument of type '{ "app.save": string; "app.greeting": string; }' is not assignable to parameter of type 'LocaleMessages<{ readonly "app.save": "Save"; readonly "app.greeting": "Hello {name}"; readonly "app.delete": "Delete"; }> & Record<never, never>'. Property '"app.delete"' is missing in type '{ "app.save": string; "app.greeting": string; }' but required in type 'LocaleMessages<{ readonly "app.save": "Save"; readonly "app.greeting": "Hello {name}"; readonly "app.delete": "Delete"; }>'.
"app.save": "Enregistrer", "app.greeting": "Bonjour {name}", });

So does a string that drops a placeholder English has:

import { function defineLocale<TBase extends MessageCatalog>(): <const T extends LocaleMessages<TBase> & Record<Exclude<keyof T, MessageKey<TBase>> | PlaceholderGaps<TBase, T>, never>>(catalog: T) => T
Identity helper that checks a locale catalog against a base catalog — usually `englishMessages` — and keeps the catalog's own literal type, so `defineLocale<typeof en>()(fr)` is `typeof fr` and not a widened map. It checks types only: a missing key, an extra key, a string where English has a plural (or the reverse), and a string or plural `other` that drops a placeholder English has all fail typecheck. Placeholders may come in any order, and a plural may leave out a category English fills. ```ts const fr = defineLocale<typeof englishMessages>()({ "chelone.page.create": "Créer {name}", // …every other English key }); ```
defineLocale
, function defineMessages<const T extends MessageCatalog>(catalog: T): T
Identity helper pinning literal keys AND literal template strings (for param inference).
defineMessages
} from "@chelone/core";
const
const en: {
    readonly "app.save": "Save";
    readonly "app.greeting": "Hello {name}";
}
en
=
defineMessages<{
    readonly "app.save": "Save";
    readonly "app.greeting": "Hello {name}";
}>(catalog: {
    readonly "app.save": "Save";
    readonly "app.greeting": "Hello {name}";
}): {
    readonly "app.save": "Save";
    readonly "app.greeting": "Hello {name}";
}
Identity helper pinning literal keys AND literal template strings (for param inference).
defineMessages
({
"app.save": "Save", "app.greeting": "Hello {name}", }); const
const fr: LocaleMessages<{
    readonly "app.save": "Save";
    readonly "app.greeting": "Hello {name}";
}> & Record<"app.greeting", never>
fr
=
defineLocale<{
    readonly "app.save": "Save";
    readonly "app.greeting": "Hello {name}";
}>(): <const T extends LocaleMessages<{
    readonly "app.save": "Save";
    readonly "app.greeting": "Hello {name}";
}> & Record<Exclude<keyof T, MessageKey<{
    readonly "app.save": "Save";
    readonly "app.greeting": "Hello {name}";
}>> | PlaceholderGaps<{
    readonly "app.save": "Save";
    readonly "app.greeting": "Hello {name}";
}, T>, never>>(catalog: T) => T
Identity helper that checks a locale catalog against a base catalog — usually `englishMessages` — and keeps the catalog's own literal type, so `defineLocale<typeof en>()(fr)` is `typeof fr` and not a widened map. It checks types only: a missing key, an extra key, a string where English has a plural (or the reverse), and a string or plural `other` that drops a placeholder English has all fail typecheck. Placeholders may come in any order, and a plural may leave out a category English fills. ```ts const fr = defineLocale<typeof englishMessages>()({ "chelone.page.create": "Créer {name}", // …every other English key }); ```
defineLocale
<typeof
const en: {
    readonly "app.save": "Save";
    readonly "app.greeting": "Hello {name}";
}
en
>()({
"app.save": "Enregistrer", "app.greeting": "Bonjour",
Type 'string' is not assignable to type 'never'.
});

A key the locale invented fails, and so does a string where English has a plural, or the reverse. Placeholders may arrive in any order, and a plural may leave out a category English fills. defineLocale checks types only: <I18nKeyEditor> still lists what a mounted locale missed at runtime, as Translation describes.

Any engine

<Admin i18n> takes AnyCheloneI18n, an alias of I18nProvider, the catalog-erased contract. createI18n is one implementation of it and hands back the catalog-typed CheloneI18n<TCatalog>, which is structurally assignable to the contract. createI18nextProvider from @chelone/utils/i18next returns an I18nProvider as well.

src/i18n.ts
import {
  createI18n,
  englishMessages,
  type I18nProvider,
} from "@chelone/core";
import {
  createI18nextProvider,
  toI18nextResource,
} from "@chelone/utils/i18next";
import i18next from "i18next";
 
const instance = i18next.createInstance();
await instance.init({
  lng: "en",
  resources: { en: { translation: toI18nextResource(englishMessages) } },
});
 
export const fromCore: I18nProvider = createI18n({
  locale: "en",
  messages: { en: englishMessages },
});
 
export const fromI18next: I18nProvider = createI18nextProvider(instance);

Either way the typing of a call comes from Register.messages, never from the engine. The instance <Admin> holds is erased, and useTranslate() reads it back as RegisteredTranslate, so an app that mounts i18next and registers typeof englishMessages checks the same keys and params as one that mounts createI18n.

The runtime contract, what translate owes the components and how a provider passes testI18nProviderContract, stays in Translation.

Permissions

Register.permissions gives RegisteredPermissions, which is unknown when nothing is registered. It is the type getPermissions resolves to and the type usePermissions().permissions carries.

import { type interface AuthProvider<TPermissions = ("admin" | "editor")[]>
The auth contract. All methods return promises; reject = not authenticated / not permitted. `refreshAuth` is the token-refresh hook point (charter §11); the P6 shell builds the no-flash SSR gate on `checkAuth`.
AuthProvider
, type type RegisteredPermissions = ("admin" | "editor")[]
The registered permissions type, or `unknown` when nothing is registered. An app registers it with `interface Register { permissions: string[] }`.
RegisteredPermissions
} from "@chelone/core";
declare module "@chelone/core" { interface Register { Register.permissions: ("admin" | "editor")[]permissions: ("admin" | "editor")[]; } } type
type Permissions = ("admin" | "editor")[]
Permissions
= type RegisteredPermissions = ("admin" | "editor")[]
The registered permissions type, or `unknown` when nothing is registered. An app registers it with `interface Register { permissions: string[] }`.
RegisteredPermissions
;
const const authProvider: AuthProvider<("admin" | "editor")[]>authProvider: interface AuthProvider<TPermissions = ("admin" | "editor")[]>
The auth contract. All methods return promises; reject = not authenticated / not permitted. `refreshAuth` is the token-refresh hook point (charter §11); the P6 shell builds the no-flash SSR gate on `checkAuth`.
AuthProvider
= {
AuthProvider<("admin" | "editor")[]>.login(params: Record<string, unknown>): Promise<void | {
    redirectTo?: string;
}>
login
: async () => {},
AuthProvider<("admin" | "editor")[]>.logout(params?: unknown): Promise<void | string>logout: async () => {}, AuthProvider<("admin" | "editor")[]>.checkAuth(params?: unknown): Promise<void>checkAuth: async () => {}, AuthProvider<("admin" | "editor")[]>.checkError(error: unknown): Promise<void>checkError: async () => {}, getPermissions: async () => "admin",
Type '() => Promise<string>' is not assignable to type '(params?: unknown) => Promise<("admin" | "editor")[] | undefined>'. Type 'Promise<string>' is not assignable to type 'Promise<("admin" | "editor")[] | undefined>'. Type 'string' is not assignable to type '("admin" | "editor")[]'.
};

getPermissions resolves TPermissions | undefined, and AuthProvider's TPermissions defaults to RegisteredPermissions, so a provider that hands back a bare string fails where the app registered a list of names.

What the registered type reaches:

Read byWhere it lands
AuthProvider<TPermissions>getPermissions resolves it
usePermissions()permissions on UsePermissionsResult
<Admin permissions>the prop, in React
<Admin> function childAdminChildren, resolved by useResolvedChildren and useAdminChildren
<AdminUI permissions>AdminUIProps, in React
supabaseAuthProviderSupabaseAuthProvider, and SupabaseAuthProviderOptions.getPermissions

The function child is React's. Its parameter is the registered type, so a (permissions) => node child gates its resources on names your app registered:

import { type type AdminChildren = ReactNode | ((permissions: RegisteredPermissions | undefined) => ReactNode | Promise<ReactNode>)
`<Admin>` children: either a static node OR a `(permissions) => node | Promise<node>` function (parity §0a-9). The function form lets an app derive its resources from the resolved permissions.
AdminChildren
} from "@chelone/react";
declare module "@chelone/core" { interface Register { Register.permissions: ("admin" | "editor")[]permissions: ("admin" | "editor")[]; } } type
type Children = ReactNode | ((permissions: RegisteredPermissions | undefined) => ReactNode | Promise<ReactNode>)
Children
= type AdminChildren = ReactNode | ((permissions: RegisteredPermissions | undefined) => ReactNode | Promise<ReactNode>)
`<Admin>` children: either a static node OR a `(permissions) => node | Promise<node>` function (parity §0a-9). The function form lets an app derive its resources from the resolved permissions.
AdminChildren
;

usePermissions reads it in both adapters:

src/permissions.tsx
import { 
function usePermissions(params?: unknown, options?: {
    staleTime?: number;
    enabled?: boolean;
}): UsePermissionsResult
Fetch the current user's permissions via `authProvider.getPermissions` (R2 parity with ra-core's `usePermissions`). Undefined-tolerant and no-throw: - No `authProvider`, or no `getPermissions` on it → `{ permissions: undefined, isPending: false }` (access control / permissions not configured). - Otherwise wraps {@link permissionsQueryOptions } via react-query. Unlike {@link useCanAccess } / {@link useGetIdentity } (which THROW without a provider), this degrades gracefully so a no-auth app can call it to gate optional features.
@exampleconst { permissions, isPending } = usePermissions(); if (!isPending && permissions?.includes("admin")) return <AdminPanel />;
usePermissions
} from "@chelone/react";
declare module "@chelone/core" { interface Register { Register.permissions: ("admin" | "editor")[]permissions: ("admin" | "editor")[]; } } const {
const permissions: ("admin" | "editor")[] | undefined
The current user's permissions (`undefined` until resolved / unconfigured).
permissions
, const isPending: boolean
Whether the permissions query is in flight (always `false` when unconfigured).
isPending
} =
function usePermissions(params?: unknown, options?: {
    staleTime?: number;
    enabled?: boolean;
}): UsePermissionsResult
Fetch the current user's permissions via `authProvider.getPermissions` (R2 parity with ra-core's `usePermissions`). Undefined-tolerant and no-throw: - No `authProvider`, or no `getPermissions` on it → `{ permissions: undefined, isPending: false }` (access control / permissions not configured). - Otherwise wraps {@link permissionsQueryOptions } via react-query. Unlike {@link useCanAccess } / {@link useGetIdentity } (which THROW without a provider), this degrades gracefully so a no-auth app can call it to gate optional features.
@exampleconst { permissions, isPending } = usePermissions(); if (!isPending && permissions?.includes("admin")) return <AdminPanel />;
usePermissions
();
const const isAdmin: boolean | undefinedisAdmin = !const isPending: boolean
Whether the permissions query is in flight (always `false` when unconfigured).
isPending
&& const permissions: ("admin" | "editor")[] | undefined
The current user's permissions (`undefined` until resolved / unconfigured).
permissions
?.Array<"admin" | "editor">.includes(searchElement: "admin" | "editor", fromIndex?: number): boolean
Determines whether an array includes a certain element, returning true or false as appropriate.
@paramsearchElement The element to search for.@paramfromIndex The position in this array at which to begin searching for searchElement.
includes
("admin");

canAccess and useCanAccess are not keyed by Register. CanAccessParams takes resource: string and action: string, and useCanAccess answers a boolean for that pair. Registering permissions types what getPermissions resolves to, never which actions a resource allows. The runtime side of all of it is in Security.

Vue

useTranslate() returns RegisteredTranslate in Vue exactly as it does in React, and usePermissions() reads the same Register.permissions. Two places differ:

  • usePermissions().permissions is a ComputedRef<RegisteredPermissions | undefined>, beside ComputedRefs for isPending and error, so a template reacts to it.
  • <Admin> has no permissions prop and no function child. React's <Admin permissions> and its (permissions) => node child have no Vue counterpart, so a Vue app reads permissions through usePermissions() where it needs them.