Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/actions/setup-node/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ inputs:
node-version:
description: 'Specify Node version'
required: false
default: '22'
default: '24'

runs:
using: 'composite'
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ on:
default: false
type: boolean

permissions:
id-token: write # Required for OIDC
contents: write

jobs:
package_release:
name: Release from "${{ github.ref_name }}" branch
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/size.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: preactjs/compressed-size-action@v2
env:
NODE_OPTIONS: --max_old_space_size=4096
YARN_IGNORE_ENGINES: 'true' # Skip validation for node20 requirement
with:
repo-token: '${{ secrets.GITHUB_TOKEN }}'
pattern: './dist/**/*.{js,cjs,css,json}'
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Root configs:
- codecov.yml
- commitlint.config.mjs
- eslint.config.mjs,
- i18next-parser.config.js
- i18next.config.ts
- jest.config.js
- jest.config.js
- playwright.config.ts
Expand Down
15 changes: 0 additions & 15 deletions i18next-parser.config.js

This file was deleted.

29 changes: 29 additions & 0 deletions i18next.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { defineConfig } from 'i18next-cli';

export default defineConfig({
locales: ['de', 'en', 'es', 'fr', 'hi', 'it', 'ja', 'ko', 'nl', 'pt', 'ru', 'tr'],
extract: {
defaultNS: false,
extractFromComments: false,
functions: ['t', '*.t'],
input: ['./src/**/*.{tsx,ts}'],
keySeparator: false,
nsSeparator: false,
output: 'src/i18n/{{language}}.json',
preservePatterns: [
// to preserve a whole group
'timestamp/*',

// or exact key if you want :
// 'timestamp/DateSeparator',

// or if you’re using explicit namespaces:
// 'translation:timestamp/DateSeparator',
],
removeUnusedKeys: false,
},
types: {
input: ['locales/{{language}}/{{namespace}}.json'],
output: 'src/types/i18next.d.ts',
},
});
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@
"eslint-plugin-sort-destructure-keys": "^2.0.0",
"globals": "^15.13.0",
"husky": "^8.0.3",
"i18next-parser": "^9.3.0",
"i18next-cli": "^1.31.0",
"jest": "^29.7.0",
"jest-axe": "^8.0.0",
"jest-environment-jsdom": "^29.7.0",
Expand All @@ -233,7 +233,7 @@
"prettier": "^3.5.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"semantic-release": "^24.2.3",
"semantic-release": "^25.0.2",
"stream-chat": "^9.25.0",
"ts-jest": "^29.2.5",
"typescript": "^5.4.5",
Expand All @@ -242,7 +242,7 @@
"scripts": {
"build": "rm -rf dist && yarn build-translations && yarn bundle",
"bundle": "concurrently ./scripts/bundle-esm.mjs ./scripts/copy-css.sh scripts/bundle-cjs.mjs",
"build-translations": "i18next",
"build-translations": "i18next-cli extract",
"coverage": "jest --collectCoverage && codecov",
"lint": "yarn prettier --list-different && yarn eslint && yarn validate-translations",
"lint-fix": "yarn prettier-fix && yarn eslint-fix",
Expand Down
13 changes: 11 additions & 2 deletions src/components/Attachment/AttachmentActions.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useMemo } from 'react';
import type { Action, Attachment } from 'stream-chat';

import { useTranslationContext } from '../../context';
Expand Down Expand Up @@ -26,6 +26,15 @@ const UnMemoizedAttachmentActions = (props: AttachmentActionsProps) => {
value?: string,
) => actionHandler?.(name, value, event);

const knownActionText = useMemo<Record<string, string>>(
() => ({
Cancel: t('Cancel'),
Send: t('Send'),
Shuffle: t('Shuffle'),
}),
[t],
);

return (
<div className='str-chat__message-attachment-actions'>
<div className='str-chat__message-attachment-actions-form'>
Expand All @@ -38,7 +47,7 @@ const UnMemoizedAttachmentActions = (props: AttachmentActionsProps) => {
key={`${id}-${action.value}`}
onClick={(event) => handleActionClick(event, action.name, action.value)}
>
{action.text ? t(action.text) : null}
{action.text ? (knownActionText[action.text] ?? t(action.text)) : null}
</button>
))}
</div>
Expand Down
4 changes: 2 additions & 2 deletions src/components/Message/ReminderNotification.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ export const ReminderNotification = ({ reminder }: ReminderNotificationProps) =>
<span>
{isBehindRefreshBoundary
? t('Due since {{ dueSince }}', {
dueSince: t(`timestamp/ReminderNotification`, {
dueSince: t('timestamp/ReminderNotification', {
timestamp: reminder.remindAt,
}),
})
: t(`Due {{ timeLeft }}`, {
: t('Due {{ timeLeft }}', {
timeLeft: t('duration/Message reminder', {
milliseconds: timeLeftMs,
}),
Expand Down
4 changes: 2 additions & 2 deletions src/components/Message/hooks/useMuteHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const useMuteHandler = (

notify(
successMessage ||
t(`{{ user }} has been muted`, {
t('{{ user }} has been muted', {
user: message.user.name || message.user.id,
}),
'success',
Expand All @@ -61,7 +61,7 @@ export const useMuteHandler = (
try {
await client.unmuteUser(message.user.id);

const fallbackMessage = t(`{{ user }} has been unmuted`, {
const fallbackMessage = t('{{ user }} has been unmuted', {
user: message.user.name || message.user.id,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { UnsupportedAttachmentPreview as DefaultUnknownAttachmentPreview } from
import type { VoiceRecordingPreviewProps } from './VoiceRecordingPreview';
import { VoiceRecordingPreview as DefaultVoiceRecordingPreview } from './VoiceRecordingPreview';
import type { FileAttachmentPreviewProps } from './FileAttachmentPreview';
import { FileAttachmentPreview as DefaultFilePreview } from './FileAttachmentPreview';
import DefaultFilePreview from './FileAttachmentPreview';
import type { ImageAttachmentPreviewProps } from './ImageAttachmentPreview';
import { ImageAttachmentPreview as DefaultImagePreview } from './ImageAttachmentPreview';
import { useAttachmentsForPreview, useMessageComposer } from '../hooks';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export type FileAttachmentPreviewProps<CustomLocalMetadata = unknown> =
| LocalVideoAttachment<CustomLocalMetadata>
>;

export const FileAttachmentPreview = ({
const FileAttachmentPreview = ({
attachment,
handleRetry,
removeAttachments,
Expand Down Expand Up @@ -50,6 +50,7 @@ export const FileAttachmentPreview = ({

{['blocked', 'failed'].includes(uploadState) && !!handleRetry && (
<button
aria-label={t('aria/Retry upload')}
className='str-chat__attachment-preview-error str-chat__attachment-preview-error-file'
data-testid='file-preview-item-retry-button'
onClick={() => {
Expand Down Expand Up @@ -84,3 +85,4 @@ export const FileAttachmentPreview = ({
</div>
);
};
export default FileAttachmentPreview;
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export const ImageAttachmentPreview = ({

{['blocked', 'failed'].includes(uploadState) && (
<button
aria-label={t('aria/Retry upload')}
className='str-chat__attachment-preview-error str-chat__attachment-preview-error-image'
data-testid='image-preview-item-retry-button'
onClick={() => handleRetry(attachment)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export const VoiceRecordingPreview = ({
{['blocked', 'failed'].includes(attachment.localMetadata?.uploadState) &&
!!handleRetry && (
<button
aria-label={t('aria/Retry upload')}
className='str-chat__attachment-preview-error str-chat__attachment-preview-error-file'
data-testid='file-preview-item-retry-button'
onClick={() => handleRetry(attachment)}
Expand Down
4 changes: 3 additions & 1 deletion src/components/MessageInput/SendButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import { SendIcon } from './icons';
import { useMessageComposerHasSendableData } from './hooks';
import type { UpdatedMessage } from 'stream-chat';
import { useTranslationContext } from '../../context';

export type SendButtonProps = {
sendMessage: (
Expand All @@ -10,10 +11,11 @@ export type SendButtonProps = {
) => void;
} & React.ComponentProps<'button'>;
export const SendButton = ({ sendMessage, ...rest }: SendButtonProps) => {
const { t } = useTranslationContext();
const hasSendableData = useMessageComposerHasSendableData();
return (
<button
aria-label='Send'
aria-label={t('aria/Send')}
className='str-chat__send-button'
data-testid='send-button'
disabled={!hasSendableData}
Expand Down
13 changes: 11 additions & 2 deletions src/components/Poll/PollCreationDialog/MultipleAnswersField.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import clsx from 'clsx';
import React from 'react';
import React, { useMemo } from 'react';
import { SimpleSwitchField } from '../../Form/SwitchField';
import { FieldError } from '../../Form/FieldError';
import { useTranslationContext } from '../../../context';
Expand All @@ -20,6 +20,15 @@ export const MultipleAnswersField = () => {
pollComposer.state,
pollComposerStateSelector,
);

const knownValidationErrors = useMemo<Record<string, string>>(
() => ({
'Enforce unique vote is enabled': t('Enforce unique vote is enabled'),
'Type a number from 2 to 10': t('Type a number from 2 to 10'),
}),
[t],
);

return (
<div
className={clsx('str-chat__form__expandable-field', {
Expand All @@ -44,7 +53,7 @@ export const MultipleAnswersField = () => {
<FieldError
className='str-chat__form__input-field__error'
data-testid={'poll-max-votes-allowed-input-field-error'}
text={error && t(error)}
text={error && (knownValidationErrors[error] ?? t('Error'))}
/>
<input
id='max_votes_allowed'
Expand Down
11 changes: 9 additions & 2 deletions src/components/Poll/PollCreationDialog/NameField.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useMemo } from 'react';
import clsx from 'clsx';
import { FieldError } from '../../Form/FieldError';
import { useTranslationContext } from '../../../context';
Expand All @@ -15,6 +15,13 @@ export const NameField = () => {
const { t } = useTranslationContext();
const { pollComposer } = useMessageComposer();
const { error, name } = useStateStore(pollComposer.state, pollComposerStateSelector);
const knownValidationErrors = useMemo<Record<string, string>>(
() => ({
'Question is required': t('Question is required'),
}),
[t],
);

return (
<div
className={clsx(
Expand All @@ -31,7 +38,7 @@ export const NameField = () => {
<FieldError
className='str-chat__form__input-field__error'
data-testid={'poll-name-input-field-error'}
text={error && t(error)}
text={error && (knownValidationErrors[error] ?? t('Error'))}
/>
<input
id='name'
Expand Down
12 changes: 10 additions & 2 deletions src/components/Poll/PollCreationDialog/OptionFieldSet.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import clsx from 'clsx';
import React, { useCallback } from 'react';
import React, { useCallback, useMemo } from 'react';
import { FieldError } from '../../Form/FieldError';
import { DragAndDropContainer } from '../../DragAndDrop/DragAndDropContainer';
import { useTranslationContext } from '../../../context';
Expand All @@ -20,6 +20,14 @@ export const OptionFieldSet = () => {
);
const { t } = useTranslationContext('OptionFieldSet');

const knownValidationErrors = useMemo<Record<string, string>>(
() => ({
'Option already exists': t('Option already exists'),
'Option is empty': t('Option is empty'),
}),
[t],
);

const onSetNewOrder = useCallback(
(newOrder: number[]) => {
const prevOptions = pollComposer.options;
Expand Down Expand Up @@ -52,7 +60,7 @@ export const OptionFieldSet = () => {
<FieldError
className='str-chat__form__input-field__error'
data-testid={'poll-option-input-field-error'}
text={error && t(error)}
text={error && (knownValidationErrors[error] ?? t('Error'))}
/>
<input
id={option.id}
Expand Down
32 changes: 30 additions & 2 deletions src/components/TextareaComposer/SuggestionList/CommandItem.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,49 @@
import type { PropsWithChildren } from 'react';
import { useMemo } from 'react';
import React from 'react';
import type { CommandResponse } from 'stream-chat';
import { useTranslationContext } from '../../../context';

export type CommandItemProps = {
entity: CommandResponse;
};

export const CommandItem = (props: PropsWithChildren<CommandItemProps>) => {
const { t } = useTranslationContext();
const { entity } = props;
const knownArgsTranslations = useMemo<Record<string, string>>(
() => ({
ban: t('ban-command-args'),
giphy: t('giphy-command-args'),
mute: t('mute-command-args'),
unban: t('unban-command-args'),
unmute: t('unmute-command-args'),
}),
[t],
);

const knownDescriptionTranslations = useMemo<Record<string, string>>(
() => ({
ban: t('ban-command-description'),
giphy: t('giphy-command-description'),
mute: t('mute-command-description'),
unban: t('unban-command-description'),
unmute: t('unmute-command-description'),
}),
[t],
);

return (
<div className='str-chat__slash-command'>
<span className='str-chat__slash-command-header'>
<strong>{entity.name}</strong> {entity.args}
<strong>{entity.name}</strong>{' '}
{entity.args && (knownArgsTranslations[entity.name ?? ''] ?? t(entity.args))}
</span>
<br />
<span className='str-chat__slash-command-description'>{entity.description}</span>
<span className='str-chat__slash-command-description'>
{entity.description &&
(knownDescriptionTranslations[entity.name ?? ''] ?? t(entity.description))}
</span>
</div>
);
};
Loading