nucleus-core-ts 0.9.914 → 0.9.915

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/dist/.build-ok +1 -1
  2. package/dist/fe/components/ChatPanel/components/ChatPanel.js +4 -4
  3. package/dist/fe/components/ChatPanel/components/ConversationList.js +3 -3
  4. package/dist/fe/components/ChatPanel/components/MessageComposer.js +1 -1
  5. package/dist/fe/components/ChatPanel/components/MessageThread.js +18 -17
  6. package/dist/fe/components/ChatPanel/helpers.d.ts +23 -3
  7. package/dist/fe/components/ChatPanel/helpers.js +31 -13
  8. package/dist/fe/components/ChatPanel/hooks/useChat.js +16 -11
  9. package/dist/fe/components/ChatPanel/labels.d.ts +69 -0
  10. package/dist/fe/components/ChatPanel/labels.js +33 -1
  11. package/dist/fe/components/NucleusTextInput/components/NucleusTextInput.d.ts +1 -1
  12. package/dist/fe/components/NucleusTextInput/components/NucleusTextInput.js +3 -2
  13. package/dist/fe/components/NucleusTextInput/components/PasswordStrengthIndicator.d.ts +19 -2
  14. package/dist/fe/components/NucleusTextInput/components/PasswordStrengthIndicator.js +16 -7
  15. package/dist/fe/components/NucleusTextInput/types/index.d.ts +8 -0
  16. package/dist/fe/components/ResetPasswordPage/components/ResetPasswordForm.js +5 -5
  17. package/dist/fe/components/ResetPasswordPage/components/ResetPasswordPage.js +3 -3
  18. package/dist/fe/components/ResetPasswordPage/labels.d.ts +10 -0
  19. package/dist/fe/components/ResetPasswordPage/labels.js +6 -0
  20. package/dist/fe/components/SetPasswordPage/components/PasswordStrengthIndicator.d.ts +4 -1
  21. package/dist/fe/components/SetPasswordPage/components/PasswordStrengthIndicator.js +13 -8
  22. package/dist/fe/components/SetPasswordPage/components/SetPasswordForm.js +10 -10
  23. package/dist/fe/components/SetPasswordPage/components/SetPasswordPage.js +4 -4
  24. package/dist/fe/components/SetPasswordPage/labels.d.ts +35 -0
  25. package/dist/fe/components/SetPasswordPage/labels.js +19 -1
  26. package/package.json +1 -1
package/dist/.build-ok CHANGED
@@ -1 +1 @@
1
- 0.9.914
1
+ 0.9.915
@@ -46,11 +46,11 @@ export function ChatPanel(props) {
46
46
  }).then((res)=>res.json()).then((data)=>{
47
47
  if (data?.data) {
48
48
  store.addMessage(data.data);
49
- store.bumpConversationPreview(conversationId, data.data.content ?? 'Attachment', data.data.createdAt);
49
+ store.bumpConversationPreview(conversationId, data.data.content ?? say.attachmentPreview, data.data.createdAt);
50
50
  }
51
51
  store.setSending(false);
52
52
  }).catch(()=>{
53
- store.setError('Failed to upload attachment');
53
+ store.setError(say.uploadFailed);
54
54
  store.setSending(false);
55
55
  });
56
56
  };
@@ -65,7 +65,7 @@ export function ChatPanel(props) {
65
65
  children: [
66
66
  /*#__PURE__*/ _jsx("span", {
67
67
  className: theme.sidebar.title,
68
- children: props.title ?? 'Messages'
68
+ children: props.title ?? say.panelTitle
69
69
  }),
70
70
  (props.directory || props.onNewConversation) && /*#__PURE__*/ _jsx("button", {
71
71
  type: "button",
@@ -79,7 +79,7 @@ export function ChatPanel(props) {
79
79
  stroke: "currentColor",
80
80
  children: [
81
81
  /*#__PURE__*/ _jsx("title", {
82
- children: "New"
82
+ children: say.newConversationIconTitle
83
83
  }),
84
84
  /*#__PURE__*/ _jsx("path", {
85
85
  strokeLinecap: "round",
@@ -23,7 +23,7 @@ export function ConversationList({ conversations, activeId, currentUserId, isLoa
23
23
  return /*#__PURE__*/ _jsx("div", {
24
24
  className: theme.sidebar.list,
25
25
  children: conversations.map((conversation)=>{
26
- const title = conversationTitle(conversation, currentUserId, resolveUserName);
26
+ const title = conversationTitle(conversation, currentUserId, say, resolveUserName);
27
27
  const unread = conversation.myParticipant?.unreadCount ?? conversation.unreadCount;
28
28
  const otherId = otherParticipantId(conversation, currentUserId);
29
29
  const avatarUrl = conversation.avatarUrl ?? (otherId ? resolveUserAvatar?.(otherId) : undefined);
@@ -52,7 +52,7 @@ export function ConversationList({ conversations, activeId, currentUserId, isLoa
52
52
  }),
53
53
  /*#__PURE__*/ _jsx("span", {
54
54
  className: theme.conversation.time,
55
- children: formatRelativeTime(conversation.lastMessageAt)
55
+ children: formatRelativeTime(conversation.lastMessageAt, say)
56
56
  })
57
57
  ]
58
58
  }),
@@ -61,7 +61,7 @@ export function ConversationList({ conversations, activeId, currentUserId, isLoa
61
61
  children: [
62
62
  /*#__PURE__*/ _jsx("span", {
63
63
  className: theme.conversation.preview,
64
- children: conversation.lastMessagePreview || 'No messages yet'
64
+ children: conversation.lastMessagePreview || say.noMessagesPreview
65
65
  }),
66
66
  unread > 0 && /*#__PURE__*/ _jsx("span", {
67
67
  className: theme.conversation.badge,
@@ -103,7 +103,7 @@ export function MessageComposer({ theme, value, disabled, uploadEnabled, onChang
103
103
  className: theme.composer.sendButton,
104
104
  disabled: !canSend,
105
105
  onClick: submit,
106
- children: "Send"
106
+ children: say.send
107
107
  })
108
108
  ]
109
109
  })
@@ -4,20 +4,20 @@ import { useEffect, useRef } from 'react';
4
4
  import { cn } from '../../../utils/cn';
5
5
  import { conversationTitle, formatClockTime } from '../helpers';
6
6
  import { DEFAULT_CHAT_LABELS } from '../labels';
7
- function Attachments({ message, theme }) {
7
+ function Attachments({ message, theme, say }) {
8
8
  if (message.attachments.length === 0) return null;
9
9
  return /*#__PURE__*/ _jsx("span", {
10
10
  className: theme.attachment.grid,
11
11
  children: message.attachments.map((attachment)=>attachment.kind === 'image' ? /*#__PURE__*/ _jsx("img", {
12
12
  src: attachment.url,
13
- alt: attachment.originalName ?? 'image',
13
+ alt: attachment.originalName ?? say.imageAttachment,
14
14
  className: theme.attachment.image
15
15
  }, attachment.id) : /*#__PURE__*/ _jsx("a", {
16
16
  href: attachment.url,
17
17
  target: "_blank",
18
18
  rel: "noreferrer",
19
19
  className: theme.attachment.file,
20
- children: attachment.originalName ?? 'Download file'
20
+ children: attachment.originalName ?? say.downloadFile
21
21
  }, attachment.id))
22
22
  });
23
23
  }
@@ -40,16 +40,19 @@ export function MessageThread({ conversation, messages, currentUserId, typing, h
40
40
  children: say.selectConversation
41
41
  });
42
42
  }
43
- const title = conversationTitle(conversation, currentUserId, resolveUserName);
43
+ const title = conversationTitle(conversation, currentUserId, say, resolveUserName);
44
44
  const activeParticipants = conversation.participants.filter((p)=>!p.leftAt);
45
- const subtitle = conversation.type === 'group' ? `${activeParticipants.length} participants` : undefined;
45
+ const subtitle = conversation.type === 'group' ? say.participantCount(activeParticipants.length) : undefined;
46
46
  const otherReaders = conversation.participants.filter((p)=>p.userId !== currentUserId && !p.leftAt);
47
47
  const isReadByOthers = (message)=>{
48
48
  const created = Date.parse(message.createdAt);
49
49
  return otherReaders.some((p)=>p.lastReadAt != null && Date.parse(p.lastReadAt) >= created);
50
50
  };
51
51
  const typingUsers = typing.filter((t)=>t.conversationId === conversation.id && Date.now() - t.at < 6000);
52
- const typingLabel = typingUsers.length === 1 ? `${resolveUserName?.(typingUsers[0]?.userId ?? '') ?? 'Someone'} is typing` : 'Several people are typing';
52
+ // The whole sentence comes from one label, ellipsis included. Building it as
53
+ // `${name} is typing` + '...' would leave "is typing" untranslatable, and in
54
+ // Turkish the verb goes after the name anyway.
55
+ const typingLabel = typingUsers.length === 1 ? say.typingOne(resolveUserName?.(typingUsers[0]?.userId ?? '') ?? say.someone) : say.typingMany;
53
56
  return /*#__PURE__*/ _jsxs("div", {
54
57
  className: theme.thread.base,
55
58
  children: [
@@ -76,7 +79,7 @@ export function MessageThread({ conversation, messages, currentUserId, typing, h
76
79
  type: "button",
77
80
  className: theme.thread.loadMore,
78
81
  onClick: onLoadOlder,
79
- children: isLoading ? 'Loading...' : 'Load earlier messages'
82
+ children: isLoading ? say.loadingMessages : say.loadEarlier
80
83
  }),
81
84
  messages.length === 0 && !isLoading ? /*#__PURE__*/ _jsx("div", {
82
85
  className: theme.thread.empty,
@@ -90,7 +93,7 @@ export function MessageThread({ conversation, messages, currentUserId, typing, h
90
93
  children: [
91
94
  conversation.type === 'group' && !own && message.senderId && /*#__PURE__*/ _jsx("div", {
92
95
  className: theme.message.sender,
93
- children: resolveUserName?.(message.senderId) ?? 'User'
96
+ children: resolveUserName?.(message.senderId) ?? say.unknownSender
94
97
  }),
95
98
  message.deletedAt ? /*#__PURE__*/ _jsx("span", {
96
99
  className: theme.message.deleted,
@@ -102,7 +105,8 @@ export function MessageThread({ conversation, messages, currentUserId, typing, h
102
105
  }),
103
106
  /*#__PURE__*/ _jsx(Attachments, {
104
107
  message: message,
105
- theme: theme
108
+ theme: theme,
109
+ say: say
106
110
  })
107
111
  ]
108
112
  }),
@@ -110,15 +114,15 @@ export function MessageThread({ conversation, messages, currentUserId, typing, h
110
114
  className: theme.message.meta,
111
115
  children: [
112
116
  /*#__PURE__*/ _jsx("span", {
113
- children: formatClockTime(message.createdAt)
117
+ children: formatClockTime(message.createdAt, say)
114
118
  }),
115
119
  message.editedAt && /*#__PURE__*/ _jsx("span", {
116
120
  className: theme.message.edited,
117
- children: "· edited"
121
+ children: say.edited
118
122
  }),
119
123
  own && !message.deletedAt && /*#__PURE__*/ _jsx("span", {
120
124
  className: cn(theme.message.receipt, isReadByOthers(message) && theme.message.receiptRead),
121
- title: isReadByOthers(message) ? 'Görüldü' : 'Gönderildi',
125
+ title: isReadByOthers(message) ? say.receiptRead : say.receiptSent,
122
126
  children: isReadByOthers(message) ? '✓✓' : '✓'
123
127
  })
124
128
  ]
@@ -129,12 +133,9 @@ export function MessageThread({ conversation, messages, currentUserId, typing, h
129
133
  })
130
134
  ]
131
135
  }),
132
- typingUsers.length > 0 && /*#__PURE__*/ _jsxs("div", {
136
+ typingUsers.length > 0 && /*#__PURE__*/ _jsx("div", {
133
137
  className: theme.typing,
134
- children: [
135
- typingLabel,
136
- "..."
137
- ]
138
+ children: typingLabel
138
139
  })
139
140
  ]
140
141
  });
@@ -1,6 +1,26 @@
1
+ import type { ChatPanelLabels } from './labels';
1
2
  import type { ChatConversationDTO } from './types';
2
3
  export declare function otherParticipantId(conversation: ChatConversationDTO, currentUserId: string): string | null;
3
- export declare function conversationTitle(conversation: ChatConversationDTO, currentUserId: string, resolveUserName?: (userId: string) => string | undefined): string;
4
+ export declare function conversationTitle(conversation: ChatConversationDTO, currentUserId: string, labels: ChatPanelLabels, resolveUserName?: (userId: string) => string | undefined): string;
4
5
  export declare function initialsOf(name: string): string;
5
- export declare function formatRelativeTime(iso: string | null): string;
6
- export declare function formatClockTime(iso: string): string;
6
+ /**
7
+ * The age of a message, in the panel's own words.
8
+ *
9
+ * The units used to be English letters written into the source (`5m`, `3h`,
10
+ * `2d`), which no installation could change, and the fallback past a week was a
11
+ * bare `toLocaleDateString()` — no locale, so the string came out in the
12
+ * SERVER's locale during the server render and the BROWSER's after hydration.
13
+ * The same row rendered twice, differently, and React tore. Both halves now
14
+ * come from the labels: the units as functions of the number, the date under
15
+ * the named `locale`.
16
+ */
17
+ export declare function formatRelativeTime(iso: string | null, labels: ChatPanelLabels): string;
18
+ /**
19
+ * The clock beneath a message.
20
+ *
21
+ * `hour12` is pinned as well as the locale: left to the environment, a locale
22
+ * that prints "2:32 PM" on one machine prints "14:32" on the other, which is
23
+ * the same hydration tear by a different route. A 24-hour clock is the one both
24
+ * ends can agree on without asking anybody.
25
+ */
26
+ export declare function formatClockTime(iso: string, labels: ChatPanelLabels): string;
@@ -3,33 +3,51 @@ export function otherParticipantId(conversation, currentUserId) {
3
3
  const other = conversation.participants.find((p)=>p.userId !== currentUserId);
4
4
  return other?.userId ?? null;
5
5
  }
6
- export function conversationTitle(conversation, currentUserId, resolveUserName) {
7
- if (conversation.type === 'group') return conversation.title || 'Group chat';
6
+ export function conversationTitle(conversation, currentUserId, labels, resolveUserName) {
7
+ if (conversation.type === 'group') return conversation.title || labels.groupChat;
8
8
  const otherId = otherParticipantId(conversation, currentUserId);
9
- if (otherId) return resolveUserName?.(otherId) || 'Direct message';
10
- return conversation.title || 'Direct message';
9
+ if (otherId) return resolveUserName?.(otherId) || labels.directMessage;
10
+ return conversation.title || labels.directMessage;
11
11
  }
12
12
  export function initialsOf(name) {
13
13
  const parts = name.trim().split(/\s+/).slice(0, 2);
14
14
  const initials = parts.map((p)=>p[0]?.toUpperCase() ?? '').join('');
15
15
  return initials || '?';
16
16
  }
17
- export function formatRelativeTime(iso) {
17
+ /**
18
+ * The age of a message, in the panel's own words.
19
+ *
20
+ * The units used to be English letters written into the source (`5m`, `3h`,
21
+ * `2d`), which no installation could change, and the fallback past a week was a
22
+ * bare `toLocaleDateString()` — no locale, so the string came out in the
23
+ * SERVER's locale during the server render and the BROWSER's after hydration.
24
+ * The same row rendered twice, differently, and React tore. Both halves now
25
+ * come from the labels: the units as functions of the number, the date under
26
+ * the named `locale`.
27
+ */ export function formatRelativeTime(iso, labels) {
18
28
  if (!iso) return '';
19
29
  const date = new Date(iso);
20
30
  const diffMs = Date.now() - date.getTime();
21
31
  const mins = Math.floor(diffMs / 60000);
22
32
  const hours = Math.floor(diffMs / 3600000);
23
33
  const days = Math.floor(diffMs / 86400000);
24
- if (mins < 1) return 'now';
25
- if (mins < 60) return `${mins}m`;
26
- if (hours < 24) return `${hours}h`;
27
- if (days < 7) return `${days}d`;
28
- return date.toLocaleDateString();
34
+ if (mins < 1) return labels.justNow;
35
+ if (mins < 60) return labels.minutesAgo(mins);
36
+ if (hours < 24) return labels.hoursAgo(hours);
37
+ if (days < 7) return labels.daysAgo(days);
38
+ return date.toLocaleDateString(labels.locale);
29
39
  }
30
- export function formatClockTime(iso) {
31
- return new Date(iso).toLocaleTimeString([], {
40
+ /**
41
+ * The clock beneath a message.
42
+ *
43
+ * `hour12` is pinned as well as the locale: left to the environment, a locale
44
+ * that prints "2:32 PM" on one machine prints "14:32" on the other, which is
45
+ * the same hydration tear by a different route. A 24-hour clock is the one both
46
+ * ends can agree on without asking anybody.
47
+ */ export function formatClockTime(iso, labels) {
48
+ return new Date(iso).toLocaleTimeString(labels.locale, {
32
49
  hour: '2-digit',
33
- minute: '2-digit'
50
+ minute: '2-digit',
51
+ hour12: false
34
52
  });
35
53
  }
@@ -1,23 +1,28 @@
1
1
  'use client';
2
2
  import { useEffect, useEffectEvent, useRef } from 'react';
3
3
  import { usePubSub } from '../../../../src/Client/PubSub';
4
+ import { DEFAULT_CHAT_LABELS } from '../labels';
4
5
  import { useChatStore } from '../store';
5
6
  const TYPING_THROTTLE_MS = 2500;
6
- function errMsg(error) {
7
- return error instanceof Error ? error.message : 'Something went wrong';
7
+ function errMsg(error, fallback) {
8
+ return error instanceof Error ? error.message : fallback;
8
9
  }
9
- function previewFor(message) {
10
+ function previewFor(message, say) {
10
11
  if (message.content?.trim()) {
11
12
  const trimmed = message.content.trim();
12
13
  return trimmed.length > 120 ? `${trimmed.slice(0, 117)}...` : trimmed;
13
14
  }
14
15
  if (message.attachments.length > 0) {
15
- return message.attachments.length > 1 ? `${message.attachments.length} attachments` : 'Attachment';
16
+ return message.attachments.length > 1 ? say.attachmentsPreview(message.attachments.length) : say.attachmentPreview;
16
17
  }
17
18
  return '';
18
19
  }
19
20
  export function useChat(props) {
20
21
  const { currentUserId, actions, topic = 'chat', wsUrl, wsPath } = props;
22
+ const say = {
23
+ ...DEFAULT_CHAT_LABELS,
24
+ ...props.labels
25
+ };
21
26
  const store = useChatStore();
22
27
  const lastSeenEventIdRef = useRef(null);
23
28
  const eventsInitializedRef = useRef(false);
@@ -43,7 +48,7 @@ export function useChat(props) {
43
48
  store.setLoadingConversations(false);
44
49
  },
45
50
  onErrorHandle: (error)=>{
46
- store.setError(errMsg(error));
51
+ store.setError(errMsg(error, say.genericError));
47
52
  store.setLoadingConversations(false);
48
53
  }
49
54
  });
@@ -89,7 +94,7 @@ export function useChat(props) {
89
94
  markReadNow(conversationId);
90
95
  },
91
96
  onErrorHandle: (error)=>{
92
- store.setError(errMsg(error));
97
+ store.setError(errMsg(error, say.genericError));
93
98
  store.setLoadingMessages(false);
94
99
  }
95
100
  });
@@ -151,12 +156,12 @@ export function useChat(props) {
151
156
  onAfterHandle: (data)=>{
152
157
  if (data?.data) {
153
158
  store.addMessage(data.data);
154
- store.bumpConversationPreview(active, previewFor(data.data), data.data.createdAt);
159
+ store.bumpConversationPreview(active, previewFor(data.data, say), data.data.createdAt);
155
160
  }
156
161
  store.setSending(false);
157
162
  },
158
163
  onErrorHandle: (error)=>{
159
- store.setError(errMsg(error));
164
+ store.setError(errMsg(error, say.genericError));
160
165
  store.setSending(false);
161
166
  }
162
167
  });
@@ -188,7 +193,7 @@ export function useChat(props) {
188
193
  openConversation(data.data.id);
189
194
  }
190
195
  },
191
- onErrorHandle: (error)=>store.setError(errMsg(error))
196
+ onErrorHandle: (error)=>store.setError(errMsg(error, say.genericError))
192
197
  });
193
198
  });
194
199
  const createGroup = useEffectEvent((title, participantIds)=>{
@@ -204,7 +209,7 @@ export function useChat(props) {
204
209
  openConversation(data.data.id);
205
210
  }
206
211
  },
207
- onErrorHandle: (error)=>store.setError(errMsg(error))
212
+ onErrorHandle: (error)=>store.setError(errMsg(error, say.genericError))
208
213
  });
209
214
  });
210
215
  const handleRealtime = useEffectEvent((payload)=>{
@@ -215,7 +220,7 @@ export function useChat(props) {
215
220
  const message = payload.message;
216
221
  const conversationId = payload.conversationId;
217
222
  if (!message || !conversationId) return;
218
- store.bumpConversationPreview(conversationId, previewFor(message), message.createdAt);
223
+ store.bumpConversationPreview(conversationId, previewFor(message, say), message.createdAt);
219
224
  store.clearTyping(conversationId, message.senderId ?? '');
220
225
  if (conversationId === active) {
221
226
  store.addMessage(message);
@@ -9,6 +9,11 @@
9
9
  * each other.
10
10
  *
11
11
  * Defaults stay English, so nothing that does not pass labels changes.
12
+ *
13
+ * Anything that interpolates a value is a FUNCTION of that value, never a
14
+ * translated fragment glued to an untranslated one: "3 participants" is
15
+ * "3 katılımcı" in Turkish but "typing" moves to the end of its sentence, and
16
+ * a concatenation would produce a sentence in neither language.
12
17
  */
13
18
  export type ChatPanelLabels = {
14
19
  /** While the conversation list is still arriving. */
@@ -36,5 +41,69 @@ export type ChatPanelLabels = {
36
41
  newMessageTitle: string;
37
42
  searchPeople: string;
38
43
  noPeopleFound: string;
44
+ /** The sidebar heading, when the consumer passes no `title` prop. */
45
+ panelTitle: string;
46
+ /** The accessible name of the plus icon inside the "new conversation" button. */
47
+ newConversationIconTitle: string;
48
+ /** The send button. */
49
+ send: string;
50
+ /** A conversation row whose last message does not exist yet. */
51
+ noMessagesPreview: string;
52
+ /** A group with no title of its own. */
53
+ groupChat: string;
54
+ /** A one-to-one conversation whose other person cannot be named. */
55
+ directMessage: string;
56
+ /** A sender whose name the host could not resolve. */
57
+ unknownSender: string;
58
+ /** The group thread's subtitle. */
59
+ participantCount: (count: number) => string;
60
+ /** The load-older button, while the page is in flight. */
61
+ loadingMessages: string;
62
+ /** The load-older button, at rest. */
63
+ loadEarlier: string;
64
+ /** One named person is typing. Carries its own trailing ellipsis. */
65
+ typingOne: (name: string) => string;
66
+ /** More than one person is typing. Carries its own trailing ellipsis. */
67
+ typingMany: string;
68
+ /** Stands in for a typist whose name the host could not resolve. */
69
+ someone: string;
70
+ /** Marks a message its sender changed after sending. */
71
+ edited: string;
72
+ /** The delivery tick's tooltip, once somebody else has read the message. */
73
+ receiptRead: string;
74
+ /** The delivery tick's tooltip, before anybody has read it. */
75
+ receiptSent: string;
76
+ /** Alt text for an image attachment that arrived without a file name. */
77
+ imageAttachment: string;
78
+ /** The link on a non-image attachment that arrived without a file name. */
79
+ downloadFile: string;
80
+ /** A conversation row previewing a message that is only a file. */
81
+ attachmentPreview: string;
82
+ /** A conversation row previewing a message that is only files. */
83
+ attachmentsPreview: (count: number) => string;
84
+ /** Shown when an attachment upload fails. */
85
+ uploadFailed: string;
86
+ /** Shown when a request fails without saying why. */
87
+ genericError: string;
88
+ /**
89
+ * The relative stamps on conversation rows. Kept short on purpose — they sit
90
+ * in a column beside a name that must not be pushed out of the row.
91
+ */
92
+ justNow: string;
93
+ minutesAgo: (minutes: number) => string;
94
+ hoursAgo: (hours: number) => string;
95
+ daysAgo: (days: number) => string;
96
+ /**
97
+ * The BCP-47 tag the panel formats dates and clock times with.
98
+ *
99
+ * It is a label rather than a guess because the guess tore: calling
100
+ * `toLocaleDateString()` with no argument reads the SERVER's locale while the
101
+ * page renders on the server and the BROWSER's once React takes over, so one
102
+ * message produced two different strings and hydration mismatched. Naming the
103
+ * locale makes both passes agree. Clock times are additionally forced to
104
+ * `hour12: false` for the same reason — whether a locale prints "14:32" or
105
+ * "2:32 PM" must not depend on which machine did the printing.
106
+ */
107
+ locale: string;
39
108
  };
40
109
  export declare const DEFAULT_CHAT_LABELS: ChatPanelLabels;
@@ -9,6 +9,11 @@
9
9
  * each other.
10
10
  *
11
11
  * Defaults stay English, so nothing that does not pass labels changes.
12
+ *
13
+ * Anything that interpolates a value is a FUNCTION of that value, never a
14
+ * translated fragment glued to an untranslated one: "3 participants" is
15
+ * "3 katılımcı" in Turkish but "typing" moves to the end of its sentence, and
16
+ * a concatenation would produce a sentence in neither language.
12
17
  */ export const DEFAULT_CHAT_LABELS = {
13
18
  loadingConversations: 'Loading conversations...',
14
19
  noConversations: 'No conversations yet',
@@ -23,5 +28,32 @@
23
28
  close: 'Close',
24
29
  newMessageTitle: 'New message',
25
30
  searchPeople: 'Search people...',
26
- noPeopleFound: 'No people found'
31
+ noPeopleFound: 'No people found',
32
+ panelTitle: 'Messages',
33
+ newConversationIconTitle: 'New',
34
+ send: 'Send',
35
+ noMessagesPreview: 'No messages yet',
36
+ groupChat: 'Group chat',
37
+ directMessage: 'Direct message',
38
+ unknownSender: 'User',
39
+ participantCount: (count)=>`${count} participants`,
40
+ loadingMessages: 'Loading...',
41
+ loadEarlier: 'Load earlier messages',
42
+ typingOne: (name)=>`${name} is typing...`,
43
+ typingMany: 'Several people are typing...',
44
+ someone: 'Someone',
45
+ edited: '· edited',
46
+ receiptRead: 'Read',
47
+ receiptSent: 'Sent',
48
+ imageAttachment: 'Image',
49
+ downloadFile: 'Download file',
50
+ attachmentPreview: 'Attachment',
51
+ attachmentsPreview: (count)=>`${count} attachments`,
52
+ uploadFailed: 'Failed to upload attachment',
53
+ genericError: 'Something went wrong',
54
+ justNow: 'now',
55
+ minutesAgo: (minutes)=>`${minutes}m`,
56
+ hoursAgo: (hours)=>`${hours}h`,
57
+ daysAgo: (days)=>`${days}d`,
58
+ locale: 'en-US'
27
59
  };
@@ -1,2 +1,2 @@
1
1
  import type { NucleusTextInputProps } from '../types';
2
- export declare function NucleusTextInput<T extends string = string>({ value, onChange, type, label, placeholder, helperText, errorMessage, disabled, readOnly, leftIcon, rightIcon, size, fullWidth, className, inputClassName, labelClassName, wrapperClassName, showTypewriterError, showTypeIcon, showValidationIcon, enableValidation, validateOnBlur, validateOnChange, validationConfig, customValidator, onValidationChange, unit, unitPosition, formatNumber, thousandSeparator, phoneFormat, isNewPassword, maxInputLength, confirmValue, onConfirmChange, showPasswordStrength, preventCopy, preventPaste, preventContextMenu, trimOnBlur, onFocus, onBlur, ...restProps }: NucleusTextInputProps<T>): import("react/jsx-runtime").JSX.Element;
2
+ export declare function NucleusTextInput<T extends string = string>({ value, onChange, type, label, placeholder, helperText, errorMessage, disabled, readOnly, leftIcon, rightIcon, size, fullWidth, className, inputClassName, labelClassName, wrapperClassName, showTypewriterError, showTypeIcon, showValidationIcon, enableValidation, validateOnBlur, validateOnChange, validationConfig, customValidator, onValidationChange, unit, unitPosition, formatNumber, thousandSeparator, phoneFormat, isNewPassword, maxInputLength, confirmValue, onConfirmChange, showPasswordStrength, passwordStrengthWords, preventCopy, preventPaste, preventContextMenu, trimOnBlur, onFocus, onBlur, ...restProps }: NucleusTextInputProps<T>): import("react/jsx-runtime").JSX.Element;
@@ -20,7 +20,7 @@ const TYPE_ICONS = {
20
20
  tel: /*#__PURE__*/ _jsx(PhoneIcon, {}),
21
21
  url: /*#__PURE__*/ _jsx(LinkIcon, {})
22
22
  };
23
- export function NucleusTextInput({ value = '', onChange, type = 'text', label, placeholder, helperText, errorMessage, disabled = false, readOnly = false, leftIcon, rightIcon, size = 'md', fullWidth = true, className, inputClassName, labelClassName, wrapperClassName, showTypewriterError = true, showTypeIcon = true, showValidationIcon = true, enableValidation = true, validateOnBlur = true, validateOnChange = false, validationConfig, customValidator, onValidationChange, unit, unitPosition = 'suffix', formatNumber = false, thousandSeparator = ' ', phoneFormat = 'tr', isNewPassword = false, maxInputLength, confirmValue, onConfirmChange, showPasswordStrength = false, preventCopy = false, preventPaste = false, preventContextMenu = false, trimOnBlur = false, onFocus, onBlur, ...restProps }) {
23
+ export function NucleusTextInput({ value = '', onChange, type = 'text', label, placeholder, helperText, errorMessage, disabled = false, readOnly = false, leftIcon, rightIcon, size = 'md', fullWidth = true, className, inputClassName, labelClassName, wrapperClassName, showTypewriterError = true, showTypeIcon = true, showValidationIcon = true, enableValidation = true, validateOnBlur = true, validateOnChange = false, validationConfig, customValidator, onValidationChange, unit, unitPosition = 'suffix', formatNumber = false, thousandSeparator = ' ', phoneFormat = 'tr', isNewPassword = false, maxInputLength, confirmValue, onConfirmChange, showPasswordStrength = false, passwordStrengthWords, preventCopy = false, preventPaste = false, preventContextMenu = false, trimOnBlur = false, onFocus, onBlur, ...restProps }) {
24
24
  const [isFocused, setIsFocused] = useState(false);
25
25
  const [isHovered, setIsHovered] = useState(false);
26
26
  const [showPassword, setShowPassword] = useState(false);
@@ -314,7 +314,8 @@ export function NucleusTextInput({ value = '', onChange, type = 'text', label, p
314
314
  }) : null
315
315
  }),
316
316
  type === 'password' && showPasswordStrength && value && /*#__PURE__*/ _jsx(PasswordStrengthIndicator, {
317
- password: value
317
+ password: value,
318
+ words: passwordStrengthWords
318
319
  })
319
320
  ]
320
321
  });
@@ -1,6 +1,23 @@
1
+ /**
2
+ * The four words this meter says, so a portal can say them in its own language.
3
+ *
4
+ * They were literals — "Weak", "Fair", "Good", "Strong" — printed under the
5
+ * password box on the screen every employee meets when they set a password. The
6
+ * page around it was already translatable; this one component underneath was
7
+ * not, so a Turkish install showed a Turkish label, a Turkish hint, and an
8
+ * English verdict between them.
9
+ */
10
+ export type PasswordStrengthWords = {
11
+ weak: string;
12
+ fair: string;
13
+ good: string;
14
+ strong: string;
15
+ };
1
16
  interface PasswordStrengthIndicatorProps {
2
17
  password: string;
3
18
  className?: string;
19
+ /** Omit to keep the English defaults, so no existing caller changes. */
20
+ words?: Partial<PasswordStrengthWords>;
4
21
  }
5
22
  type StrengthLevel = 'weak' | 'fair' | 'good' | 'strong';
6
23
  interface StrengthResult {
@@ -9,6 +26,6 @@ interface StrengthResult {
9
26
  label: string;
10
27
  color: string;
11
28
  }
12
- declare function calculateStrength(password: string): StrengthResult;
13
- export declare function PasswordStrengthIndicator({ password, className }: PasswordStrengthIndicatorProps): import("react/jsx-runtime").JSX.Element | null;
29
+ declare function calculateStrength(password: string, words: PasswordStrengthWords): StrengthResult;
30
+ export declare function PasswordStrengthIndicator({ password, className, words, }: PasswordStrengthIndicatorProps): import("react/jsx-runtime").JSX.Element | null;
14
31
  export { calculateStrength, type StrengthLevel, type StrengthResult };
@@ -5,7 +5,13 @@ import gsap from 'gsap';
5
5
  import { useRef } from 'react';
6
6
  import { cn } from '../utils/cn';
7
7
  gsap.registerPlugin(useGSAP);
8
- function calculateStrength(password) {
8
+ const DEFAULT_STRENGTH_WORDS = {
9
+ weak: 'Weak',
10
+ fair: 'Fair',
11
+ good: 'Good',
12
+ strong: 'Strong'
13
+ };
14
+ function calculateStrength(password, words) {
9
15
  let score = 0;
10
16
  if (!password) {
11
17
  return {
@@ -30,7 +36,7 @@ function calculateStrength(password) {
30
36
  return {
31
37
  level: 'weak',
32
38
  score,
33
- label: 'Weak',
39
+ label: words.weak,
34
40
  color: 'bg-red-500'
35
41
  };
36
42
  }
@@ -38,7 +44,7 @@ function calculateStrength(password) {
38
44
  return {
39
45
  level: 'fair',
40
46
  score,
41
- label: 'Fair',
47
+ label: words.fair,
42
48
  color: 'bg-orange-500'
43
49
  };
44
50
  }
@@ -46,20 +52,23 @@ function calculateStrength(password) {
46
52
  return {
47
53
  level: 'good',
48
54
  score,
49
- label: 'Good',
55
+ label: words.good,
50
56
  color: 'bg-yellow-500'
51
57
  };
52
58
  }
53
59
  return {
54
60
  level: 'strong',
55
61
  score,
56
- label: 'Strong',
62
+ label: words.strong,
57
63
  color: 'bg-green-500'
58
64
  };
59
65
  }
60
- export function PasswordStrengthIndicator({ password, className }) {
66
+ export function PasswordStrengthIndicator({ password, className, words }) {
61
67
  const barRef = useRef(null);
62
- const strength = calculateStrength(password);
68
+ const strength = calculateStrength(password, {
69
+ ...DEFAULT_STRENGTH_WORDS,
70
+ ...words
71
+ });
63
72
  const percentage = password ? Math.min(strength.score / 7 * 100, 100) : 0;
64
73
  useGSAP(()=>{
65
74
  if (!barRef.current) return;
@@ -41,6 +41,14 @@ export interface NucleusTextInputProps<T extends string = string> extends Omit<I
41
41
  confirmValue?: string;
42
42
  onConfirmChange?: (value: string) => void;
43
43
  showPasswordStrength?: boolean;
44
+ /**
45
+ * The four words the strength meter says.
46
+ *
47
+ * Omit for the English defaults. Without this the meter printed "Weak" and
48
+ * "Strong" under a Turkish label on the one screen every employee meets, and
49
+ * the page around it had no way to reach them.
50
+ */
51
+ passwordStrengthWords?: Partial<import('../components/PasswordStrengthIndicator').PasswordStrengthWords>;
44
52
  preventCopy?: boolean;
45
53
  preventPaste?: boolean;
46
54
  preventContextMenu?: boolean;
@@ -88,7 +88,7 @@ export function ResetPasswordForm({ labels, token, resetPasswordAction, onBackTo
88
88
  const reqs = [
89
89
  {
90
90
  key: 'length',
91
- label: `At least ${policy.minLength} characters`,
91
+ label: say.ruleMinLength(policy.minLength),
92
92
  test: (p)=>p.length >= policy.minLength
93
93
  }
94
94
  ];
@@ -227,7 +227,7 @@ export function ResetPasswordForm({ labels, token, resetPasswordAction, onBackTo
227
227
  }),
228
228
  /*#__PURE__*/ _jsx("p", {
229
229
  className: theme.success.message,
230
- children: "Your password has been successfully reset. You can now log in with your new password."
230
+ children: say.successMessage
231
231
  }),
232
232
  /*#__PURE__*/ _jsx("div", {
233
233
  className: theme.links.container,
@@ -266,7 +266,7 @@ export function ResetPasswordForm({ labels, token, resetPasswordAction, onBackTo
266
266
  children: /*#__PURE__*/ _jsx(NucleusTextInput, {
267
267
  type: "password",
268
268
  label: say.newPassword,
269
- placeholder: "••••••••",
269
+ placeholder: say.passwordPlaceholder,
270
270
  value: store.newPassword,
271
271
  onChange: store.setNewPassword,
272
272
  disabled: isLoading,
@@ -285,7 +285,7 @@ export function ResetPasswordForm({ labels, token, resetPasswordAction, onBackTo
285
285
  children: /*#__PURE__*/ _jsx(NucleusTextInput, {
286
286
  type: "password",
287
287
  label: say.confirmPassword,
288
- placeholder: "••••••••",
288
+ placeholder: say.passwordPlaceholder,
289
289
  value: store.confirmPassword,
290
290
  onChange: store.setConfirmPassword,
291
291
  disabled: isLoading,
@@ -329,7 +329,7 @@ export function ResetPasswordForm({ labels, token, resetPasswordAction, onBackTo
329
329
  fullWidth: true,
330
330
  loading: isLoading,
331
331
  disabled: isLoading,
332
- children: "Reset Password"
332
+ children: say.submitButton
333
333
  })
334
334
  }),
335
335
  /*#__PURE__*/ _jsx("div", {
@@ -10,7 +10,7 @@ import { resetPasswordPageTheme } from '../theme';
10
10
  import { ResetPasswordForm } from './ResetPasswordForm';
11
11
  import { ResetPasswordHeader } from './ResetPasswordHeader';
12
12
  gsap.registerPlugin(useGSAP);
13
- export function ResetPasswordPage({ labels, logo, title = 'Reset Password', subtitle = 'Enter your new password below.', token, resetPasswordAction, onBackToLogin, backToLoginHref, showBackground = true, className, passwordPolicy }) {
13
+ export function ResetPasswordPage({ labels, logo, title, subtitle, token, resetPasswordAction, onBackToLogin, backToLoginHref, showBackground = true, className, passwordPolicy }) {
14
14
  const say = {
15
15
  ...DEFAULT_RESET_PASSWORD_LABELS,
16
16
  ...labels
@@ -48,8 +48,8 @@ export function ResetPasswordPage({ labels, logo, title = 'Reset Password', subt
48
48
  children: [
49
49
  /*#__PURE__*/ _jsx(ResetPasswordHeader, {
50
50
  logo: logo,
51
- title: title,
52
- subtitle: subtitle
51
+ title: title ?? say.title,
52
+ subtitle: subtitle ?? say.subtitle
53
53
  }),
54
54
  /*#__PURE__*/ _jsx(ResetPasswordForm, {
55
55
  labels: labels,
@@ -4,13 +4,23 @@
4
4
  * Defaults stay English, so nothing that does not pass labels changes.
5
5
  */
6
6
  export type ResetPasswordLabels = {
7
+ /** The heading and the line under it, used when the page is given no title/subtitle. */
8
+ title: string;
9
+ subtitle: string;
7
10
  newPassword: string;
8
11
  confirmPassword: string;
12
+ /** Shown inside both password boxes while they are empty. */
13
+ passwordPlaceholder: string;
9
14
  passwordRequirements: string;
15
+ /** What the button that submits the form says. */
16
+ submitButton: string;
10
17
  success: string;
18
+ /** The paragraph under the success heading. */
19
+ successMessage: string;
11
20
  backToLogin: string;
12
21
  pageLabel: string;
13
22
  /** The rules listed under the box, one line each. */
23
+ ruleMinLength: (count: number) => string;
14
24
  ruleUppercase: string;
15
25
  ruleLowercase: string;
16
26
  ruleNumber: string;
@@ -3,12 +3,18 @@
3
3
  *
4
4
  * Defaults stay English, so nothing that does not pass labels changes.
5
5
  */ export const DEFAULT_RESET_PASSWORD_LABELS = {
6
+ title: 'Reset Password',
7
+ subtitle: 'Enter your new password below.',
6
8
  newPassword: 'New Password',
7
9
  confirmPassword: 'Confirm Password',
10
+ passwordPlaceholder: '••••••••',
8
11
  passwordRequirements: 'Password requirements:',
12
+ submitButton: 'Reset Password',
9
13
  success: 'Password Reset Successful',
14
+ successMessage: 'Your password has been successfully reset. You can now log in with your new password.',
10
15
  backToLogin: 'Back to login',
11
16
  pageLabel: 'Reset password page',
17
+ ruleMinLength: (count)=>`At least ${count} characters`,
12
18
  ruleUppercase: 'One uppercase letter',
13
19
  ruleLowercase: 'One lowercase letter',
14
20
  ruleNumber: 'One number',
@@ -1,7 +1,10 @@
1
1
  import type { ReactElement } from 'react';
2
+ import { type SetPasswordLabels } from '../labels';
2
3
  import type { PasswordStrength } from '../types';
3
4
  type PasswordStrengthIndicatorProps = {
4
5
  strength: PasswordStrength;
6
+ /** Every word this indicator says, so a portal can say them in its own language. */
7
+ labels?: Partial<SetPasswordLabels>;
5
8
  showRequirements?: boolean;
6
9
  passwordMinLength?: number;
7
10
  requireUppercase?: boolean;
@@ -9,5 +12,5 @@ type PasswordStrengthIndicatorProps = {
9
12
  requireNumber?: boolean;
10
13
  requireSpecialChar?: boolean;
11
14
  };
12
- export declare function PasswordStrengthIndicator({ strength, showRequirements, passwordMinLength, requireUppercase, requireLowercase, requireNumber, requireSpecialChar, }: PasswordStrengthIndicatorProps): ReactElement;
15
+ export declare function PasswordStrengthIndicator({ strength, labels, showRequirements, passwordMinLength, requireUppercase, requireLowercase, requireNumber, requireSpecialChar, }: PasswordStrengthIndicatorProps): ReactElement;
13
16
  export {};
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { cn } from '../../../utils/cn';
4
+ import { DEFAULT_SET_PASSWORD_LABELS } from '../labels';
4
5
  import { setPasswordPageTheme } from '../theme';
5
6
  const CheckIcon = ()=>/*#__PURE__*/ _jsx("svg", {
6
7
  className: "w-3 h-3",
@@ -24,7 +25,11 @@ const CrossIcon = ()=>/*#__PURE__*/ _jsx("svg", {
24
25
  clipRule: "evenodd"
25
26
  })
26
27
  });
27
- export function PasswordStrengthIndicator({ strength, showRequirements = true, passwordMinLength = 8, requireUppercase = true, requireLowercase = true, requireNumber = true, requireSpecialChar = false }) {
28
+ export function PasswordStrengthIndicator({ strength, labels, showRequirements = true, passwordMinLength = 8, requireUppercase = true, requireLowercase = true, requireNumber = true, requireSpecialChar = false }) {
29
+ const say = {
30
+ ...DEFAULT_SET_PASSWORD_LABELS,
31
+ ...labels
32
+ };
28
33
  const theme = setPasswordPageTheme.passwordStrength;
29
34
  const getStrengthColor = (index)=>{
30
35
  if (index >= strength.score) return theme.bar.inactive;
@@ -37,31 +42,31 @@ export function PasswordStrengthIndicator({ strength, showRequirements = true, p
37
42
  const requirements = [
38
43
  {
39
44
  key: 'minLength',
40
- label: `At least ${passwordMinLength} characters`,
45
+ label: say.requirementMinLength(passwordMinLength),
41
46
  met: strength.hasMinLength,
42
47
  show: true
43
48
  },
44
49
  {
45
50
  key: 'uppercase',
46
- label: 'One uppercase letter',
51
+ label: say.requirementUppercase,
47
52
  met: strength.hasUppercase,
48
53
  show: requireUppercase
49
54
  },
50
55
  {
51
56
  key: 'lowercase',
52
- label: 'One lowercase letter',
57
+ label: say.requirementLowercase,
53
58
  met: strength.hasLowercase,
54
59
  show: requireLowercase
55
60
  },
56
61
  {
57
62
  key: 'number',
58
- label: 'One number',
63
+ label: say.requirementNumber,
59
64
  met: strength.hasNumber,
60
65
  show: requireNumber
61
66
  },
62
67
  {
63
68
  key: 'special',
64
- label: 'One special character',
69
+ label: say.requirementSpecialChar,
65
70
  met: strength.hasSpecialChar,
66
71
  show: requireSpecialChar
67
72
  }
@@ -75,7 +80,7 @@ export function PasswordStrengthIndicator({ strength, showRequirements = true, p
75
80
  "aria-valuenow": strength.score,
76
81
  "aria-valuemin": 0,
77
82
  "aria-valuemax": 5,
78
- "aria-label": "Password strength",
83
+ "aria-label": say.passwordStrength,
79
84
  children: [
80
85
  0,
81
86
  1,
@@ -88,7 +93,7 @@ export function PasswordStrengthIndicator({ strength, showRequirements = true, p
88
93
  }),
89
94
  showRequirements && /*#__PURE__*/ _jsx("ul", {
90
95
  className: theme.requirements.container,
91
- "aria-label": "Password requirements",
96
+ "aria-label": say.passwordRequirements,
92
97
  children: requirements.map((req)=>/*#__PURE__*/ _jsxs("li", {
93
98
  className: cn(theme.requirements.item, req.met ? theme.requirements.met : theme.requirements.unmet),
94
99
  children: [
@@ -22,16 +22,16 @@ export function SetPasswordForm({ labels, passwordChangeAction, passwordSetActio
22
22
  return say.passwordRequired;
23
23
  }
24
24
  if (store.newPassword.length < policy.minLength) {
25
- return `Password must be at least ${policy.minLength} characters`;
25
+ return say.passwordTooShort(policy.minLength);
26
26
  }
27
27
  if (policy.maxLength && store.newPassword.length > policy.maxLength) {
28
- return `Password must be at most ${policy.maxLength} characters`;
28
+ return say.passwordTooLong(policy.maxLength);
29
29
  }
30
30
  if (policy.requireUppercase && !/[A-Z]/.test(store.newPassword)) {
31
- return 'Password must contain at least one uppercase letter';
31
+ return say.passwordNeedsUppercase;
32
32
  }
33
33
  if (policy.requireLowercase && !/[a-z]/.test(store.newPassword)) {
34
- return 'Password must contain at least one lowercase letter';
34
+ return say.passwordNeedsLowercase;
35
35
  }
36
36
  if (policy.requireNumber && !/[0-9]/.test(store.newPassword)) {
37
37
  return say.passwordNeedsNumber;
@@ -39,7 +39,7 @@ export function SetPasswordForm({ labels, passwordChangeAction, passwordSetActio
39
39
  if (policy.requireSpecialChar) {
40
40
  const specialCharsRegex = new RegExp(`[${policy.specialChars.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')}]`);
41
41
  if (!specialCharsRegex.test(store.newPassword)) {
42
- return 'Password must contain at least one special character';
42
+ return say.passwordNeedsSpecialChar;
43
43
  }
44
44
  }
45
45
  if (!store.confirmPassword) {
@@ -105,7 +105,7 @@ export function SetPasswordForm({ labels, passwordChangeAction, passwordSetActio
105
105
  }),
106
106
  /*#__PURE__*/ _jsx(NucleusTextInput, {
107
107
  type: "password",
108
- label: "New Password",
108
+ label: say.newPasswordLabel,
109
109
  value: store.newPassword,
110
110
  onChange: (val)=>store.setNewPassword(val, policy.minLength),
111
111
  placeholder: say.newPasswordPlaceholder,
@@ -116,7 +116,7 @@ export function SetPasswordForm({ labels, passwordChangeAction, passwordSetActio
116
116
  }),
117
117
  /*#__PURE__*/ _jsx(NucleusTextInput, {
118
118
  type: "password",
119
- label: "Confirm Password",
119
+ label: say.confirmPasswordLabel,
120
120
  value: store.confirmPassword,
121
121
  onChange: store.setConfirmPassword,
122
122
  placeholder: say.confirmPasswordPlaceholder,
@@ -128,19 +128,19 @@ export function SetPasswordForm({ labels, passwordChangeAction, passwordSetActio
128
128
  type: "submit",
129
129
  disabled: activeAction?.state.isPending,
130
130
  className: theme.form.submitButton,
131
- children: activeAction?.state.isPending ? 'Setting Password...' : 'Set Password'
131
+ children: activeAction?.state.isPending ? say.submitting : say.submit
132
132
  }),
133
133
  /*#__PURE__*/ _jsx("div", {
134
134
  className: theme.form.backLink,
135
135
  children: backToLoginHref ? /*#__PURE__*/ _jsx("a", {
136
136
  href: backToLoginHref,
137
137
  className: theme.form.backLinkText,
138
- children: "Back to login"
138
+ children: say.backToLogin
139
139
  }) : onBackToLogin ? /*#__PURE__*/ _jsx("button", {
140
140
  type: "button",
141
141
  onClick: onBackToLogin,
142
142
  className: theme.form.backLinkText,
143
- children: "Back to login"
143
+ children: say.backToLogin
144
144
  }) : null
145
145
  })
146
146
  ]
@@ -40,7 +40,7 @@ export function SetPasswordPage({ logo, title = 'Set Password', subtitle = 'Ente
40
40
  },
41
41
  onErrorHandle: (error)=>{
42
42
  store.setStep('error');
43
- store.setError(error?.message || 'Token verification failed');
43
+ store.setError(error?.message || say.tokenVerificationFailed);
44
44
  }
45
45
  });
46
46
  });
@@ -96,7 +96,7 @@ export function SetPasswordPage({ logo, title = 'Set Password', subtitle = 'Ente
96
96
  }),
97
97
  /*#__PURE__*/ _jsx("p", {
98
98
  className: theme.states.verifying.subtitle,
99
- children: "Please wait while we verify your invitation link..."
99
+ children: say.verifyingSubtitle
100
100
  })
101
101
  ]
102
102
  })
@@ -144,7 +144,7 @@ export function SetPasswordPage({ logo, title = 'Set Password', subtitle = 'Ente
144
144
  className: theme.states.error.subtitle,
145
145
  children: isInvite ? say.invalidInviteBody : say.invalidLinkBody
146
146
  }),
147
- store.error && store.error !== 'Token verification failed' && /*#__PURE__*/ _jsx("p", {
147
+ store.error && store.error !== say.tokenVerificationFailed && /*#__PURE__*/ _jsx("p", {
148
148
  className: theme.states.error.errorMessage,
149
149
  children: store.error
150
150
  }),
@@ -213,7 +213,7 @@ export function SetPasswordPage({ logo, title = 'Set Password', subtitle = 'Ente
213
213
  }),
214
214
  /*#__PURE__*/ _jsx("p", {
215
215
  className: theme.states.success.subtitle,
216
- children: "Your account is now ready. Redirecting you to login..."
216
+ children: say.successSubtitle
217
217
  }),
218
218
  /*#__PURE__*/ _jsxs("div", {
219
219
  className: theme.layout.redirectWrapper,
@@ -16,10 +16,23 @@ export type SetPasswordLabels = {
16
16
  needHelp: string;
17
17
  success: string;
18
18
  redirecting: string;
19
+ /**
20
+ * The two sentences an employee waits in front of. The headings above them
21
+ * were translatable and these were not, so the wait read half in one
22
+ * language and half in another.
23
+ */
24
+ verifyingSubtitle: string;
25
+ successSubtitle: string;
26
+ newPasswordLabel: string;
27
+ confirmPasswordLabel: string;
19
28
  newPasswordPlaceholder: string;
20
29
  confirmPasswordPlaceholder: string;
21
30
  passwordStrength: string;
22
31
  passwordRequirements: string;
32
+ /** The one button on the form, and the word it wears while it works. */
33
+ submit: string;
34
+ submitting: string;
35
+ backToLogin: string;
23
36
  pageLabel: string;
24
37
  /**
25
38
  * The invalid-link screen. Its heading was translatable and the three
@@ -33,12 +46,34 @@ export type SetPasswordLabels = {
33
46
  invalidLinkHelp: string;
34
47
  goToLogin: string;
35
48
  invalidToken: string;
49
+ /**
50
+ * Never shown — it is the fallback the page compares against to decide the
51
+ * raw failure is not worth repeating under the heading. It reads from labels
52
+ * so no English literal is left in the component.
53
+ */
54
+ tokenVerificationFailed: string;
36
55
  /** Said when something is wrong, at the moment it is wrong. */
37
56
  passwordRequired: string;
57
+ /**
58
+ * The length rules carry a number, so they are functions: Turkish puts the
59
+ * count and the noun in an order English does not, and a sentence glued from
60
+ * a translated half and a number is a sentence in neither language.
61
+ */
62
+ passwordTooShort: (minLength: number) => string;
63
+ passwordTooLong: (maxLength: number) => string;
64
+ passwordNeedsUppercase: string;
65
+ passwordNeedsLowercase: string;
38
66
  passwordNeedsNumber: string;
67
+ passwordNeedsSpecialChar: string;
39
68
  confirmRequired: string;
40
69
  passwordsDoNotMatch: string;
41
70
  noActionConfigured: string;
42
71
  failed: string;
72
+ /** The checklist under the strength bar. */
73
+ requirementMinLength: (minLength: number) => string;
74
+ requirementUppercase: string;
75
+ requirementLowercase: string;
76
+ requirementNumber: string;
77
+ requirementSpecialChar: string;
43
78
  };
44
79
  export declare const DEFAULT_SET_PASSWORD_LABELS: SetPasswordLabels;
@@ -11,14 +11,21 @@
11
11
  * Defaults stay English, so nothing that does not pass labels changes.
12
12
  */ export const DEFAULT_SET_PASSWORD_LABELS = {
13
13
  verifying: 'Verifying Invitation',
14
+ verifyingSubtitle: 'Please wait while we verify your invitation link...',
14
15
  invalidLink: 'Invalid Link',
15
16
  needHelp: 'Need help? Contact your system administrator.',
16
17
  success: 'Password Set Successfully!',
18
+ successSubtitle: 'Your account is now ready. Redirecting you to login...',
17
19
  redirecting: 'Redirecting...',
20
+ newPasswordLabel: 'New Password',
21
+ confirmPasswordLabel: 'Confirm Password',
18
22
  newPasswordPlaceholder: 'Enter your new password',
19
23
  confirmPasswordPlaceholder: 'Confirm your new password',
20
24
  passwordStrength: 'Password strength',
21
25
  passwordRequirements: 'Password requirements',
26
+ submit: 'Set Password',
27
+ submitting: 'Setting Password...',
28
+ backToLogin: 'Back to login',
22
29
  pageLabel: 'Set password page',
23
30
  invalidInviteBody: 'This invitation link is invalid or has expired.',
24
31
  invalidLinkBody: 'This link is invalid or has expired.',
@@ -26,10 +33,21 @@
26
33
  invalidLinkHelp: 'Please request a new link or contact support.',
27
34
  goToLogin: 'Go to Login',
28
35
  invalidToken: 'Invalid or missing token',
36
+ tokenVerificationFailed: 'Token verification failed',
29
37
  passwordRequired: 'Password is required',
38
+ passwordTooShort: (minLength)=>`Password must be at least ${minLength} characters`,
39
+ passwordTooLong: (maxLength)=>`Password must be at most ${maxLength} characters`,
40
+ passwordNeedsUppercase: 'Password must contain at least one uppercase letter',
41
+ passwordNeedsLowercase: 'Password must contain at least one lowercase letter',
30
42
  passwordNeedsNumber: 'Password must contain at least one number',
43
+ passwordNeedsSpecialChar: 'Password must contain at least one special character',
31
44
  confirmRequired: 'Please confirm your password',
32
45
  passwordsDoNotMatch: 'Passwords do not match',
33
46
  noActionConfigured: 'No action configured',
34
- failed: 'Failed to set password'
47
+ failed: 'Failed to set password',
48
+ requirementMinLength: (minLength)=>`At least ${minLength} characters`,
49
+ requirementUppercase: 'One uppercase letter',
50
+ requirementLowercase: 'One lowercase letter',
51
+ requirementNumber: 'One number',
52
+ requirementSpecialChar: 'One special character'
35
53
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nucleus-core-ts",
3
- "version": "0.9.914",
3
+ "version": "0.9.915",
4
4
  "description": "Production-ready, enterprise-grade TypeScript framework for building multi-tenant APIs",
5
5
  "author": "Hidayet Can Özcan <hidayetcan@gmail.com>",
6
6
  "license": "SEE LICENSE IN LICENSE",