impact-ui 3.7.40-alpha.2 → 3.7.40-alpha.3

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "impact-ui",
3
- "version": "3.7.40-alpha.2",
3
+ "version": "3.7.40-alpha.3",
4
4
  "description": "Impact Analytics UI library",
5
5
  "main": "dist/index.js",
6
6
  "files": [
@@ -0,0 +1,95 @@
1
+ import { AGLocaleMap, BASE_LOCALE } from "../../locales/index";
2
+
3
+ /**
4
+ * AG Grid bakes these pagination labels into the panel HTML at create time.
5
+ * Updating localeText/getLocaleText alone will not refresh them — we patch
6
+ * these keys into the DOM after setAgGridLocaleText().
7
+ */
8
+ export const AG_GRID_PAGINATION_LOCALE_KEYS = [
9
+ "page",
10
+ "to",
11
+ "of",
12
+ "firstPage",
13
+ "previousPage",
14
+ "nextPage",
15
+ "lastPage",
16
+ ];
17
+
18
+ export function getAgGridLocaleTextForLocale(locale) {
19
+ return AGLocaleMap?.[locale] || AGLocaleMap?.[BASE_LOCALE] || {};
20
+ }
21
+
22
+ /**
23
+ * Update the mutable key→value object that getLocaleText reads from.
24
+ * Call this when the app locale changes (instead of remounting the grid).
25
+ */
26
+ export function setAgGridLocaleText(localeTextRef, locale) {
27
+ localeTextRef.current = getAgGridLocaleTextForLocale(locale);
28
+ return localeTextRef.current;
29
+ }
30
+
31
+ /**
32
+ * AG Grid getLocaleText callback — looks up one key at a time from the
33
+ * object managed by setAgGridLocaleText.
34
+ */
35
+ export function createAgGridGetLocaleText(localeTextRef) {
36
+ return (params) => {
37
+ let localisedText = localeTextRef.current?.[params.key];
38
+
39
+ if (localisedText == null) {
40
+ return params.defaultValue;
41
+ }
42
+
43
+ const variableValues = params.variableValues;
44
+ if (variableValues?.length) {
45
+ let found = 0;
46
+ while (found < variableValues.length) {
47
+ const idx = localisedText.indexOf("${variable}");
48
+ if (idx === -1) break;
49
+ localisedText =
50
+ localisedText.slice(0, idx) +
51
+ variableValues[found] +
52
+ localisedText.slice(idx + "${variable}".length);
53
+ found += 1;
54
+ }
55
+ }
56
+
57
+ return localisedText;
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Apply the pagination keys from the current localeText object onto the
63
+ * already-rendered AG Grid paging panel (no grid remount).
64
+ */
65
+ export function syncAgGridPaginationLocale(container, localeText) {
66
+ if (!container || !localeText) return;
67
+
68
+ container.querySelectorAll(".ag-paging-panel").forEach((panel) => {
69
+ const setByIdSuffix = (suffix, text) => {
70
+ if (text == null) return;
71
+ const el = panel.querySelector(`[id$="-${suffix}"]`);
72
+ if (el) el.textContent = text;
73
+ };
74
+
75
+ setByIdSuffix("to", localeText.to);
76
+ setByIdSuffix("of", localeText.of);
77
+ setByIdSuffix("start-page", localeText.page);
78
+ setByIdSuffix("of-page", localeText.of);
79
+
80
+ const buttons = panel.querySelectorAll(
81
+ ".ag-paging-page-summary-panel .ag-paging-button"
82
+ );
83
+ const buttonLabels = [
84
+ localeText.firstPage,
85
+ localeText.previousPage,
86
+ localeText.nextPage,
87
+ localeText.lastPage,
88
+ ];
89
+ buttons.forEach((button, index) => {
90
+ if (buttonLabels[index]) {
91
+ button.setAttribute("aria-label", buttonLabels[index]);
92
+ }
93
+ });
94
+ });
95
+ }
@@ -39,7 +39,12 @@ import { buildAdvanceSearchFilterModel } from "./filterUtils";
39
39
  import BulkActionBar from "./actionBar";
40
40
  import AdvanceSortModal from "./AdvanceSortModal";
41
41
  import { useTranslation } from "../../i18n";
42
- import { AGLocaleMap } from "../../locales/index";
42
+ import {
43
+ createAgGridGetLocaleText,
44
+ getAgGridLocaleTextForLocale,
45
+ setAgGridLocaleText,
46
+ syncAgGridPaginationLocale,
47
+ } from "./agGridLocale";
43
48
 
44
49
  const TableContent = forwardRef(
45
50
  (
@@ -159,6 +164,24 @@ const TableContent = forwardRef(
159
164
  );
160
165
 
161
166
  const { t, locale } = useTranslation();
167
+ const localeTextRef = useRef(getAgGridLocaleTextForLocale(locale));
168
+ const getLocaleText = useMemo(
169
+ () => createAgGridGetLocaleText(localeTextRef),
170
+ []
171
+ );
172
+ const syncPaginationLocale = useCallback(() => {
173
+ syncAgGridPaginationLocale(
174
+ document.getElementById(gridId || "myGrid"),
175
+ localeTextRef.current
176
+ );
177
+ }, [gridId]);
178
+
179
+ useEffect(() => {
180
+ setAgGridLocaleText(localeTextRef, locale);
181
+ const frameId = requestAnimationFrame(syncPaginationLocale);
182
+ return () => cancelAnimationFrame(frameId);
183
+ }, [locale, syncPaginationLocale]);
184
+
162
185
  const saveViewCustomFilter = saveViewCustomFilterProp || {
163
186
  options: [
164
187
  { label: t("table.viewGlobal"), value: "global" },
@@ -698,6 +721,7 @@ const TableContent = forwardRef(
698
721
 
699
722
  const onFirstDataRendered = (params) => {
700
723
  if (params?.api) params.api.getModel().appliedFontSize = tableFontSize;
724
+ syncPaginationLocale();
701
725
  args?.onFirstDataRendered?.(params);
702
726
  args?.gridOptions?.onFirstDataRendered?.(params);
703
727
  };
@@ -731,9 +755,14 @@ const TableContent = forwardRef(
731
755
  const paginationChanged = useCallback(
732
756
  (params) => {
733
757
  setDynmaicStyleToButtonComponent();
758
+ syncPaginationLocale();
734
759
  onPaginationChanged(params);
735
760
  },
736
- [setDynmaicStyleToButtonComponent, onPaginationChanged]
761
+ [
762
+ setDynmaicStyleToButtonComponent,
763
+ syncPaginationLocale,
764
+ onPaginationChanged,
765
+ ]
737
766
  );
738
767
 
739
768
  useEffect(() => {
@@ -1538,10 +1567,11 @@ const TableContent = forwardRef(
1538
1567
  }`}
1539
1568
  >
1540
1569
  <AgGridReact
1541
- {...(({ components, ...rest }) => rest)(args)}
1570
+ {...(({ components, localeText, getLocaleText, ...rest }) => rest)(
1571
+ args
1572
+ )}
1542
1573
  components={wrappedComponents}
1543
1574
  ref={actualRef}
1544
- key={`aggrid-${locale}`}
1545
1575
  suppressRowTransform={
1546
1576
  viewPoint === "list" ? false : args?.suppressRowTransform
1547
1577
  }
@@ -1576,7 +1606,7 @@ const TableContent = forwardRef(
1576
1606
  getRowClass={getRowClassWrapper}
1577
1607
  noRowsOverlayComponent={EmptyStateOverlay}
1578
1608
  onSortChanged={onGridSortChanged}
1579
- localeText={AGLocaleMap?.[locale]}
1609
+ getLocaleText={getLocaleText}
1580
1610
  />
1581
1611
  {isCommentFeatureEnabled && enableCommentPanel && (
1582
1612
  <div
@@ -25,7 +25,12 @@ import TableAdvanceSearchModal from "../Table/advanceSearchModal";
25
25
  import BulkActionBar from "./actionBar";
26
26
  import { ReactComponent as EmptyStateImage } from "../../assets/emptyStateImage.svg";
27
27
  import { useTranslation } from "../../i18n";
28
- import { AGLocaleMap } from "../../locales/index";
28
+ import {
29
+ createAgGridGetLocaleText,
30
+ getAgGridLocaleTextForLocale,
31
+ setAgGridLocaleText,
32
+ syncAgGridPaginationLocale,
33
+ } from "../Table/agGridLocale";
29
34
 
30
35
  export const OldTable = forwardRef(
31
36
  (
@@ -119,6 +124,23 @@ export const OldTable = forwardRef(
119
124
  );
120
125
 
121
126
  const { t, locale } = useTranslation();
127
+ const localeTextRef = useRef(getAgGridLocaleTextForLocale(locale));
128
+ const getLocaleText = useMemo(
129
+ () => createAgGridGetLocaleText(localeTextRef),
130
+ []
131
+ );
132
+ const syncPaginationLocale = useCallback(() => {
133
+ syncAgGridPaginationLocale(
134
+ document.getElementById(gridId || "myGrid"),
135
+ localeTextRef.current
136
+ );
137
+ }, [gridId]);
138
+
139
+ useEffect(() => {
140
+ setAgGridLocaleText(localeTextRef, locale);
141
+ const frameId = requestAnimationFrame(syncPaginationLocale);
142
+ return () => cancelAnimationFrame(frameId);
143
+ }, [locale, syncPaginationLocale]);
122
144
 
123
145
  const getSaveViewCustomFilter = saveViewCustomFilter ?? {
124
146
  options: [
@@ -319,12 +341,14 @@ export const OldTable = forwardRef(
319
341
  }, [columnDefs]);
320
342
 
321
343
  const paginationChanged = (params) => {
344
+ syncPaginationLocale();
322
345
  setDynmaicStyleToButtonComponent();
323
346
  onPaginationChanged(params);
324
347
  };
325
348
 
326
349
  const onFirstDataRendered = (params) => {
327
350
  if (params?.api) params.api.getModel().appliedFontSize = tableFontSize;
351
+ syncPaginationLocale();
328
352
  args?.onFirstDataRendered?.(params);
329
353
  args?.gridOptions?.onFirstDataRendered?.(params);
330
354
  };
@@ -620,9 +644,8 @@ export const OldTable = forwardRef(
620
644
  }`}
621
645
  >
622
646
  <AgGridReact
623
- {...args}
647
+ {...(({ localeText, getLocaleText, ...rest }) => rest)(args)}
624
648
  ref={actualRef}
625
- key={`aggrid-${locale}`}
626
649
  suppressRowTransform={
627
650
  viewPoint === "list" ? false : args?.suppressRowTransform
628
651
  }
@@ -651,7 +674,7 @@ export const OldTable = forwardRef(
651
674
  onFirstDataRendered={onFirstDataRendered}
652
675
  getRowHeight={getRowHeightWrapper}
653
676
  getRowClass={getRowClassWrapper}
654
- localeText={AGLocaleMap?.[locale]}
677
+ getLocaleText={getLocaleText}
655
678
  />
656
679
  </div>
657
680
  {(bottomLeftOptions || bottomCenterOptions || args.pagination) && (
@@ -0,0 +1,319 @@
1
+ import { Meta } from "@storybook/blocks";
2
+ import { LocaleDictionaryCollapse } from "./LocaleDictionaryCollapse";
3
+
4
+ <Meta title="Internationalization" />
5
+
6
+ <div className="ia-styles ia-guide-container i18n-guide">
7
+
8
+ # Internationalization (i18n)
9
+
10
+ `impact-ui` ships a built-in multi-language system based on React Context. Library components read their internal strings (labels, placeholders, messages, aria text, etc.) through it. Consuming apps can supply their own dictionaries, override library strings, and switch languages at runtime.
11
+
12
+ **Shipped locales:** `en-US` (complete base) and `de-DE` (German).
13
+
14
+ ---
15
+
16
+ ## How it works
17
+
18
+ Messages are resolved with a 3-layer merge (later layers win):
19
+
20
+ 1. **`en-US`** — guaranteed-complete base (nothing renders blank)
21
+ 2. **`ourDictionaries[locale]`** — impact-ui translation for the active locale (e.g. `de-DE`)
22
+ 3. **Host messages** — from your app’s `loadMessages(locale)` (always win)
23
+
24
+ Interpolation uses `{placeholder}` tokens:
25
+
26
+ ```jsx
27
+ t("monthPicker.placeholder", { format: "MM/YYYY" })
28
+ // → "Select MM/YYYY" (en-US) / "Auswählen MM/YYYY" (de-DE)
29
+ ```
30
+
31
+ Missing keys fall back to the key string itself so nothing renders blank.
32
+
33
+ ---
34
+
35
+ ## 1. Wrap your app with `I18nProvider`
36
+
37
+ ```jsx
38
+ import { I18nProvider } from "impact-ui";
39
+
40
+ const appMessages = {
41
+ "en-US": { "app.title": "My App" },
42
+ "de-DE": { "app.title": "Meine App" },
43
+ };
44
+
45
+ const App = () => (
46
+ <I18nProvider
47
+ defaultLocale="en-US"
48
+ loadMessages={(locale) => appMessages[locale] || {}}
49
+ >
50
+ <YourApp />
51
+ </I18nProvider>
52
+ );
53
+ ```
54
+
55
+ `loadMessages` can also return a Promise for lazy-loaded dictionaries:
56
+
57
+ ```jsx
58
+ loadMessages={(locale) =>
59
+ import(`./locales/${locale}.json`).then((m) => m.default)
60
+ }
61
+ ```
62
+
63
+ > `I18nProvider` is optional — without it, components fall back to static English and `setLocale` becomes a no-op.
64
+
65
+ ---
66
+
67
+ ## 2. Translate your own strings with `useTranslation`
68
+
69
+ Use this for any text your app passes into `impact-ui` components as props (titles, menu labels, etc.). Prefix your own keys (e.g. `app.*`) to avoid clashing with library keys (`common.*`, `table.*`, …):
70
+
71
+ ```jsx
72
+ import { useTranslation, Header } from "impact-ui";
73
+
74
+ const Example = () => {
75
+ const { t } = useTranslation();
76
+ return <Header title={t("app.header.title")} />;
77
+ };
78
+ ```
79
+
80
+ Library components (`Loader`, `Table`, `Sidebar`, `ChatBot`, `Select`, date pickers, etc.) translate their own internal strings automatically once `I18nProvider` is mounted — no extra props needed.
81
+
82
+ ---
83
+
84
+ ## 3. Switch the active language
85
+
86
+ ```jsx
87
+ import { useI18n } from "impact-ui";
88
+
89
+ const LanguageToggle = () => {
90
+ const { locale, setLocale } = useI18n();
91
+ return (
92
+ <button onClick={() => setLocale(locale === "en-US" ? "de-DE" : "en-US")}>
93
+ {locale}
94
+ </button>
95
+ );
96
+ };
97
+ ```
98
+
99
+ Or use the `Header` built-in language switcher:
100
+
101
+ ```jsx
102
+ import { Header, useI18n } from "impact-ui";
103
+
104
+ const AppHeader = () => {
105
+ const { setLocale } = useI18n();
106
+ return (
107
+ <Header
108
+ showLanguageIcon={true}
109
+ />
110
+ );
111
+ };
112
+ ```
113
+
114
+ Date pickers also sync `moment.locale`, and AG Grid uses the matching locale pack via `AGLocaleMap`.
115
+
116
+ ---
117
+
118
+ ## 4. Override library strings
119
+
120
+ Return overrides from `loadMessages` using the same keys shipped by the library — host messages always win:
121
+
122
+ ```jsx
123
+ loadMessages={(locale) => ({
124
+ "header.askIris":
125
+ locale === "de-DE" ? "Frag Iris" : "Ask Iris (custom)",
126
+ })}
127
+ ```
128
+
129
+ Inspect shipped keys in code:
130
+
131
+ ```js
132
+ import { enUS, ourDictionaries } from "impact-ui";
133
+
134
+ console.log(Object.keys(enUS));
135
+ console.log(ourDictionaries["de-DE"]);
136
+ ```
137
+
138
+ ---
139
+
140
+ ## 5. Add a locale not shipped by the library
141
+
142
+ `impact-ui` currently maintains `en-US` and `de-DE`. For any other locale, supply your own JSON via `loadMessages`. Library keys you don’t override fall back to English.
143
+
144
+ ```jsx
145
+ const frFR = {
146
+ "common.cancel": "Annuler",
147
+ "common.apply": "Appliquer",
148
+ // …override only the keys you need
149
+ };
150
+
151
+ <I18nProvider
152
+ defaultLocale="fr-FR"
153
+ loadMessages={(locale) => (locale === "fr-FR" ? frFR : {})}
154
+ >
155
+ <YourApp />
156
+ </I18nProvider>
157
+ ```
158
+
159
+ ---
160
+
161
+ ## Storybook tooling
162
+
163
+ Use the Storybook toolbar while browsing component stories:
164
+
165
+ - **Language** — switch between English and Deutsch
166
+ - **i18n Keys** — show raw keys (`common.cancel`) instead of translated text, so you can trace which key backs any visible string
167
+
168
+ For consumer-style demos, stories can declare their own dictionary via `parameters.i18nMessages` and the `I18nDemo` helper (see any `Internationalized` story).
169
+
170
+ ---
171
+
172
+ ## Available exports
173
+
174
+ | Export | Purpose |
175
+ | --- | --- |
176
+ | `I18nProvider` | Context provider — wrap your app |
177
+ | `useTranslation` | `{ t, locale, … }` for translating strings |
178
+ | `useI18n` | Full context (`locale`, `setLocale`, `messages`, `loading`, `t`) |
179
+ | `I18nContext` | Raw React context (advanced) |
180
+ | `buildMessages` / `translate` | Pure helpers (no React) |
181
+ | `ourDictionaries` | Map of shipped locale → dictionary |
182
+ | `enUS` | English base dictionary |
183
+ | `BASE_LOCALE` | `"en-US"` |
184
+ | `LOCALE_REGISTRY` | Labels + flag icons for the Header switcher |
185
+
186
+ ---
187
+
188
+ ## Shipped dictionaries
189
+
190
+ Expand a panel below to inspect the live JSON used by the library. Content is imported from `src/locales/` so it stays in sync with the source files.
191
+
192
+ <LocaleDictionaryCollapse />
193
+
194
+ </div>
195
+
196
+ <style>
197
+ {`
198
+ .i18n-guide h1,
199
+ .i18n-guide h2,
200
+ .i18n-guide h3 {
201
+ margin-bottom: 12px;
202
+ }
203
+
204
+ .i18n-guide section,
205
+ .i18n-guide p {
206
+ margin-bottom: 12px;
207
+ }
208
+
209
+ .i18n-guide pre {
210
+ background: #f6f8fa;
211
+ border: 1px solid #e1e4e8;
212
+ border-radius: 6px;
213
+ padding: 12px 16px;
214
+ overflow-x: auto;
215
+ font-size: 13px;
216
+ line-height: 1.45;
217
+ }
218
+
219
+ .i18n-guide table {
220
+ width: 100%;
221
+ border-collapse: collapse;
222
+ margin: 12px 0 24px;
223
+ font-size: 14px;
224
+ }
225
+
226
+ .i18n-guide th,
227
+ .i18n-guide td {
228
+ border: 1px solid #e1e4e8;
229
+ padding: 8px 12px;
230
+ text-align: left;
231
+ }
232
+
233
+ .i18n-guide th {
234
+ background: #f6f8fa;
235
+ }
236
+
237
+ .i18n-dict-collapse {
238
+ display: flex;
239
+ flex-direction: column;
240
+ gap: 12px;
241
+ margin: 16px 0 32px;
242
+ }
243
+
244
+ .i18n-dict-panel {
245
+ border: 1px solid #d0d7de;
246
+ border-radius: 8px;
247
+ background: #fff;
248
+ overflow: hidden;
249
+ }
250
+
251
+ .i18n-dict-summary {
252
+ display: flex;
253
+ align-items: center;
254
+ justify-content: space-between;
255
+ gap: 12px;
256
+ padding: 12px 16px;
257
+ cursor: pointer;
258
+ list-style: none;
259
+ font-weight: 600;
260
+ font-size: 14px;
261
+ background: #f6f8fa;
262
+ user-select: none;
263
+ }
264
+
265
+ .i18n-dict-summary::-webkit-details-marker {
266
+ display: none;
267
+ }
268
+
269
+ .i18n-dict-summary::before {
270
+ content: "▸";
271
+ display: inline-block;
272
+ margin-right: 8px;
273
+ transition: transform 0.15s ease;
274
+ color: #57606a;
275
+ }
276
+
277
+ .i18n-dict-panel[open] > .i18n-dict-summary::before {
278
+ transform: rotate(90deg);
279
+ }
280
+
281
+ .i18n-dict-summary-title {
282
+ flex: 1;
283
+ }
284
+
285
+ .i18n-dict-summary-meta {
286
+ font-weight: 500;
287
+ font-size: 12px;
288
+ color: #57606a;
289
+ background: #eaeef2;
290
+ border-radius: 999px;
291
+ padding: 2px 10px;
292
+ }
293
+
294
+ .i18n-dict-description {
295
+ margin: 12px 16px 0;
296
+ font-size: 13px;
297
+ color: #57606a;
298
+ }
299
+
300
+ .i18n-dict-pre {
301
+ margin: 12px 16px 16px;
302
+ max-height: 420px;
303
+ overflow: auto;
304
+ background: #0d1117;
305
+ color: #e6edf3;
306
+ border: none;
307
+ border-radius: 6px;
308
+ padding: 12px 16px;
309
+ font-size: 12px;
310
+ line-height: 1.5;
311
+ }
312
+
313
+ .i18n-dict-pre code {
314
+ background: transparent;
315
+ color: inherit;
316
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
317
+ }
318
+ `}
319
+ </style>
@@ -0,0 +1,44 @@
1
+ import React from "react"; // dont remove
2
+ import enUS from "../locales/en-US.json";
3
+ import deDE from "../locales/de-DE.json";
4
+
5
+ const dictionaries = [
6
+ {
7
+ code: "en-US",
8
+ title: "en-US.json — English (US)",
9
+ description: "Complete base dictionary. Missing keys in other locales fall back here.",
10
+ data: enUS,
11
+ },
12
+ {
13
+ code: "de-DE",
14
+ title: "de-DE.json — Deutsch",
15
+ description: "German translations shipped by impact-ui. Partial keys fall back to en-US.",
16
+ data: deDE,
17
+ },
18
+ ];
19
+
20
+ /**
21
+ * Collapsible viewer for the shipped locale JSON files.
22
+ * Used by I18n.mdx so the full dictionaries don't dominate the docs page.
23
+ */
24
+ export function LocaleDictionaryCollapse() {
25
+ return (
26
+ <div className="i18n-dict-collapse">
27
+ {dictionaries.map(({ code, title, description, data }) => {
28
+ const keyCount = Object.keys(data).length;
29
+ return (
30
+ <details key={code} className="i18n-dict-panel">
31
+ <summary className="i18n-dict-summary">
32
+ <span className="i18n-dict-summary-title">{title}</span>
33
+ <span className="i18n-dict-summary-meta">{keyCount} keys</span>
34
+ </summary>
35
+ <p className="i18n-dict-description">{description}</p>
36
+ <pre className="i18n-dict-pre">
37
+ <code style={{color:'black'}}>{JSON.stringify(data, null, 2)}</code>
38
+ </pre>
39
+ </details>
40
+ );
41
+ })}
42
+ </div>
43
+ );
44
+ }
@@ -6,6 +6,7 @@ import { Meta } from "@storybook/blocks";
6
6
 
7
7
  ## Core Documentation
8
8
  - [Getting Started](https://impact-ui-v3.iaproducts.ai/?path=/docs/getting-started--docs)
9
+ - [Internationalization](https://impact-ui-v3.iaproducts.ai/?path=/docs/internationalization--docs)
9
10
  - [Change Log](https://impact-ui-v3.iaproducts.ai/?path=/docs/change-log--docs)
10
11
 
11
12
  ## Patterns