Skip to content

Commit

Permalink
feat: add commands translations (#2149)
Browse files Browse the repository at this point in the history
  • Loading branch information
MartinCupela committed Oct 31, 2023
1 parent 34ee89c commit f55c86f
Show file tree
Hide file tree
Showing 15 changed files with 288 additions and 29 deletions.
50 changes: 45 additions & 5 deletions docusaurus/docs/React/guides/customization/suggestion-list.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,46 @@ In this example, we will demonstrate how to customize the autocomplete suggestio
appear above the `MessageInput` component when one of the supported [`autocompleteTriggers`](../../components/contexts/message-input-context.mdx#autocompletetriggers)
is entered into the text input.

## Commands translations

The default strings for command suggestion items are in English. To define your custom translations, please, provide these under the following keys in the JSON file corresponding to the language mutation. For example, override the German translations in your `de.json` file, you would target these defaults:

```
{
...
"ban-command-args": "[@Benutzername] [Text]",
"ban-command-description": "Einen Benutzer verbannen",
"ban-command-name": "Verbannen",
...
"giphy-command-args": "[Text]",
"giphy-command-description": "Poste ein zufälliges Gif in den Kanal",
"giphy-command-name": "Giphy",
...
"mute-command-args": "[@Benutzername]",
"mute-command-description": "Stummschalten eines Benutzers",
"mute-command-name": "Stumm schalten"
...
"unban-command-args": "[@Benutzername]",
"unban-command-description": "Einen Benutzer entbannen",
"unban-command-name": "Entbannen",
...
"unmute-command-args": "[@Benutzername]",
"unmute-command-description": "Stummschaltung eines Benutzers aufheben",
"unmute-command-name": "Stummschaltung aufheben",
```

Where:

- **name** is the name of the command
- **description** a phrase describing what the command does
- **args** format notation in which the following strings will be considered as inputs for the given command

:::note
The SDK's default English translation sheet (`en.json`) does not contain the above keys, as the displayed strings are taken directly from the channel data queried from the Stream's back-end.
:::

## Custom suggestion list item components

The [`Channel`](../../components/core-components/channel.mdx) component accepts three props that adjust the look and feel of the autocomplete
suggestion list:

Expand All @@ -21,7 +61,7 @@ suggestion list:

Below we show how to create custom header and list items, while leaving the list container unchanged.

## Suggestion Header
### Suggestion Header

By default, the component library handles autocomplete suggestions for user mentions `@`, commands `/`,
and emojis `:`. The header component receives the text `value` of the `MessageInput` via props. The current trigger
Expand Down Expand Up @@ -54,7 +94,7 @@ To customize `autocompleteTriggers`, pass your own [`TriggerProvider`](../../com
component to `Channel`.
:::

## Suggestion List Items
### Suggestion List Items

Similar to our header component, we will conditionally render the list items based on the `item` type
received by our custom component. The `SuggestionItem` type represents a union of type options
Expand Down Expand Up @@ -108,11 +148,11 @@ const SuggestionItem = React.forwardRef(
);
```

## Implementation
### Implementation

Now that each individual piece has been constructed, we can assemble all of the snippets into the final code example.

### The Code
#### The Code

```css
.suggestion-header {
Expand Down Expand Up @@ -210,7 +250,7 @@ const App = () => (
);
```

### The Result
#### The Result

**Mentions UI:**

Expand Down
49 changes: 39 additions & 10 deletions docusaurus/docs/React/guides/theming/translations.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -330,16 +330,45 @@ The `Streami18n` class wraps [`i18next`](https://www.npmjs.com/package/i18next)
### Class Constructor Options
| Option | Description | Type | Default |
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ---------- |
| language | connected user's language | string | 'en' |
| translationsForLanguage | overrides existing component text | object | {} |
| disableDateTimeTranslations | disables translation of date times | boolean | false |
| debug | enables i18n debug mode | boolean | false |
| logger | logs warnings/errors | function | () => {} |
| dayjsLocaleConfigForLanguage | internal Day.js [config object](https://github.com/iamkun/dayjs/tree/dev/src/locale) and [calendar locale config object](https://day.js.org/docs/en/plugin/calendar) | object | 'enConfig' |
| DateTimeParser | custom date time parser | function | Day.js |
| timezone | valid timezone identifier string (https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) | function | Day.js |
| Option | Description | Type | Default |
|------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-------------------------------------------------|
| DateTimeParser | custom date time parser | function | Day.js |
| dayjsLocaleConfigForLanguage | internal Day.js [config object](https://github.com/iamkun/dayjs/tree/dev/src/locale) and [calendar locale config object](https://day.js.org/docs/en/plugin/calendar) | object | 'enConfig' |
| debug | enables i18n debug mode | boolean | false |
| disableDateTimeTranslations | disables translation of date times | boolean | false |
| language | connected user's language | string | 'en' |
| logger | logs warnings/errors | function | () => {} |
| parseMissingKeyHandler | function executed, when a key is not found among the translations | function | (key: string, defaultValue?: string) => string; |
| translationsForLanguage | overrides existing component text | object | {} |
| timezone | valid timezone identifier string (https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) | function | Day.js |
#### parseMissingKeyHandler
The default implementation returns the default value provided to the translator function, for example the component below will display string `'hello'` if the key `'some-key'` is not found among the translations for a given language:
```tsx
import { useTranslationContext } from 'stream-chat-react';

const Component = () => {
const { t } = useTranslationContext('useCommandTrigger');

return (
<div>{t('some-key', {defaultValue: 'hello'})}</div>
);
}
```
The custom handler may log missing key warnings to the console in the development environment:
```ts
import { Streami18n, Streami18nOptions } from 'stream-chat-react';

const parseMissingKeyHandler: Streami18nOptions['parseMissingKeyHandler'] = (key: string, defaultValue?: string) => {
console.warn(`Streami18n: Missing translation for key: ${key}`);
return defaultValue ?? key;
};

const i18nInstance = new Streami18n({ parseMissingKeyHandler });
```
### Class Instance Methods
Expand Down
36 changes: 31 additions & 5 deletions src/components/MessageInput/hooks/useCommandTrigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,25 @@ import { CommandItem } from '../../CommandItem/CommandItem';

import { useChannelStateContext } from '../../../context/ChannelStateContext';
import { useChatContext } from '../../../context/ChatContext';
import { useTranslationContext } from '../../../context';

import type { CommandResponse } from 'stream-chat';

import type { CommandTriggerSetting } from '../DefaultTriggerProvider';

import type { DefaultStreamChatGenerics } from '../../../types/types';

type ValidCommand<
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
> = Required<Pick<CommandResponse<StreamChatGenerics>, 'name'>> &
Omit<CommandResponse<StreamChatGenerics>, 'name'>;

export const useCommandTrigger = <
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
>(): CommandTriggerSetting<StreamChatGenerics> => {
const { themeVersion } = useChatContext<StreamChatGenerics>('useCommandTrigger');
const { channelConfig } = useChannelStateContext<StreamChatGenerics>('useCommandTrigger');
const { t } = useTranslationContext('useCommandTrigger');

const commands = channelConfig?.commands;

Expand All @@ -25,7 +32,7 @@ export const useCommandTrigger = <
}
const selectedCommands = commands.filter((command) => command.name?.indexOf(query) !== -1);

// sort alphabetically unless the you're matching the first char
// sort alphabetically unless you're matching the first char
selectedCommands.sort((a, b) => {
let nameA = a.name?.toLowerCase();
let nameB = b.name?.toLowerCase();
Expand All @@ -51,10 +58,29 @@ export const useCommandTrigger = <
const result = selectedCommands.slice(0, themeVersion === '2' ? 5 : 10);
if (onReady)
onReady(
result.filter(
(result): result is CommandResponse<StreamChatGenerics> & { name: string } =>
result.name !== undefined,
),
result
.filter(
(result): result is CommandResponse<StreamChatGenerics> & { name: string } =>
result.name !== undefined,
)
.map((commandData) => {
const translatedCommandData: ValidCommand<StreamChatGenerics> = {
name: t(`${commandData.name}-command-name`, {
defaultValue: commandData.name,
}),
};

if (commandData.args)
translatedCommandData.args = t(`${commandData.name}-command-args`, {
defaultValue: commandData.args,
});
if (commandData.description)
translatedCommandData.description = t(`${commandData.name}-command-description`, {
defaultValue: commandData.description,
});

return translatedCommandData;
}),
query,
);

Expand Down
17 changes: 8 additions & 9 deletions src/i18n/Streami18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,13 +242,14 @@ type TimezoneParser = {
const supportsTz = (dateTimeParser: unknown): dateTimeParser is TimezoneParser =>
(dateTimeParser as TimezoneParser).tz !== undefined;

type Options = {
export type Streami18nOptions = {
DateTimeParser?: DateTimeParserModule;
dayjsLocaleConfigForLanguage?: Partial<ILocale> & { calendar?: CalendarLocaleConfig };
debug?: boolean;
disableDateTimeTranslations?: boolean;
language?: TranslationLanguages;
logger?: (message?: string) => void;
parseMissingKeyHandler?: (key: string, defaultValue?: string) => string;
timezone?: string;
translationsForLanguage?: Partial<typeof enTranslations>;
};
Expand Down Expand Up @@ -467,7 +468,7 @@ export class Streami18n {
keySeparator: false;
lng: string;
nsSeparator: false;
parseMissingKeyHandler: (key: string) => string;
parseMissingKeyHandler?: (key: string, defaultValue?: string) => string;
};
/**
* A valid TZ identifier string (https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)
Expand Down Expand Up @@ -499,7 +500,7 @@ export class Streami18n {
*
* @param {*} options
*/
constructor(options: Options = {}) {
constructor(options: Streami18nOptions = {}) {
const finalOptions = {
...defaultStreami18nOptions,
...options,
Expand Down Expand Up @@ -553,14 +554,12 @@ export class Streami18n {
keySeparator: false,
lng: this.currentLanguage,
nsSeparator: false,

parseMissingKeyHandler: (key) => {
this.logger(`Streami18n: Missing translation for key: ${key}`);

return key;
},
};

if (finalOptions.parseMissingKeyHandler) {
this.i18nextConfig.parseMissingKeyHandler = finalOptions.parseMissingKeyHandler;
}

this.validateCurrentLanguage();

const dayjsLocaleConfigForLanguage = finalOptions.dayjsLocaleConfigForLanguage;
Expand Down
15 changes: 15 additions & 0 deletions src/i18n/de.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{
"Attach files": "Dateien anhängen",
"ban-command-args": "[@Benutzername] [Text]",
"ban-command-description": "Einen Benutzer verbannen",
"ban-command-name": "Verbannen",
"Cancel": "Stornieren",
"Channel Missing": "Kanal fehlt",
"Close": "Schließen",
Expand All @@ -25,6 +28,9 @@
"Error · Unsent": "Fehler nicht gesendet",
"Error: {{ errorMessage }}": "Fehler: {{ errorMessage }}",
"Flag": "Meldung",
"giphy-command-args": "[Text]",
"giphy-command-description": "Poste ein zufälliges Gif in den Kanal",
"giphy-command-name": "Giphy",
"Latest Messages": "Neueste Nachrichten",
"Menu": "Menü",
"Message Failed · Click to try again": "Nachricht fehlgeschlagen · Klicken, um es erneut zu versuchen",
Expand All @@ -33,6 +39,9 @@
"Message has been successfully flagged": "Nachricht wurde erfolgreich gemeldet",
"Message pinned": "Nachricht gepinnt",
"Mute": "Stumm schalten",
"mute-command-args": "[@Benutzername]",
"mute-command-description": "Stummschalten eines Benutzers",
"mute-command-name": "Stumm schalten",
"New": "Neu",
"New Messages!": "Neue Nachrichten!",
"No chats here yet…": "Noch keine Chats hier...",
Expand All @@ -55,7 +64,13 @@
"This message was deleted...": "Diese Nachricht wurde gelöscht...",
"Thread": "Thread",
"Type your message": "Nachricht eingeben",
"unban-command-args": "[@Benutzername]",
"unban-command-description": "Einen Benutzer entbannen",
"unban-command-name": "Entbannen",
"Unmute": "Stummschaltung aufheben",
"unmute-command-args": "[@Benutzername]",
"unmute-command-description": "Stummschaltung eines Benutzers aufheben",
"unmute-command-name": "Stummschaltung aufheben",
"Unpin": "Pin entfernen",
"Upload type: \"{{ type }}\" is not allowed": "Upload-Typ: \"{{ type }}\" ist nicht erlaubt",
"Wait until all attachments have uploaded": "Bitte warten, bis alle Anhänge hochgeladen wurden",
Expand Down
15 changes: 15 additions & 0 deletions src/i18n/es.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{
"Attach files": "Adjuntar archivos",
"ban-command-args": "[@usuario] [texto]",
"ban-command-description": "Prohibir a un usuario",
"ban-command-name": "prohibir",
"Cancel": "Cancelar",
"Channel Missing": "Falta canal",
"Close": "Cerca",
Expand All @@ -25,6 +28,9 @@
"Error · Unsent": "Error · No enviado",
"Error: {{ errorMessage }}": "Error: {{ errorMessage }}",
"Flag": "Bandera",
"giphy-command-args": "[texto]",
"giphy-command-description": "Publicar un gif aleatorio en el canal",
"giphy-command-name": "giphy",
"Latest Messages": "Últimos mensajes",
"Menu": "Menú",
"Message Failed · Click to try again": "Mensaje fallido · Haga clic para volver a intentarlo",
Expand All @@ -33,6 +39,9 @@
"Message has been successfully flagged": "El mensaje se marcó correctamente",
"Message pinned": "Mensaje fijado",
"Mute": "Mudo",
"mute-command-args": "[@usuario]",
"mute-command-description": "Silenciar a un usuario",
"mute-command-name": "silenciar",
"New": "Nuevo",
"New Messages!": "¡Nuevos mensajes!",
"No chats here yet…": "Aún no hay mensajes aquí...",
Expand All @@ -55,7 +64,13 @@
"This message was deleted...": "Este mensaje fue eliminado ...",
"Thread": "Hilo",
"Type your message": "Escribe tu mensaje",
"unban-command-args": "[@usuario]",
"unban-command-description": "Quitar la prohibición a un usuario",
"unban-command-name": "quitar la prohibición",
"Unmute": "Activar sonido",
"unmute-command-args": "[@usuario]",
"unmute-command-description": "Anular el silencio de un usuario",
"unmute-command-name": "quitar el silencio",
"Unpin": "Desprender",
"Upload type: \"{{ type }}\" is not allowed": "Tipo de carga: \"{{ type }}\" no está permitido",
"Wait until all attachments have uploaded": "Espere hasta que se hayan cargado todos los archivos adjuntos",
Expand Down
15 changes: 15 additions & 0 deletions src/i18n/fr.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{
"Attach files": "Pièces jointes",
"ban-command-args": "[@nomdutilisateur] [texte]",
"ban-command-description": "Bannir un utilisateur",
"ban-command-name": "bannir",
"Cancel": "Annuler",
"Channel Missing": "Canal Manquant",
"Close": "Fermer",
Expand All @@ -25,6 +28,9 @@
"Error · Unsent": "Erreur - Non envoyé",
"Error: {{ errorMessage }}": "Erreur : {{ errorMessage }}",
"Flag": "Signaler",
"giphy-command-args": "[texte]",
"giphy-command-description": "Poster un GIF aléatoire dans le canal",
"giphy-command-name": "giphy",
"Latest Messages": "Derniers messages",
"Menu": "Menu",
"Message Failed · Click to try again": "Échec de l'envoi du message - Cliquez pour réessayer",
Expand All @@ -33,6 +39,9 @@
"Message has been successfully flagged": "Le message a été signalé avec succès",
"Message pinned": "Message épinglé",
"Mute": "Muet",
"mute-command-args": "[@nomdutilisateur]",
"mute-command-description": "Muter un utilisateur",
"mute-command-name": "muter",
"New": "Nouveaux",
"New Messages!": "Nouveaux Messages!",
"No chats here yet…": "Pas encore de messages ici...",
Expand All @@ -55,7 +64,13 @@
"This message was deleted...": "Ce message a été supprimé...",
"Thread": "Fil de discussion",
"Type your message": "Saisissez votre message",
"unban-command-args": "[@nomdutilisateur]",
"unban-command-description": "Débannir un utilisateur",
"unban-command-name": "débannir",
"Unmute": "Désactiver muet",
"unmute-command-args": "[@nomdutilisateur]",
"unmute-command-description": "Démuter un utilisateur",
"unmute-command-name": "démuter",
"Unpin": "Détacher",
"Upload type: \"{{ type }}\" is not allowed": "Le type de téléchargement: \"{{ type }}\" n'est pas autorisé",
"Wait until all attachments have uploaded": "Attendez que toutes les pièces jointes soient téléchargées",
Expand Down
Loading

0 comments on commit f55c86f

Please sign in to comment.