-
-
Notifications
You must be signed in to change notification settings - Fork 209
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Is there a way to have the translationKey strongly typed? #721
Comments
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 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")}</>;
}
;
|
Here's my simplified version FYI, that just patches
And I also just use |
Here's a helper for
|
next update i'll post is to change the |
@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 PS: I am also wrapping |
@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
<#721 (comment)>,
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? |
Yes, we are going to priorize this. However feel free to PR 👍😊 |
You could just clean up the resulting paths using this:
|
This little rework could help us to get dicwords from another namespaces different to default which sets in useTypeSafeTranslation.
|
Feel free to PR improving the types |
My current implementation works good with next.js 13 app directory.
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` // example
*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 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<typeof common>>;
home: RemovePlural<Paths<typeof home>>;}
type TranslateKey<T> = T extends string ? RemovePlural<Paths<typeof common>> : 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
<#721 (comment)>,
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
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! Edit: I had to include the namespace in |
@sandrooco it should work in the latest next-translate version. The implementation supports these 2 scenarios:
goes no further. No matter what parameters you use, with |
Thanks for adding this feature! very helpful! However, I have tried to add it and I get two errors with this code:
Thanks again for this library! |
@valerioleo probably depends on the TypeScript version. Feel free to PR these missing parts. About t`some.key` because for now is not possible to strong type the content of |
Here is a functional version that defaults to
Example
![]()
![]() |
Outdated typescript version might be the case here. |
this is what i use
next-translate.d.ts import { FieldPath, FieldPathValue, FieldValues } from './tr'
export type TranslationsKeys = {
// I use custom loadLocaleFrom
common: typeof import('@/app/ru.json')
index: typeof import('@/app/[lang]/(index)/ru.json')
contacts: typeof import('@/app/[lang]/contacts/ru.json')
}
export type Tr<
TFieldValues extends Record<Namespace, FieldValues>,
Namespace extends keyof TranslationsKeys,
> = Omit<I18n, 't'> & {
t: <
TFieldName extends FieldPath<
TFieldValues[[OtherNamespace] extends [never]
? Namespace
: OtherNamespace]
>,
ReturnObjects extends boolean = false,
OtherNamespace extends keyof TranslationsKeys = never,
>(
key: TFieldName,
query?: TranslationQuery | null,
options?: {
returnObjects?: ReturnObjects
fallback?: string | string[]
default?: FieldPathValue<
TFieldValues[[OtherNamespace] extends [never]
? Namespace
: OtherNamespace],
TFieldName
>
ns?: OtherNamespace
},
) => ReturnObjects extends true
? FieldPathValue<
TFieldValues[[OtherNamespace] extends [never]
? Namespace
: OtherNamespace],
TFieldName
>
: string
}
declare module 'next-translate/useTranslation' {
export default function useTranslation<
Namespace extends keyof TranslationsKeys,
>(namespace: Namespace): Tr<TranslationsKeys, Namespace>
} tr.d.ts (types ripped from react-hook-form) export type BrowserNativeObject = Date | FileList | File
export type Primitive =
| null
| undefined
| string
| number
| boolean
| symbol
| bigint
export type IsEqual<T1, T2> = T1 extends T2
? (<G>() => G extends T1 ? 1 : 2) extends <G>() => G extends T2 ? 1 : 2
? true
: false
: false
export type AnyIsEqual<T1, T2> = T1 extends T2
? IsEqual<T1, T2> extends true
? true
: never
: never
export type PathImpl<K extends string | number, V, TraversedTypes> = V extends
| Primitive
| BrowserNativeObject
? `${K}`
: true extends AnyIsEqual<TraversedTypes, V>
? `${K}`
: `${K}` | `${K}.${PathInternal<V, TraversedTypes | V>}`
export type IsTuple<T extends ReadonlyArray<any>> = number extends T['length']
? false
: true
export type ArrayKey = number
export type TupleKeys<T extends ReadonlyArray<any>> = Exclude<
keyof T,
keyof any[]
>
export type PathInternal<T, TraversedTypes = T> = T extends ReadonlyArray<
infer V
>
? IsTuple<T> extends true
? {
[K in TupleKeys<T>]-?: PathImpl<K & string, T[K], TraversedTypes>
}[TupleKeys<T>]
: PathImpl<ArrayKey, V, TraversedTypes>
: {
[K in keyof T]-?: PathImpl<K & string, T[K], TraversedTypes>
}[keyof T]
export type FieldValues = Record<string, any>
export type FieldPath<TFieldValues extends FieldValues> = Path<TFieldValues>
export type Path<T> = T extends any ? PathInternal<T> : never
export type FieldPathValue<
TFieldValues extends FieldValues,
TFieldPath extends FieldPath<TFieldValues>,
> = PathValue<TFieldValues, TFieldPath>
export type ArrayPath<T> = T extends any ? ArrayPathInternal<T> : never
export type ArrayPathInternal<T, TraversedTypes = T> = T extends ReadonlyArray<
infer V
>
? IsTuple<T> extends true
? {
[K in TupleKeys<T>]-?: ArrayPathImpl<K & string, T[K], TraversedTypes>
}[TupleKeys<T>]
: ArrayPathImpl<ArrayKey, V, TraversedTypes>
: {
[K in keyof T]-?: ArrayPathImpl<K & string, T[K], TraversedTypes>
}[keyof T]
export type ArrayPathImpl<
K extends string | number,
V,
TraversedTypes,
> = V extends Primitive | BrowserNativeObject
? IsAny<V> extends true
? string
: never
: V extends ReadonlyArray<infer U>
? U extends Primitive | BrowserNativeObject
? IsAny<V> extends true
? string
: never
: true extends AnyIsEqual<TraversedTypes, V>
? never
: `${K}` | `${K}.${ArrayPathInternal<V, TraversedTypes | V>}`
: true extends AnyIsEqual<TraversedTypes, V>
? never
: `${K}.${ArrayPathInternal<V, TraversedTypes | V>}`
export type IsAny<T> = 0 extends 1 & T ? true : false
export type PathValue<T, P extends Path<T> | ArrayPath<T>> = T extends any
? P extends `${infer K}.${infer R}`
? K extends keyof T
? R extends Path<T[K]>
? PathValue<T[K], R>
: never
: K extends `${ArrayKey}`
? T extends ReadonlyArray<infer V>
? PathValue<V, R & Path<V>>
: never
: never
: P extends keyof T
? T[P]
: P extends `${ArrayKey}`
? T extends ReadonlyArray<infer V>
? V
: never
: never
: never |
I used this to support namespaced keys type NamespacedTranslationKeys = {
[Namespace in keyof TranslationsKeys]: `${Namespace}:${TranslationsKeys[Namespace]}`
}[keyof TranslationsKeys] Entire code: https://gist.github.com/cassus/cb28122d20f61d3be9c7e09d23033be9 |
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.
The text was updated successfully, but these errors were encountered: