Skip to content

Commit

Permalink
docs: Add JSdocs for TranslationKeys, VocabProvider and `useLangu…
Browse files Browse the repository at this point in the history
…age` (#206)
  • Loading branch information
askoufis committed Feb 28, 2024
1 parent 070acdd commit 79a1c13
Show file tree
Hide file tree
Showing 7 changed files with 150 additions and 13 deletions.
15 changes: 15 additions & 0 deletions .changeset/mighty-waves-pump.md
Original file line number Diff line number Diff line change
@@ -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<typeof fooTranslations | typeof barTranslations>;
```
5 changes: 5 additions & 0 deletions .changeset/ninety-tomatoes-work.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@vocab/core': patch
---

Add documentation to the `TranslationKeys` type
5 changes: 5 additions & 0 deletions .changeset/yellow-months-sit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@vocab/react': patch
---

Add documentation for `VocabProvider` and `useLanguage`
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
11 changes: 10 additions & 1 deletion fixtures/simple/src/client.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof commonTranslations>;

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 - \'‘’“”"!@#$%^&*()_+\\/`~\\\\',
);
Expand Down
42 changes: 37 additions & 5 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,13 @@ export type ParsedFormatFn = (parts: any) => any;
export type ParsedFormatFnByKey = Record<string, ParsedFormatFn>;

/**
* 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 extends string> =
| Suggestions
Expand Down Expand Up @@ -75,9 +72,44 @@ export type TranslationFile<
load: (language: Language) => Promise<void>;
};

/**
* 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<typeof translations>;
*
* @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<typeof fooTranslations | typeof barTranslations>;
*/
export type TranslationKeys<
Translations extends TranslationFile<any, ParsedFormatFnByKey>,
> = keyof Awaited<ReturnType<Translations['getMessages']>>;
// 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<ReturnType<Translations['getMessages']>>
: never;

export interface LanguageTarget {
// The name or tag of a language
Expand Down
59 changes: 52 additions & 7 deletions packages/react/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<TranslationsValue | undefined>(
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
* <VocabProvider language="th-TH" locale="th-TH-u-ca-gregory">
* </App>
* <VocabProvider />
*/
locale?: TranslationsContextValue['locale'];
children: ReactNode;
}

/**
* Provides a translation context for your application
*
* @example
* import { VocabProvider } from '@vocab/react';
*
* <VocabProvider language="en">
* <App />
* <VocabProvider />
*/
export const VocabProvider = ({
children,
language,
Expand All @@ -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(
Expand Down

0 comments on commit 79a1c13

Please sign in to comment.