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:
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): TIdentity 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: stringtext = 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:
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): TIdentity 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");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" });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");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");translate("app.greeting")leaves outname, whichHello {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 undercountor undersmart_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): TIdentity 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): TIdentity 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");translate("app.greeting", { nme: nevernme: "Ann" });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): TIdentity 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", andname: stringgives`Hello ${string}` | `Bonjour ${string}`. Params keep their literal types, so a call needs noas const. - A plural returns every form of every locale at once, whatever
countis. Which form a count picks depends on the locale'sIntl.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, sotranslate(isPost ? "resources.posts.name" : "resources.comments.name")keepsstringfor the member no catalog holds. - A catalog imported from JSON returns
string. TypeScript types a JSON import's values asstring, 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): TIdentity 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");
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) => TIdentity 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): TIdentity 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>()({ "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) => TIdentity 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): TIdentity 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",});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.
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",};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 by | Where it lands |
|---|---|
AuthProvider<TPermissions> | getPermissions resolves it |
usePermissions() | permissions on UsePermissionsResult |
<Admin permissions> | the prop, in React |
<Admin> function child | AdminChildren, resolved by useResolvedChildren and useAdminChildren |
<AdminUI permissions> | AdminUIProps, in React |
supabaseAuthProvider | SupabaseAuthProvider, 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:
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.usePermissions } from "@chelone/react";
declare module "@chelone/core" {
interface Register {
Register.permissions: ("admin" | "editor")[]permissions: ("admin" | "editor")[];
}
}
const { const permissions: ("admin" | "editor")[] | undefinedThe current user's permissions (`undefined` until resolved / unconfigured).permissions, const isPending: booleanWhether 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.usePermissions();
const const isAdmin: boolean | undefinedisAdmin = !const isPending: booleanWhether the permissions query is in flight (always `false` when unconfigured).isPending && const permissions: ("admin" | "editor")[] | undefinedThe current user's permissions (`undefined` until resolved / unconfigured).permissions?.Array<"admin" | "editor">.includes(searchElement: "admin" | "editor", fromIndex?: number): booleanDetermines whether an array includes a certain element, returning true or false as appropriate.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().permissionsis aComputedRef<RegisteredPermissions | undefined>, besideComputedRefs forisPendinganderror, so a template reacts to it.<Admin>has nopermissionsprop and no function child. React's<Admin permissions>and its(permissions) => nodechild have no Vue counterpart, so a Vue app reads permissions throughusePermissions()where it needs them.