next-translate
next-translate copied to clipboard
Is there a way to have the translationKey strongly typed?
Hi there!
Thanks for a great library :)
Question regarding Typescript: We have a Typescript solution using this library. However, we have not been able to find a way to make the translation-key provided to t() be strongly typed. The type of t seems to be t(string) no matter what we do.
Is it possible to make the input to the t() function strongly typed based on our json files with the localizations? It would help us a lot in avoiding typos.
Thanks! :)
NB. We tried downloading the complex typescript example, but that also seemed to allow any string as input to the t-function.
I solved it using the following custom hook
// util-types.ts
type Join<S1, S2> = S1 extends string
? S2 extends string
? `${S1}.${S2}`
: never
: never;
export type Paths<T> = {
[K in keyof T]: T[K] extends Record<string, unknown>
? Join<K, Paths<T[K]>>
: K;
}[keyof T];
// useTypeSafeTranslation.ts
import useTranslation from "next-translate/useTranslation";
import { TranslationQuery } from "next-translate";
import { Paths } from "../types/util-types";
import common from "../../locales/es-es/common.json";
import home from "../../locales/es-es/home.json";
import catalog from "../../locales/es-es/catalog.json";
import auth from "../../locales/es-es/auth.json";
export type TranslationKeys = {
common: Paths<typeof common>;
home: Paths<typeof home>;
catalog: Paths<typeof catalog>;
auth: Paths<typeof auth>;
};
export const useTypeSafeTranslation = <T extends keyof TranslationKeys>(
ns: T
) => {
const { t, lang } = useTranslation(ns);
return {
t: (
s: TranslationKeys[T],
q?: TranslationQuery,
o?: {
returnObjects?: boolean;
fallback?: string | string[];
default?: string;
}
) => t(s, q, o),
lang,
};
};
And then you just pass the namespace to the hook, and you have a type safe t
function
import React from "react";
import { useTypeSafeTranslation } from "./useTypeSafeTranslation";
interface TestComponentProps {}
export const TestComponent: React.FC<TestComponentProps> = () => {
const { t } = useTypeSafeTranslation("common");
return <>{t("footer.legal.paymentMethods")}</>;
}
;

Adapted from https://github.com/benawad/dogehouse
Here's my simplified version FYI, that just patches t
and adds a minimum amount of code:
import type { I18n, Translate } from "next-translate";
import useTranslation from "next-translate/useTranslation";
import type { TranslationsKeys } from "src/utility/i18n/available-translations";
type Tail<T> = T extends [unknown, ...infer Rest] ? Rest : never;
export interface TypeSafeTranslate<Namespace extends keyof TranslationsKeys>
extends Omit<I18n, "t"> {
t: (
key: TranslationsKeys[Namespace],
...rest: Tail<Parameters<Translate>>
) => string;
}
export function useTypeSafeTranslation<
Namespace extends keyof TranslationsKeys
>(namespace: Namespace): TypeSafeTranslate<Namespace> {
return useTranslation(namespace);
}
And I also just use import type
for all the translations to be extra sure they don't accidentally get bundled (tree shaking has always been a little finnicky for me). I then added a lint rule with ESLint to error whenever users in my codebase try to use useTranslation
directly, which if they really need to, can disable with an ESLint disable comment. Working on an equivalent helper for Trans.
Here's a helper for Trans
:
import UnsafeTrans from "next-translate/Trans";
import type { TransProps as UnsafeTransProps } from "next-translate";
import type { TranslationsKeys } from "src/utility/i18n/available-translations";
export interface TransProps<Namespace extends keyof TranslationsKeys>
extends Omit<UnsafeTransProps, "i18nKey"> {
i18nKey: `${Namespace}:${TranslationsKeys[Namespace]}`;
}
export function Trans<Namespace extends keyof TranslationsKeys>(
props: TransProps<Namespace>
): JSX.Element {
return <UnsafeTrans {...props} />;
}
next update i'll post is to change the Paths
type to support this library's _
prefixed pluralization rules; the nested plurals sound harder to express in TypeScript.
@osdiab what looks like your file?
import type { TranslationsKeys } from "src/utility/i18n/available-translations";
Like this:
import type userProfile from "locales/en/user-profile.json";
import type common from "locales/en/common.json";
type Join<S1, S2> = S1 extends string
? S2 extends string
? `${S1}.${S2}`
: never
: never;
export type Paths<T> = {
[K in keyof T]: T[K] extends Record<string, unknown>
? Join<K, Paths<T[K]>>
: K;
}[keyof T];
export interface TranslationsKeys {
common: Paths<typeof common>;
"user-profile": Paths<typeof userProfile>;
}
It's a little annoying that I have to add this boilerplate to get the types of the locales - but I bet can make a script that does this kind of thing automatically.
another way with this generator: https://www.npmjs.com/package/next-translate-localekeys. Is able to work with the basics for generating locale keys that are available and can be inserted in the useTranslation hook
for those who are interested in a working example I setup my solution for this matter in this library https://github.com/knitkode/koine/blob/main/packages/next/types-i18n.ts
it allows to augment the namespace Koine
with type NextTranslations
whose keys point to the defaultLocale translation files,
PS: I am also wrapping useT
and the other methods tweaking some behaviours, so there is more than type safety in that library
@saschahapp nice work π
Is there any way to support type-safe params too?
@quyctd my package does not currently support that. But if this would be an improvement, I would gladly add it.
@saschahapp Yes, please consider it. Since Next.js usually come with typescript, having type-safe for both keys and params will be awesome π
@osdiab Any news on pluralization?
No, havenβt really focused on that issue as of late.
Omar
On Tue, Oct 11 2022 at 10:57 PM, mleister97 @.***> wrote:
@osdiab https://github.com/osdiab Any news on pluralization?
β Reply to this email directly, view it on GitHub https://github.com/aralroca/next-translate/issues/721#issuecomment-1274730933, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAONU3W37AJHZBTIAHK2EDDWCVW27ANCNFSM5HOCJ7EA . You are receiving this because you were mentioned.Message ID: @.***>
Are there any plans to integrate this feature into the library? i18next does that and it is the only feature that stops me from migrating to next-translate.
Yes, we are going to priorize this. However feel free to PR ππ
@osdiab Any news on pluralization?
You could just clean up the resulting paths using this:
type RemoveSuffix<Key extends string> = Key extends `${infer Prefix}${
| "_zero"
| "_one"
| "_two"
| "_few"
| "_many"
| "_other"}`
? Prefix
: Key;
I solved it using the following custom hook
// util-types.ts type Join<S1, S2> = S1 extends string ? S2 extends string ? `${S1}.${S2}` : never : never; export type Paths<T> = { [K in keyof T]: T[K] extends Record<string, unknown> ? Join<K, Paths<T[K]>> : K; }[keyof T];
// useTypeSafeTranslation.ts import useTranslation from "next-translate/useTranslation"; import { TranslationQuery } from "next-translate"; import { Paths } from "../types/util-types"; import common from "../../locales/es-es/common.json"; import home from "../../locales/es-es/home.json"; import catalog from "../../locales/es-es/catalog.json"; import auth from "../../locales/es-es/auth.json"; export type TranslationKeys = { common: Paths<typeof common>; home: Paths<typeof home>; catalog: Paths<typeof catalog>; auth: Paths<typeof auth>; }; export const useTypeSafeTranslation = <T extends keyof TranslationKeys>( ns: T ) => { const { t, lang } = useTranslation(ns); return { t: ( s: TranslationKeys[T], q?: TranslationQuery, o?: { returnObjects?: boolean; fallback?: string | string[]; default?: string; } ) => t(s, q, o), lang, }; };
And then you just pass the namespace to the hook, and you have a type safe
t
functionimport React from "react"; import { useTypeSafeTranslation } from "./useTypeSafeTranslation"; interface TestComponentProps {} export const TestComponent: React.FC<TestComponentProps> = () => { const { t } = useTypeSafeTranslation("common"); return <>{t("footer.legal.paymentMethods")}</>; } ;
![]()
Adapted from https://github.com/benawad/dogehouse
This little rework could help us to get dicwords from another namespaces different to default which sets in useTypeSafeTranslation.
import useTranslation from "next-translate/useTranslation";
import type { TranslationQuery } from "next-translate";
import type ProfileEn from "@/../locales/en/profile.json";
import type CommonEn from "@/../locales/en/common.json";
import type ProfileRu from "@/../locales/ru/profile.json";
import type CommonRu from "@/../locales/ru/common.json";
type Join<S1, S2> = S1 extends string
? S2 extends string
? `${S1}.${S2}`
: never
: never;
export type Paths<T> = {
[K in keyof T]: T[K] extends Record<string, unknown>
? Join<K, Paths<T[K]>>
: K;
}[keyof T];
type All<T> = {
[Ns in keyof T]: `${Extract<Ns, string>}:${Extract<T[Ns], string>}`;
}[keyof T];
export interface TranslationKeys {
common: Paths<typeof CommonRu & typeof CommonEn>;
profile: Paths<typeof ProfileRu & typeof ProfileEn>;
}
export const useTypeSafeTranslation = <T extends keyof TranslationKeys>(
ns: T
) => {
const { t, lang } = useTranslation(ns);
return {
t: (
s: TranslationKeys[T] | All<Omit<TranslationKeys, T>>,
q?: TranslationQuery,
o?: {
returnObjects?: boolean;
fallback?: string | string[];
default?: string;
}
) => t(s, q, o),
lang,
};
};
Feel free to PR improving the types
My current implementation works good with next.js 13 app directory.
next-translate.d.ts
import type { I18n, Translate } from "next-translate";
import type common from "~/../locales/en/common.json";
import type home from "~/../locales/en/home.json";
type Join<S1, S2> = S1 extends string
? S2 extends string
? `${S1}.${S2}`
: never
: never;
export type Paths<T> = {
[K in keyof T]: T[K] extends Record<string, unknown>
? Join<K, Paths<T[K]>>
: K;
}[keyof T];
export interface TranslationsKeys {
common: Paths<typeof common>;
home: Paths<typeof home>;
}
export interface TypeSafeTranslate<Namespace extends keyof TranslationsKeys>
extends Omit<I18n, "t"> {
t: (
key: TranslationsKeys[Namespace],
...rest: Tail<Parameters<Translate>>
) => string;
}
declare module "next-translate/useTranslation" {
export default function useTranslation<
Namespace extends keyof TranslationsKeys,
>(namespace: Namespace): TypeSafeTranslate<Namespace>;
}
I modified the last example allowing plurals:
t('example', { count: 5 }); // example_other
exact matches:
t('example', { count: 99 }); // example_99
and tagged template string:
t`example` // example
next-translate.d.ts:
import type { I18n, Translate } from "next-translate";
type RemovePlural<Key extends string> = Key extends `${infer Prefix}${| "_zero"
| "_one"
| "_two"
| "_few"
| "_many"
| "_other"
| `_${infer Num}`}`
? Prefix
: Key;
type Join<S1, S2> = S1 extends string
? S2 extends string
? `${S1}.${S2}`
: never
: never;
export type Paths<T> = RemovePlural<{
[K in keyof T]: T[K] extends Record<string, unknown>
? Join<K, Paths<T[K]>>
: K;
}[keyof T]>;
export interface TranslationsKeys {
common: Paths<typeof import("./locales/en/common.json")>;
home: Paths<typeof import("./locales/en/home.json")>;
}
export interface TypeSafeTranslate<Namespace extends keyof TranslationsKeys>
extends Omit<I18n, "t"> {
t: {
(key: TranslationsKeys[Namespace], ...rest: Tail<Parameters<Translate>>): string;
<T extends string>(template: TemplateStringsArray): string;
};
}
declare module "next-translate/useTranslation" {
export default function useTranslation<
Namespace extends keyof TranslationsKeys,
>(namespace: Namespace): TypeSafeTranslate<Namespace>;
}
I am thinking of adding a new configuration property to auto-generate and update this file as new namespaces are added. What do you think about this? π€
Sounds like a good idea to me :)
Omar
On Sat, Jul 15 2023 at 6:59 PM, Aral Roca Gomez @.***> wrote:
I modified the last example allowing plurals:
t('example', { count: 5 }); // example_other
exact matches:
t('example', { count: 99 }); // example_99
and tagged template string:
t
example
// examplenext-translate.d.ts:
import type { I18n, Translate } from "next-translate";import type common from "./locales/en/common.json";import type home from "./locales/en/home.json"; type RemovePlural<Key extends string> = Key extends
${infer Prefix}${| "_zero" | "_one" | "_two" | "_few" | "_many" | "_other" |
_${infer Num}}
? Prefix : Key; type Join<S1, S2> = S1 extends string ? S2 extends string ?${S1}.${S2}
: never : never; export type Paths<T> = { [K in keyof T]: T[K] extends Record<string, unknown> ? Join<K, Paths<T[K]>> : K;}[keyof T]; export interface TranslationsKeys { common: RemovePlural<Paths>; home: RemovePlural<Paths >;} type TranslateKey<T> = T extends string ? RemovePlural<Paths > : never; export interface TypeSafeTranslate<Namespace extends keyof TranslationsKeys> extends Omit<I18n, "t"> { t: { (key: TranslationsKeys[Namespace], ...rest: Tail<Parameters<Translate>>): string; <T extends string>(template: TemplateStringsArray, ...keys: TranslateKey<T>[]): string; };} declare module "next-translate/useTranslation" { export default function useTranslation< Namespace extends keyof TranslationsKeys, (namespace: Namespace): TypeSafeTranslate<Namespace>;}
I am thinking of adding a new configuration property to auto-generate and update this file as new namespaces are added. What do you think about this? [image: π€]
β Reply to this email directly, view it on GitHub https://github.com/aralroca/next-translate/issues/721#issuecomment-1636725412, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAONU3RFAT6SMYQFE57QRW3XQJSYDANCNFSM5HOCJ7EA . You are receiving this because you were mentioned.Message ID: @.***>
I'd really like to find an elegant way to do it without needing to create next-translate.d.ts, but relying on the JSONs of the namespaces that everyone puts in is not clear to me. I don't know if pulling the types from the namespaces defined in the i18n.js file would work or not. Also it depends on where people have these namespaces, if it is the default form (inside locales/lang/namespace.json) maybe it could work. Well I'll investigate a bit more and see.
for now I did a little improvement in 2.5 to simplify the next-translate.d.ts
file:
- Release notes: https://github.com/aralroca/next-translate-plugin/releases/tag/2.5.0
- Docs: https://github.com/aralroca/next-translate/blob/2.5.0/docs/type-safety.md
Example:
import type { Paths, I18n, Translate } from 'next-translate'
export interface TranslationsKeys {
// Example with "common" and "home" namespaces in "en" (the default language):
common: Paths<typeof import('./locales/en/common.json')>
home: Paths<typeof import('./locales/en/home.json')>
// Specify here all the namespaces you have...
}
export interface TypeSafeTranslate<Namespace extends keyof TranslationsKeys>
extends Omit<I18n, 't'> {
t: {
(
key: TranslationsKeys[Namespace],
...rest: Tail<Parameters<Translate>>
): string
<T extends string>(template: TemplateStringsArray): string
}
}
declare module 'next-translate/useTranslation' {
export default function useTranslation<
Namespace extends keyof TranslationsKeys
>(namespace: Namespace): TypeSafeTranslate<Namespace>
}
@aralroca Thank you for pushing this forward!
Unfortunately I can't get it to work - is it really only this snippet that needs to be added? I also tried adding it to tsconfig.json in the include
array.
Edit: I had to include the namespace in useTranslation()
- default ns won't work out of the box.
Additional question: what if returnObjects
is set to true
? Obviously string won't work. Any idea on how we could set proper types for objects?
@sandrooco it should work in the latest next-translate version. The implementation supports these 2 scenarios:
-
namespaces in
useTranslation
-
keys in
t
:
goes no further. No matter what parameters you use, withreturnObjects
these behaviors should work the same.
Thanks for adding this feature! very helpful!
However, I have tried to add it and I get two errors with this code:
for now I did a little improvement in 2.5 to simplify the
next-translate.d.ts
file:
- Release notes: https://github.com/aralroca/next-translate-plugin/releases/tag/2.5.0
- Docs: https://github.com/aralroca/next-translate/blob/2.5.0/docs/type-safety.md
Example:
import type { Paths, I18n, Translate } from 'next-translate' export interface TranslationsKeys { // Example with "common" and "home" namespaces in "en" (the default language): common: Paths<typeof import('./locales/en/common.json')> home: Paths<typeof import('./locales/en/home.json')> // Specify here all the namespaces you have... } export interface TypeSafeTranslate<Namespace extends keyof TranslationsKeys> extends Omit<I18n, 't'> { t: { ( key: TranslationsKeys[Namespace], ...rest: Tail<Parameters<Translate>> ): string <T extends string>(template: TemplateStringsArray): string } } declare module 'next-translate/useTranslation' { export default function useTranslation< Namespace extends keyof TranslationsKeys >(namespace: Namespace): TypeSafeTranslate<Namespace> }
- That
Tail
is not defined. I tried to recreate it myself with something like:type Tail<T extends readonly any[]> = T extends readonly [any, ...infer TT] ? TT : [];
but I'm not sure this is the exact implementation - in
<T extends string>(template: TemplateStringsArray): string
, we have defined the genericT
but never used. I imagine this is the intended code?<T extends string>(template: TemplateStringsArray): T
Thanks again for this library!
@valerioleo probably depends on the TypeScript version. Feel free to PR these missing parts.
About TemplateStringsArray
the only thing is ignore these cases:
t`some.key`
because for now is not possible to strong type the content of some.key
in template strings.
@aralroca Thank you for pushing this forward! Unfortunately I can't get it to work - is it really only this snippet that needs to be added? I also tried adding it to tsconfig.json in the
include
array.Edit: I had to include the namespace in
useTranslation()
- default ns won't work out of the box. Additional question: what ifreturnObjects
is set totrue
? Obviously string won't work. Any idea on how we could set proper types for objects?
Here is a functional version that defaults to "common"
namespace while also allowing it to be overridable:
import type { I18n, Paths, Translate } from "next-translate";
import EN from "@/locales/en/common.json";
interface TranslationsKeys {
common: Paths<typeof EN>;
}
interface TypeSafeTranslate<Namespace extends keyof TranslationsKeys>
extends Omit<I18n, "t"> {
t: {
(
key: TranslationsKeys[Namespace],
...rest: Tail<Parameters<Translate>>
): string;
<T extends string>(template: TemplateStringsArray): string;
};
}
declare module "next-translate/useTranslation" {
export default function useTranslation<
Namespace extends keyof TranslationsKeys,
>(namespace: Namespace = "common"): TypeSafeTranslate<Namespace>;
}
Example
t()
function:
useTranslate()
hook:
Thanks for adding this feature! very helpful!
However, I have tried to add it and I get two errors with this code:
for now I did a little improvement in 2.5 to simplify the
next-translate.d.ts
file:
- Release notes: https://github.com/aralroca/next-translate-plugin/releases/tag/2.5.0
- Docs: https://github.com/aralroca/next-translate/blob/2.5.0/docs/type-safety.md
Example:
import type { Paths, I18n, Translate } from 'next-translate' export interface TranslationsKeys { // Example with "common" and "home" namespaces in "en" (the default language): common: Paths<typeof import('./locales/en/common.json')> home: Paths<typeof import('./locales/en/home.json')> // Specify here all the namespaces you have... } export interface TypeSafeTranslate<Namespace extends keyof TranslationsKeys> extends Omit<I18n, 't'> { t: { ( key: TranslationsKeys[Namespace], ...rest: Tail<Parameters<Translate>> ): string <T extends string>(template: TemplateStringsArray): string } } declare module 'next-translate/useTranslation' { export default function useTranslation< Namespace extends keyof TranslationsKeys >(namespace: Namespace): TypeSafeTranslate<Namespace> }
- That
Tail
is not defined. I tried to recreate it myself with something like:type Tail<T extends readonly any[]> = T extends readonly [any, ...infer TT] ? TT : [];
but I'm not sure this is the exact implementation- in
<T extends string>(template: TemplateStringsArray): string
, we have defined the genericT
but never used. I imagine this is the intended code?<T extends string>(template: TemplateStringsArray): T
Thanks again for this library!
Outdated typescript version might be the case here.