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
1 change: 1 addition & 0 deletions src/context/ComponentContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ export type ComponentContextValue = {
/** Custom UI component to display the reactions modal, defaults to and accepts same props as: [ReactionsListModal](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Reactions/ReactionsListModal.tsx) */
ReactionsListModal?: React.ComponentType<ReactionsListModalProps>;
RecordingPermissionDeniedNotification?: React.ComponentType<RecordingPermissionDeniedNotificationProps>;
/** Custom UI component to display the message reminder information in the Message UI, defaults to and accepts same props as: [ReminderNotification](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Message/ReminderNotification.tsx) */
ReminderNotification?: React.ComponentType<ReminderNotificationProps>;
/** Custom component to display the search UI, defaults to and accepts same props as: [Search](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Search/Search.tsx) */
Search?: React.ComponentType<SearchProps>;
Expand Down
73 changes: 56 additions & 17 deletions src/i18n/TranslationBuilder/TranslationBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import type { i18n, TFunction } from 'i18next';

type TopicName = string;
type TranslatorName = string;

export type Translator<O extends Record<string, unknown> = Record<string, unknown>> =
(params: { key: string; value: string; t: TFunction; options: O }) => string | null;

Expand Down Expand Up @@ -44,25 +47,45 @@ export type TranslationTopicConstructor = new (

export class TranslationBuilder {
private topics = new Map<string, TranslationTopic>();
// need to keep a registration buffer so that translators can be registered once a topic is registered
// what does not happen when Streami18n is instantiated but rather once Streami18n.init() is invoked
private translatorRegistrationsBuffer: Record<
TopicName,
Record<TranslatorName, Translator>
> = {};

constructor(private i18next: i18n) {}

registerTopic = (name: string, Topic: TranslationTopicConstructor) => {
const topic = new Topic({ i18next: this.i18next });
this.topics.set(name, topic);
this.i18next.use({
name,
process: (value: string, key: string, options: Record<string, unknown>) => {
const topic = this.topics.get(name);
if (!topic) return value;
return topic.translate(value, key, options);
},
type: 'postProcessor' as const,
});
registerTopic = (name: TopicName, Topic: TranslationTopicConstructor) => {
let topic = this.topics.get(name);

if (!topic) {
topic = new Topic({ i18next: this.i18next });
this.topics.set(name, topic);
this.i18next.use({
name,
process: (value: string, key: string, options: Record<string, unknown>) => {
const topic = this.topics.get(name);
if (!topic) return value;
return topic.translate(value, key, options);
},
type: 'postProcessor' as const,
});
}

const additionalTranslatorsToRegister = this.translatorRegistrationsBuffer[name];
if (additionalTranslatorsToRegister) {
Object.entries(additionalTranslatorsToRegister).forEach(
([translatorName, translator]) => {
topic.setTranslator(translatorName, translator);
},
);
delete this.translatorRegistrationsBuffer[name];
}
return topic;
};

disableTopic = (topicName: string) => {
disableTopic = (topicName: TopicName) => {
const topic = this.topics.get(topicName);
if (!topic) return;
this.i18next.use({
Expand All @@ -73,18 +96,34 @@ export class TranslationBuilder {
this.topics.delete(topicName);
};

getTopic = (topicName: string) => this.topics.get(topicName);
getTopic = (topicName: TopicName) => this.topics.get(topicName);

registerTranslators(topicName: string, translators: Record<string, Translator>) {
registerTranslators(
topicName: TopicName,
translators: Record<TranslatorName, Translator>,
) {
const topic = this.getTopic(topicName);
if (!topic) return;
if (!topic) {
if (!this.translatorRegistrationsBuffer[topicName])
this.translatorRegistrationsBuffer[topicName] = {};

Object.entries(translators).forEach(([translatorName, translator]) => {
this.translatorRegistrationsBuffer[topicName][translatorName] = translator;
});
return;
}
Object.entries(translators).forEach(([name, translator]) => {
topic.setTranslator(name, translator);
});
}

removeTranslators(topicName: string, translators: string[]) {
removeTranslators(topicName: TopicName, translators: TranslatorName[]) {
const topic = this.getTopic(topicName);
if (this.translatorRegistrationsBuffer[topicName]) {
translators.forEach((translatorName) => {
delete this.translatorRegistrationsBuffer[topicName][translatorName];
});
}
if (!topic) return;
translators.forEach((name) => {
topic.removeTranslator(name);
Expand Down
1 change: 1 addition & 0 deletions src/i18n/TranslationBuilder/notifications/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { NotificationTranslationTopic } from './NotificationTranslationTopic';
export * from './types';
58 changes: 58 additions & 0 deletions src/i18n/__tests__/TranslationBuilder.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,20 @@ describe('TranslationBuilder and TranslationTopic', () => {
const manager = new TranslationBuilder(mockI18Next);
expect(manager.i18next).toEqual(mockI18Next);
});

it('registers and retrieves the builder', () => {
const manager = new TranslationBuilder(mockI18Next);
manager.registerTopic('notification', NotificationTranslationTopic);
expect(manager.getTopic('notification')).toBeInstanceOf(NotificationTranslationTopic);
});

it('removes builder', () => {
const manager = new TranslationBuilder(mockI18Next);
manager.registerTopic('notification', NotificationTranslationTopic);
manager.disableTopic('notification');
expect(manager.getTopic('notification')).toBeUndefined();
});

it('registers and removes translators', () => {
const translator = jest.fn();
const manager = new TranslationBuilder(mockI18Next);
Expand All @@ -27,4 +30,59 @@ describe('TranslationBuilder and TranslationTopic', () => {
manager.removeTranslators('notification', ['test']);
expect(notificationBuilder.translators.get('test')).toBeUndefined();
});

it('stores translators for non-existent topic in a buffer', () => {
const manager = new TranslationBuilder(mockI18Next);
const translators = { custom1: jest.fn(), custom2: jest.fn() };
manager.registerTranslators('notification', translators);
expect(manager.topics.size).toEqual(0);
expect(manager.translatorRegistrationsBuffer.notification).toEqual(translators);
});

it('removes translators from buffer on translation removal', () => {
const manager = new TranslationBuilder(mockI18Next);
const translators = { custom1: jest.fn(), custom2: jest.fn() };
manager.registerTranslators('notification', translators);
manager.removeTranslators('notification', ['custom1']);
expect(Object.keys(manager.translatorRegistrationsBuffer.notification).length).toBe(
1,
);
expect(manager.translatorRegistrationsBuffer.notification.custom2).toBeDefined();
});

it('flushes the buffered translators on topic registration', () => {
const manager = new TranslationBuilder(mockI18Next);
const translators = { custom1: jest.fn(), custom2: jest.fn() };
manager.registerTranslators('notification', translators);
manager.registerTopic('notification', NotificationTranslationTopic);
expect(manager.translatorRegistrationsBuffer.notification).toBeUndefined();
});

it("overrides the topic's translators with buffered translators", () => {
const manager = new TranslationBuilder(mockI18Next);
const translator = jest.fn().mockImplementation();
const translatorName = 'api:attachment:upload:failed';
const translators = { [translatorName]: translator };
manager.registerTranslators('notification', translators);
manager.registerTopic('notification', NotificationTranslationTopic);
manager
.getTopic('notification')
.translate('key', 'value', { notification: { type: translatorName } });

expect(translator).toHaveBeenCalledTimes(1);
});

it('reuses the already registered topic on repeated registerTopic calls', () => {
const manager = new TranslationBuilder(mockI18Next);
class Topic {
constructor() {
this.id = Math.random().toString();
}
}
manager.registerTopic('custom', Topic);
const firstRegistrationId = manager.getTopic('custom').id;
manager.registerTopic('custom', Topic);
const secondRegistrationId = manager.getTopic('custom').id;
expect(firstRegistrationId).toBe(secondRegistrationId);
});
});
2 changes: 1 addition & 1 deletion src/i18n/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export * from './translations';
export * from './Streami18n';
export * from './TranslationBuilder/TranslationBuilder';
export * from './TranslationBuilder';
export {
defaultDateTimeParser,
defaultTranslatorFunction,
Expand Down