Skip to content

Commit 1e53bed

Browse files
authored
fix: allow to pre-register TranslationTopic translator functions (#2737)
1 parent f709eb6 commit 1e53bed

File tree

5 files changed

+117
-18
lines changed

5 files changed

+117
-18
lines changed

src/context/ComponentContext.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ export type ComponentContextValue = {
175175
/** 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) */
176176
ReactionsListModal?: React.ComponentType<ReactionsListModalProps>;
177177
RecordingPermissionDeniedNotification?: React.ComponentType<RecordingPermissionDeniedNotificationProps>;
178+
/** 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) */
178179
ReminderNotification?: React.ComponentType<ReminderNotificationProps>;
179180
/** 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) */
180181
Search?: React.ComponentType<SearchProps>;

src/i18n/TranslationBuilder/TranslationBuilder.ts

Lines changed: 56 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import type { i18n, TFunction } from 'i18next';
22

3+
type TopicName = string;
4+
type TranslatorName = string;
5+
36
export type Translator<O extends Record<string, unknown> = Record<string, unknown>> =
47
(params: { key: string; value: string; t: TFunction; options: O }) => string | null;
58

@@ -44,25 +47,45 @@ export type TranslationTopicConstructor = new (
4447

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

4857
constructor(private i18next: i18n) {}
4958

50-
registerTopic = (name: string, Topic: TranslationTopicConstructor) => {
51-
const topic = new Topic({ i18next: this.i18next });
52-
this.topics.set(name, topic);
53-
this.i18next.use({
54-
name,
55-
process: (value: string, key: string, options: Record<string, unknown>) => {
56-
const topic = this.topics.get(name);
57-
if (!topic) return value;
58-
return topic.translate(value, key, options);
59-
},
60-
type: 'postProcessor' as const,
61-
});
59+
registerTopic = (name: TopicName, Topic: TranslationTopicConstructor) => {
60+
let topic = this.topics.get(name);
61+
62+
if (!topic) {
63+
topic = new Topic({ i18next: this.i18next });
64+
this.topics.set(name, topic);
65+
this.i18next.use({
66+
name,
67+
process: (value: string, key: string, options: Record<string, unknown>) => {
68+
const topic = this.topics.get(name);
69+
if (!topic) return value;
70+
return topic.translate(value, key, options);
71+
},
72+
type: 'postProcessor' as const,
73+
});
74+
}
75+
76+
const additionalTranslatorsToRegister = this.translatorRegistrationsBuffer[name];
77+
if (additionalTranslatorsToRegister) {
78+
Object.entries(additionalTranslatorsToRegister).forEach(
79+
([translatorName, translator]) => {
80+
topic.setTranslator(translatorName, translator);
81+
},
82+
);
83+
delete this.translatorRegistrationsBuffer[name];
84+
}
6285
return topic;
6386
};
6487

65-
disableTopic = (topicName: string) => {
88+
disableTopic = (topicName: TopicName) => {
6689
const topic = this.topics.get(topicName);
6790
if (!topic) return;
6891
this.i18next.use({
@@ -73,18 +96,34 @@ export class TranslationBuilder {
7396
this.topics.delete(topicName);
7497
};
7598

76-
getTopic = (topicName: string) => this.topics.get(topicName);
99+
getTopic = (topicName: TopicName) => this.topics.get(topicName);
77100

78-
registerTranslators(topicName: string, translators: Record<string, Translator>) {
101+
registerTranslators(
102+
topicName: TopicName,
103+
translators: Record<TranslatorName, Translator>,
104+
) {
79105
const topic = this.getTopic(topicName);
80-
if (!topic) return;
106+
if (!topic) {
107+
if (!this.translatorRegistrationsBuffer[topicName])
108+
this.translatorRegistrationsBuffer[topicName] = {};
109+
110+
Object.entries(translators).forEach(([translatorName, translator]) => {
111+
this.translatorRegistrationsBuffer[topicName][translatorName] = translator;
112+
});
113+
return;
114+
}
81115
Object.entries(translators).forEach(([name, translator]) => {
82116
topic.setTranslator(name, translator);
83117
});
84118
}
85119

86-
removeTranslators(topicName: string, translators: string[]) {
120+
removeTranslators(topicName: TopicName, translators: TranslatorName[]) {
87121
const topic = this.getTopic(topicName);
122+
if (this.translatorRegistrationsBuffer[topicName]) {
123+
translators.forEach((translatorName) => {
124+
delete this.translatorRegistrationsBuffer[topicName][translatorName];
125+
});
126+
}
88127
if (!topic) return;
89128
translators.forEach((name) => {
90129
topic.removeTranslator(name);
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export { NotificationTranslationTopic } from './NotificationTranslationTopic';
2+
export * from './types';

src/i18n/__tests__/TranslationBuilder.test.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,20 @@ describe('TranslationBuilder and TranslationTopic', () => {
66
const manager = new TranslationBuilder(mockI18Next);
77
expect(manager.i18next).toEqual(mockI18Next);
88
});
9+
910
it('registers and retrieves the builder', () => {
1011
const manager = new TranslationBuilder(mockI18Next);
1112
manager.registerTopic('notification', NotificationTranslationTopic);
1213
expect(manager.getTopic('notification')).toBeInstanceOf(NotificationTranslationTopic);
1314
});
15+
1416
it('removes builder', () => {
1517
const manager = new TranslationBuilder(mockI18Next);
1618
manager.registerTopic('notification', NotificationTranslationTopic);
1719
manager.disableTopic('notification');
1820
expect(manager.getTopic('notification')).toBeUndefined();
1921
});
22+
2023
it('registers and removes translators', () => {
2124
const translator = jest.fn();
2225
const manager = new TranslationBuilder(mockI18Next);
@@ -27,4 +30,59 @@ describe('TranslationBuilder and TranslationTopic', () => {
2730
manager.removeTranslators('notification', ['test']);
2831
expect(notificationBuilder.translators.get('test')).toBeUndefined();
2932
});
33+
34+
it('stores translators for non-existent topic in a buffer', () => {
35+
const manager = new TranslationBuilder(mockI18Next);
36+
const translators = { custom1: jest.fn(), custom2: jest.fn() };
37+
manager.registerTranslators('notification', translators);
38+
expect(manager.topics.size).toEqual(0);
39+
expect(manager.translatorRegistrationsBuffer.notification).toEqual(translators);
40+
});
41+
42+
it('removes translators from buffer on translation removal', () => {
43+
const manager = new TranslationBuilder(mockI18Next);
44+
const translators = { custom1: jest.fn(), custom2: jest.fn() };
45+
manager.registerTranslators('notification', translators);
46+
manager.removeTranslators('notification', ['custom1']);
47+
expect(Object.keys(manager.translatorRegistrationsBuffer.notification).length).toBe(
48+
1,
49+
);
50+
expect(manager.translatorRegistrationsBuffer.notification.custom2).toBeDefined();
51+
});
52+
53+
it('flushes the buffered translators on topic registration', () => {
54+
const manager = new TranslationBuilder(mockI18Next);
55+
const translators = { custom1: jest.fn(), custom2: jest.fn() };
56+
manager.registerTranslators('notification', translators);
57+
manager.registerTopic('notification', NotificationTranslationTopic);
58+
expect(manager.translatorRegistrationsBuffer.notification).toBeUndefined();
59+
});
60+
61+
it("overrides the topic's translators with buffered translators", () => {
62+
const manager = new TranslationBuilder(mockI18Next);
63+
const translator = jest.fn().mockImplementation();
64+
const translatorName = 'api:attachment:upload:failed';
65+
const translators = { [translatorName]: translator };
66+
manager.registerTranslators('notification', translators);
67+
manager.registerTopic('notification', NotificationTranslationTopic);
68+
manager
69+
.getTopic('notification')
70+
.translate('key', 'value', { notification: { type: translatorName } });
71+
72+
expect(translator).toHaveBeenCalledTimes(1);
73+
});
74+
75+
it('reuses the already registered topic on repeated registerTopic calls', () => {
76+
const manager = new TranslationBuilder(mockI18Next);
77+
class Topic {
78+
constructor() {
79+
this.id = Math.random().toString();
80+
}
81+
}
82+
manager.registerTopic('custom', Topic);
83+
const firstRegistrationId = manager.getTopic('custom').id;
84+
manager.registerTopic('custom', Topic);
85+
const secondRegistrationId = manager.getTopic('custom').id;
86+
expect(firstRegistrationId).toBe(secondRegistrationId);
87+
});
3088
});

src/i18n/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export * from './translations';
22
export * from './Streami18n';
3-
export * from './TranslationBuilder/TranslationBuilder';
3+
export * from './TranslationBuilder';
44
export {
55
defaultDateTimeParser,
66
defaultTranslatorFunction,

0 commit comments

Comments
 (0)