Skip to content

Commit c839e0f

Browse files
committed
Localization and Icons
Added support for user localization with ENG fallback and updated scripts to use Icon components.
1 parent 1f0b16a commit c839e0f

32 files changed

Lines changed: 723 additions & 193 deletions

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ The library supports three theming modes:
5353
**2. Manual override** — Set `data-theme` on `<html>` to force a specific theme:
5454

5555
```html
56-
<html data-theme="dark"> <!-- Force dark -->
57-
<html data-theme="light"> <!-- Force light -->
56+
<html data-theme="dark" lang="en-GB"> <!-- Force dark -->
57+
<html data-theme="light" lang="en-GB"> <!-- Force light -->
5858
```
5959

6060
**3. JavaScript toggle:**

src/components/Banner/Banner.tsx

Lines changed: 21 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
import type { HTMLAttributes, ReactNode, Ref } from 'react';
22
import { useEffect, useRef, useState } from 'react';
33
import { CloseButton } from '../CloseButton';
4+
import { ErrorIcon, InfoIcon, SuccessIcon, WarningIcon } from '../../icons';
5+
import type { BannerLabels } from '../../labels';
6+
import { useComponentLibraryStrings } from '../../providers';
47
import styles from './Banner.module.scss';
58

69
export type BannerVariant = 'info' | 'warning' | 'error' | 'success';
710

11+
const DEFAULT_BANNER_LABELS: Required<BannerLabels> = {
12+
dismissAriaLabel: 'Dismiss banner',
13+
};
14+
815
export interface BannerProps extends HTMLAttributes<HTMLDivElement> {
916
/** Ref forwarded to the container `<div>`. */
1017
ref?: Ref<HTMLDivElement>;
@@ -20,38 +27,19 @@ export interface BannerProps extends HTMLAttributes<HTMLDivElement> {
2027
onDismiss?: () => void;
2128
/** Banner content. */
2229
children: ReactNode;
30+
/**
31+
* Override individual translatable strings inside the banner.
32+
* Values set here take priority over any `<ComponentLibraryProvider strings>` defaults.
33+
*/
34+
labels?: BannerLabels;
2335
}
2436

2537
/** Inline SVG icons — consistent rendering across every platform and font stack. */
2638
const variantIcons: Record<BannerVariant, ReactNode> = {
27-
info: (
28-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
29-
<circle cx="12" cy="12" r="10" />
30-
<path d="M12 16v-4" />
31-
<path d="M12 8h.01" />
32-
</svg>
33-
),
34-
warning: (
35-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
36-
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
37-
<line x1="12" y1="9" x2="12" y2="13" />
38-
<line x1="12" y1="17" x2="12.01" y2="17" />
39-
</svg>
40-
),
41-
// Octagon — visually distinct from the × close button.
42-
error: (
43-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
44-
<polygon points="7.86 2 16.14 2 22 7.86 22 16.14 16.14 22 7.86 22 2 16.14 2 7.86 7.86 2" />
45-
<line x1="12" y1="8" x2="12" y2="12" />
46-
<line x1="12" y1="16" x2="12.01" y2="16" />
47-
</svg>
48-
),
49-
success: (
50-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
51-
<circle cx="12" cy="12" r="10" />
52-
<path d="m9 12 2 2 4-4" />
53-
</svg>
54-
),
39+
info: <InfoIcon />,
40+
warning: <WarningIcon />,
41+
error: <ErrorIcon />,
42+
success: <SuccessIcon />,
5543
};
5644

5745
/** Duration matches `--transition-normal` (250 ms). */
@@ -81,8 +69,12 @@ export function Banner({
8169
children,
8270
className,
8371
ref,
72+
labels,
8473
...rest
8574
}: BannerProps) {
75+
const ctx = useComponentLibraryStrings();
76+
const l = { ...DEFAULT_BANNER_LABELS, ...ctx.banner, ...labels };
77+
8678
const [dismissing, setDismissing] = useState(false);
8779
const [dismissed, setDismissed] = useState(false);
8880

@@ -123,7 +115,7 @@ export function Banner({
123115
{dismissible && (
124116
<CloseButton
125117
size="sm"
126-
aria-label="Dismiss banner"
118+
aria-label={l.dismissAriaLabel}
127119
onClick={() => setDismissing(true)}
128120
className={styles.dismiss}
129121
/>

src/components/Banner/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export { Banner } from './Banner';
22
export type { BannerProps, BannerVariant } from './Banner';
3+
export type { BannerLabels } from '../../labels';
34

src/components/CloseButton/CloseButton.tsx

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { ButtonHTMLAttributes, Ref } from 'react';
2+
import { CloseIcon } from '../../icons';
23
import styles from './CloseButton.module.scss';
34

45
export type CloseButtonSize = 'sm' | 'md' | 'lg';
@@ -38,19 +39,7 @@ export function CloseButton({
3839
aria-label={ariaLabel}
3940
{...rest}
4041
>
41-
<svg
42-
className={styles.icon}
43-
viewBox="0 0 24 24"
44-
fill="none"
45-
stroke="currentColor"
46-
strokeWidth="2"
47-
strokeLinecap="round"
48-
strokeLinejoin="round"
49-
aria-hidden="true"
50-
>
51-
<line x1="18" y1="6" x2="6" y2="18" />
52-
<line x1="6" y1="6" x2="18" y2="18" />
53-
</svg>
42+
<CloseIcon className={styles.icon} />
5443
</button>
5544
);
5645
}

src/components/Dialog/Dialog.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,14 @@ import {
1111
} from 'react';
1212
import { CloseButton } from '../CloseButton';
1313
import { Button } from '../Button';
14+
import type { DialogLabels } from '../../labels';
15+
import { useComponentLibraryStrings } from '../../providers';
1416
import styles from './Dialog.module.scss';
1517

18+
const DEFAULT_DIALOG_LABELS: Required<DialogLabels> = {
19+
closeButtonAriaLabel: 'Close dialog',
20+
};
21+
1622
export interface DialogProps extends Omit<HTMLAttributes<HTMLDialogElement>, 'title'> {
1723
/** Ref forwarded to the native `<dialog>`. */
1824
ref?: Ref<HTMLDialogElement>;
@@ -34,6 +40,11 @@ export interface DialogProps extends Omit<HTMLAttributes<HTMLDialogElement>, 'ti
3440
onCancel?: () => void;
3541
/** Hide the confirm/cancel footer entirely. */
3642
hideActions?: boolean;
43+
/**
44+
* Override individual translatable strings inside the dialog.
45+
* Values set here take priority over any `<ComponentLibraryProvider strings>` defaults.
46+
*/
47+
labels?: DialogLabels;
3748
}
3849

3950
/**
@@ -59,8 +70,11 @@ export function Dialog({
5970
hideActions = false,
6071
className,
6172
ref,
73+
labels,
6274
...rest
6375
}: DialogProps) {
76+
const ctx = useComponentLibraryStrings();
77+
const l = { ...DEFAULT_DIALOG_LABELS, ...ctx.dialog, ...labels };
6478
const internalRef = useRef<HTMLDialogElement | null>(null);
6579

6680
const setRef = useCallback(
@@ -137,7 +151,7 @@ export function Dialog({
137151
</h2>
138152
<CloseButton
139153
size="sm"
140-
aria-label="Close dialog"
154+
aria-label={l.closeButtonAriaLabel}
141155
onClick={onClose}
142156
/>
143157
</header>

src/components/Dialog/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export { Dialog } from './Dialog';
22
export type { DialogProps } from './Dialog';
3+
export type { DialogLabels } from '../../labels';
34

src/components/HamburgerMenu/HamburgerMenu.tsx

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
useRef,
99
} from 'react';
1010
import { CloseButton } from '../CloseButton';
11+
import { HamburgerIcon } from '../../icons';
1112
import styles from './HamburgerMenu.module.scss';
1213
import { HamburgerMenuContext } from './HamburgerMenuContext';
1314

@@ -112,19 +113,7 @@ export function HamburgerMenu({
112113
aria-label={isOpen ? 'Close menu' : 'Open menu'}
113114
onClick={toggle}
114115
>
115-
<svg
116-
className={styles.hamburgerIcon}
117-
viewBox="0 0 24 24"
118-
fill="none"
119-
stroke="currentColor"
120-
strokeWidth="2"
121-
strokeLinecap="round"
122-
aria-hidden="true"
123-
>
124-
<line x1="3" y1="6" x2="21" y2="6" />
125-
<line x1="3" y1="12" x2="21" y2="12" />
126-
<line x1="3" y1="18" x2="21" y2="18" />
127-
</svg>
116+
<HamburgerIcon className={styles.hamburgerIcon} />
128117
</button>
129118

130119
{isOpen && (

src/components/MenuItemGroup/MenuItemGroup.tsx

Lines changed: 4 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { type HTMLAttributes, type ReactNode, type Ref, useState, useId, useRef, useEffect, useCallback } from 'react';
22
import { createPortal } from 'react-dom';
3+
import { ChevronRightIcon, FolderIcon } from '../../icons';
34
import styles from './MenuItemGroup.module.scss';
45
import { useSideMenu, SideMenuContext } from '../SideMenu/SideMenuContext';
56

@@ -18,24 +19,6 @@ export interface MenuItemGroupProps extends HTMLAttributes<HTMLDivElement> {
1819
children: ReactNode;
1920
}
2021

21-
/** Generic fallback icon rendered when the group has no icon and sidebar is collapsed. */
22-
function FallbackGroupIcon() {
23-
return (
24-
<svg
25-
viewBox="0 0 24 24"
26-
width="16"
27-
height="16"
28-
fill="none"
29-
stroke="currentColor"
30-
strokeWidth="2"
31-
strokeLinecap="round"
32-
strokeLinejoin="round"
33-
aria-hidden="true"
34-
>
35-
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
36-
</svg>
37-
);
38-
}
3922

4023
/**
4124
* Collapsible group of MenuItems with a label/header.
@@ -137,7 +120,7 @@ export function MenuItemGroup({
137120
onBlur={closeFlyout}
138121
>
139122
<span className={styles.icon} aria-hidden="true">
140-
{icon ?? <FallbackGroupIcon />}
123+
{icon ?? <FolderIcon />}
141124
</span>
142125
</button>
143126

@@ -177,20 +160,11 @@ export function MenuItemGroup({
177160
>
178161
{icon && <span className={styles.icon} aria-hidden="true">{icon}</span>}
179162
<span className={styles.triggerLabel}>{label}</span>
180-
<svg
163+
<ChevronRightIcon
181164
className={[styles.chevron, isOpen ? styles.chevronOpen : ''].filter(Boolean).join(' ')}
182-
viewBox="0 0 24 24"
183165
width="14"
184166
height="14"
185-
fill="none"
186-
stroke="currentColor"
187-
strokeWidth="2"
188-
strokeLinecap="round"
189-
strokeLinejoin="round"
190-
aria-hidden="true"
191-
>
192-
<polyline points="9 18 15 12 9 6" />
193-
</svg>
167+
/>
194168
</button>
195169
) : (
196170
<span id={triggerId} className={styles.staticLabel}>

src/components/NotificationPopup/NotificationPopup.tsx

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
import { type HTMLAttributes, type ReactNode, type Ref, useEffect, useRef, useCallback, useState } from 'react';
22
import { CloseButton } from '../CloseButton';
3+
import { ErrorIcon, InfoIcon, SuccessIcon, WarningIcon } from '../../icons';
4+
import type { NotificationPopupLabels } from '../../labels';
5+
import { useComponentLibraryStrings } from '../../providers';
36
import styles from './NotificationPopup.module.scss';
47

58
export type NotificationVariant = 'info' | 'success' | 'warning' | 'error';
69

10+
const DEFAULT_NOTIFICATION_LABELS: Required<NotificationPopupLabels> = {
11+
dismissAriaLabel: 'Dismiss notification',
12+
};
13+
714
export interface NotificationPopupProps extends HTMLAttributes<HTMLDivElement> {
815
/** Ref forwarded to the container. */
916
ref?: Ref<HTMLDivElement>;
@@ -26,6 +33,11 @@ export interface NotificationPopupProps extends HTMLAttributes<HTMLDivElement> {
2633
/** Called after the exit animation finishes and the element is removed from the DOM.
2734
* Use this in list/stack patterns to remove the item from state only after it has animated out. */
2835
onExited?: () => void;
36+
/**
37+
* Override individual translatable strings inside the notification.
38+
* Values set here take priority over any `<ComponentLibraryProvider strings>` defaults.
39+
*/
40+
labels?: NotificationPopupLabels;
2941
}
3042

3143
const ANIMATION_DURATION = 250;
@@ -62,8 +74,12 @@ export function NotificationPopup({
6274
onExited,
6375
className,
6476
ref,
77+
labels,
6578
...rest
6679
}: NotificationPopupProps) {
80+
const ctx = useComponentLibraryStrings();
81+
const l = { ...DEFAULT_NOTIFICATION_LABELS, ...ctx.notificationPopup, ...labels };
82+
6783
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
6884
const pausedRef = useRef(false);
6985
const prevVisibleRef = useRef(visible);
@@ -138,26 +154,10 @@ export function NotificationPopup({
138154
if (!mounted) return null;
139155

140156
const variantIcons: Record<NotificationVariant, ReactNode> = {
141-
info: (
142-
<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
143-
<path d="M8 1.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13zM0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm9 3a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm-.25-6.25a.75.75 0 0 0-1.5 0v3.5a.75.75 0 0 0 1.5 0v-3.5z" />
144-
</svg>
145-
),
146-
success: (
147-
<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
148-
<path d="M8 1.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13zM0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm11.78-2.28a.75.75 0 0 0-1.06-1.06L7 8.37 5.28 6.65a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.06 0l4.25-4.24z" />
149-
</svg>
150-
),
151-
warning: (
152-
<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
153-
<path d="M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368L8.22 1.754zM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm-.25-5.25a.75.75 0 0 0-1.5 0v2.5a.75.75 0 0 0 1.5 0v-2.5z" />
154-
</svg>
155-
),
156-
error: (
157-
<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
158-
<path d="M2.343 13.657A8 8 0 1 1 13.657 2.343 8 8 0 0 1 2.343 13.657zm1.06-1.06A6.5 6.5 0 1 0 12.596 3.404 6.5 6.5 0 0 0 3.404 12.596zM6.22 4.97a.75.75 0 0 0-1.06 1.06L6.94 8l-1.78 1.97a.75.75 0 1 0 1.06 1.06L8 9.06l1.97 1.97a.75.75 0 1 0 1.06-1.06L9.06 8l1.97-1.97a.75.75 0 0 0-1.06-1.06L8 6.94 6.22 4.97z" />
159-
</svg>
160-
),
157+
info: <InfoIcon />,
158+
success: <SuccessIcon />,
159+
warning: <WarningIcon />,
160+
error: <ErrorIcon />,
161161
};
162162

163163
const isUrgent = URGENT_VARIANTS.has(variant);
@@ -187,7 +187,7 @@ export function NotificationPopup({
187187
</div>
188188
<CloseButton
189189
size="sm"
190-
aria-label="Dismiss notification"
190+
aria-label={l.dismissAriaLabel}
191191
onClick={onDismiss}
192192
className={styles.dismiss}
193193
/>

src/components/NotificationPopup/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ export { NotificationPopup } from './NotificationPopup';
22
export type { NotificationPopupProps, NotificationVariant } from './NotificationPopup';
33
export { NotificationToastContainer } from './NotificationToastContainer';
44
export type { NotificationToastContainerProps } from './NotificationToastContainer';
5+
export type { NotificationPopupLabels } from '../../labels';
56

0 commit comments

Comments
 (0)