From 79a1c13a95c3f7b985c0ed551a79fcbc7072ff95 Mon Sep 17 00:00:00 2001 From: Adam Skoufis Date: Thu, 29 Feb 2024 10:00:27 +1100 Subject: [PATCH] docs: Add JSdocs for `TranslationKeys`, `VocabProvider` and `useLanguage` (#206) --- .changeset/mighty-waves-pump.md | 15 ++++++++ .changeset/ninety-tomatoes-work.md | 5 +++ .changeset/yellow-months-sit.md | 5 +++ README.md | 26 +++++++++++++ fixtures/simple/src/client.tsx | 11 +++++- packages/core/src/types.ts | 42 ++++++++++++++++++--- packages/react/src/index.tsx | 59 ++++++++++++++++++++++++++---- 7 files changed, 150 insertions(+), 13 deletions(-) create mode 100644 .changeset/mighty-waves-pump.md create mode 100644 .changeset/ninety-tomatoes-work.md create mode 100644 .changeset/yellow-months-sit.md diff --git a/.changeset/mighty-waves-pump.md b/.changeset/mighty-waves-pump.md new file mode 100644 index 00000000..8bcef802 --- /dev/null +++ b/.changeset/mighty-waves-pump.md @@ -0,0 +1,15 @@ +--- +'@vocab/core': patch +--- + +Enable the `TranslationKeys` type to operate on a union of translations + +**EXAMPLE USAGE** + +```tsx +import { TranslationKeys } from '@vocab/core'; +import fooTranslations from './foo.vocab'; +import barTranslations from './bar.vocab'; + +type FooBarTranslationKeys = TranslationKeys; +``` diff --git a/.changeset/ninety-tomatoes-work.md b/.changeset/ninety-tomatoes-work.md new file mode 100644 index 00000000..37cf133c --- /dev/null +++ b/.changeset/ninety-tomatoes-work.md @@ -0,0 +1,5 @@ +--- +'@vocab/core': patch +--- + +Add documentation to the `TranslationKeys` type diff --git a/.changeset/yellow-months-sit.md b/.changeset/yellow-months-sit.md new file mode 100644 index 00000000..911b02e4 --- /dev/null +++ b/.changeset/yellow-months-sit.md @@ -0,0 +1,5 @@ +--- +'@vocab/react': patch +--- + +Add documentation for `VocabProvider` and `useLanguage` diff --git a/README.md b/README.md index 747807db..0b99acb1 100644 --- a/README.md +++ b/README.md @@ -368,6 +368,32 @@ module.exports = { }; ``` +## Translation Key Types + +If you need to access the keys of your translations as a TypeScript type, you can use the `TranslationKeys` type from `@vocab/core`: + +```jsonc +// translations.json +{ + "Hello": { + "message": "Hello" + }, + "Goodbye": { + "message": "Goodbye" + } +} +``` + +```ts +import type { TranslationKeys } from '@vocab/core'; +import translations from './.vocab'; + +// "Hello" | "Goodbye" +type MyTranslationKeys = TranslationKeys< + typeof translations +>; +``` + ## Generated languages Vocab supports the creation of generated languages via the `generatedLanguages` config. diff --git a/fixtures/simple/src/client.tsx b/fixtures/simple/src/client.tsx index 9f3d7619..43f29339 100644 --- a/fixtures/simple/src/client.tsx +++ b/fixtures/simple/src/client.tsx @@ -1,14 +1,23 @@ import { VocabProvider, useTranslations } from '@vocab/react'; +import type { TranslationKeys } from '@vocab/core'; import React, { type ReactNode, useState } from 'react'; import { createRoot } from 'react-dom/client'; import commonTranslations from './.vocab'; import clientTranslations from './client.vocab'; +type CommonTranslationKeys = TranslationKeys; + +const useCommonTranslation = (key: CommonTranslationKeys) => { + const { t } = useTranslations(commonTranslations); + + return t(key); +}; + function Content() { const common = useTranslations(commonTranslations); const client = useTranslations(clientTranslations); - const message = `${common.t('hello')} ${common.t('world')}`; + const message = `${common.t('hello')} ${useCommonTranslation('world')}`; const specialCharacterResult = client.t( 'specialCharacters - \'‘’“”"!@#$%^&*()_+\\/`~\\\\', ); diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index d2a2afe1..cf4c9496 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -9,16 +9,13 @@ export type ParsedFormatFn = (parts: any) => any; export type ParsedFormatFnByKey = Record; /** - * Equivalent to the `string` type, but tricks the language server into prodiving + * Equivalent to the `string` type, but tricks TypeScript into prodiving * suggestions for string literals passed into the `Suggestions` generic parameter * * @example - * Accept any string, but suggest specific animals - * ``` * type AnyAnimal = StringWithSuggestions<"cat" | "dog">; * // Suggests cat and dog, but accepts any string * const animal: AnyAnimal = ""; - * ``` */ export type StringWithSuggestions = | Suggestions @@ -75,9 +72,44 @@ export type TranslationFile< load: (language: Language) => Promise; }; +/** + * A utility type to get the union of all translation keys from a translation file + * + * @example + * // translations.json + * { + * "Hello": { + * "message": "Hello", + * }, + * "Goodbye": { + * "message": "Goodbye", + * }, + * } + * + * // myFile.ts + * import { TranslationKeys } from '@vocab/core'; + * import translations from './.vocab'; + * + * // 'Hello' | 'Goodbye' + * type TheTranslationKeys = TranslationKeys; + * + * @example + * import { TranslationKeys } from '@vocab/core'; + * import fooTranslations from './foo.vocab'; + * import barTranslations from './bar.vocab'; + * + * // It even works with multiple translation files + * type FooBarTranslationKeys = TranslationKeys; + */ export type TranslationKeys< Translations extends TranslationFile, -> = keyof Awaited>; + // The `extends unknown` here forces TypeScript to distribute over the input type, which might be a union of + // multiple `TranslationFile`s. Without this, if you pass in a union, TypeScript tries to get the `ReturnType` + // of a union, which does not work and returns `never`. + // All types are assignable to `unknown`, so we never hit the `never` case. +> = Translations extends unknown + ? keyof Awaited> + : never; export interface LanguageTarget { // The name or tag of a language diff --git a/packages/react/src/index.tsx b/packages/react/src/index.tsx index d7c828d1..f10a8c58 100644 --- a/packages/react/src/index.tsx +++ b/packages/react/src/index.tsx @@ -16,18 +16,60 @@ import React, { type Locale = string; -interface TranslationsValue { +interface TranslationsContextValue { + /** + * The `language` passed in to your `VocabProvider` + */ language: LanguageName; + /** + * The `locale` passed in to your `VocabProvider` + * + * Please note that this value will be `undefined` if you have not passed a `locale` to your `VocabProvider`. + * If your languages are named with IETF language tags, you should just use `language` instead of + * this value, unless you specifically need to access your `locale` override. + */ locale?: Locale; } -const TranslationsContext = React.createContext( - undefined, -); - -interface VocabProviderProps extends TranslationsValue { +const TranslationsContext = React.createContext< + TranslationsContextValue | undefined +>(undefined); + +// Not extending TranslationsContextValue so we can tailor the docs for each prop to be better +// suited to the provider, rather than for the useLanguage hook +interface VocabProviderProps { + /** + * The language to load translations for. Must be one of the language names defined in your `vocab.config.js`. + */ + language: TranslationsContextValue['language']; + /** + * A locale override. By default, Vocab will use the `language` as the locale when formatting messages if + * `locale` is not set. If your languages are named with IETF language tags, you probably don't need to + * set this value. + * + * You may want to override the locale for a specific language if the default formatting for that locale + * is not desired. + * + * @example + * // Override the locale for th-TH to use the Gregorian calendar instead of the default Buddhist calendar + * + * + * + */ + locale?: TranslationsContextValue['locale']; children: ReactNode; } + +/** + * Provides a translation context for your application + * + * @example + * import { VocabProvider } from '@vocab/react'; + * + * + * + * + */ export const VocabProvider = ({ children, language, @@ -42,7 +84,10 @@ export const VocabProvider = ({ ); }; -export const useLanguage = (): TranslationsValue => { +/** + * @returns The `language` and `locale` values passed in to your `VocabProvider` + */ +export const useLanguage = (): TranslationsContextValue => { const context = useContext(TranslationsContext); if (!context) { throw new Error(