Skip to content

Commit 621059a

Browse files
committed
docs: add persistence page and add LocaleKeysTable in the Localization
1 parent b9a45fc commit 621059a

File tree

3 files changed

+279
-0
lines changed

3 files changed

+279
-0
lines changed
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { enUS, jaJP, zhCN } from "@fn-sphere/filter/locales";
2+
import { useState } from "react";
3+
import { Table } from "./table";
4+
5+
const locales = [
6+
{ key: "en-US", label: "English (en-US)", value: enUS },
7+
{ key: "ja-JP", label: "日本語 (ja-JP)", value: jaJP },
8+
{ key: "zh-CN", label: "中文 (zh-CN)", value: zhCN },
9+
] as const;
10+
11+
function getCategory(key: string): string {
12+
if (
13+
key.startsWith("operator") ||
14+
key.startsWith("add") ||
15+
key.startsWith("delete")
16+
)
17+
return "Layout";
18+
if (key.startsWith("enum")) return "Enum Filter";
19+
if (key.startsWith("number") || key.startsWith("greater") || key.startsWith("less"))
20+
return "Number Filter";
21+
if (key.startsWith("value")) return "Boolean Filter";
22+
if (["contains", "notContains", "startsWith", "endsWith"].includes(key))
23+
return "String Filter";
24+
if (["before", "after"].includes(key)) return "Date Filter";
25+
return "General Filter";
26+
}
27+
28+
export function LocaleKeysTable() {
29+
const [localeKey, setLocaleKey] = useState("en-US");
30+
const selectedLocale =
31+
locales.find((l) => l.key === localeKey)?.value ?? enUS;
32+
33+
const data = Object.entries(enUS).map(([key]) => ({
34+
Key: (
35+
<span className="font-mono text-sm text-gray-600 dark:text-gray-400">
36+
{key}
37+
</span>
38+
),
39+
"Default Value": (
40+
<span className="font-medium text-gray-900 dark:text-gray-100">
41+
{selectedLocale[key as keyof typeof selectedLocale] ?? "-"}
42+
</span>
43+
),
44+
Category: (
45+
<span className="inline-flex rounded-full bg-blue-100 px-2 py-1 text-xs font-semibold text-blue-800 dark:bg-blue-900 dark:text-blue-200">
46+
{getCategory(key)}
47+
</span>
48+
),
49+
}));
50+
51+
return (
52+
<div>
53+
<div className="mb-3 flex items-center gap-2">
54+
<label
55+
htmlFor="locale-select"
56+
className="text-sm font-medium text-gray-700 dark:text-gray-300"
57+
>
58+
Language:
59+
</label>
60+
<select
61+
id="locale-select"
62+
value={localeKey}
63+
onChange={(e) => setLocaleKey(e.target.value)}
64+
className="rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm shadow-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
65+
>
66+
{locales.map((locale) => (
67+
<option key={locale.key} value={locale.key}>
68+
{locale.label}
69+
</option>
70+
))}
71+
</select>
72+
</div>
73+
<Table data={data} className="not-content" />
74+
</div>
75+
);
76+
}

packages/docs/src/content/docs/customization/localization.mdx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ Filter Sphere provides some built-in locales. You can import them from the `@fn-
1515
| Japanese | `jaJP` |
1616
| Chinese (Simplified) | `zhCN` |
1717

18+
## Usage
19+
1820
```tsx {5-8} {13}
1921
import { enUS, jaJP, zhCN } from "@fn-sphere/filter/locales";
2022

@@ -243,3 +245,13 @@ Then add the corresponding keys to your translation files:
243245
```
244246

245247
With this approach, all Filter Sphere labels are resolved through your existing i18n pipeline, so you only need to maintain a single set of translation files.
248+
249+
## i18n Keys
250+
251+
The following table lists all available i18n keys and their default values. Use these keys when providing translations via `getLocaleText` or your i18n library.
252+
253+
import { LocaleKeysTable } from "~/components/locale-keys-table.tsx";
254+
255+
<div className="not-content">
256+
<LocaleKeysTable client:load />
257+
</div>
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
---
2+
title: Persistence
3+
description: Save and restore filter rules
4+
---
5+
6+
Filter rules can be saved to `localStorage` (or any storage) and restored later, so users don't lose their filters on page reload.
7+
8+
## Serialization
9+
10+
A `FilterGroup` is a plain object, so `JSON.stringify` works for most cases. However, if your schema includes `Date` fields, you need custom handling since `JSON.stringify` converts dates to strings.
11+
12+
```ts
13+
import type { FilterGroup } from "@fn-sphere/filter";
14+
15+
export function serializeFilterGroup(filterGroup: FilterGroup): string {
16+
const replacer = function (this: any, key: string) {
17+
return this[key] instanceof Date
18+
? { __type: "Date", value: this[key].toISOString() }
19+
: this[key];
20+
};
21+
return JSON.stringify(filterGroup, replacer);
22+
}
23+
```
24+
25+
## Deserialization
26+
27+
When reading back, revive `Date` objects and validate the structure before using it.
28+
29+
```ts
30+
import type { FilterGroup } from "@fn-sphere/filter";
31+
32+
export function deserializeFilterGroup(serialized: string): FilterGroup {
33+
const deserialized = JSON.parse(serialized, (_, value) => {
34+
if (value && typeof value === "object" && value.__type === "Date") {
35+
return new Date(value.value);
36+
}
37+
return value;
38+
});
39+
return deserialized as FilterGroup;
40+
}
41+
```
42+
43+
## Save to localStorage
44+
45+
Use the `onRuleChange` callback to save the filter rule whenever it changes.
46+
47+
```tsx
48+
import {
49+
FilterBuilder,
50+
FilterSphereProvider,
51+
useFilterSphere,
52+
} from "@fn-sphere/filter";
53+
54+
const STORAGE_KEY = "my-filter-rule";
55+
56+
function MyFilter({ schema }) {
57+
const { context } = useFilterSphere({
58+
schema,
59+
defaultRule: loadFilterRule(),
60+
onRuleChange: ({ filterRule }) => {
61+
localStorage.setItem(STORAGE_KEY, serializeFilterGroup(filterRule));
62+
},
63+
});
64+
65+
return (
66+
<FilterSphereProvider context={context}>
67+
<FilterBuilder />
68+
</FilterSphereProvider>
69+
);
70+
}
71+
```
72+
73+
## Restore from localStorage
74+
75+
Read from storage on mount and pass it as `defaultRule`. Wrap it in a try-catch so corrupted data doesn't break the UI.
76+
77+
```ts
78+
import { createFilterGroup, createSingleFilter } from "@fn-sphere/filter";
79+
80+
const fallbackRule = createFilterGroup({
81+
op: "and",
82+
conditions: [createSingleFilter()],
83+
});
84+
85+
function loadFilterRule() {
86+
try {
87+
const saved = localStorage.getItem(STORAGE_KEY);
88+
if (!saved) return fallbackRule;
89+
return deserializeFilterGroup(saved);
90+
} catch {
91+
return fallbackRule;
92+
}
93+
}
94+
```
95+
96+
## Full Example
97+
98+
Putting it all together:
99+
100+
```tsx
101+
import {
102+
FilterBuilder,
103+
FilterSphereProvider,
104+
useFilterSphere,
105+
createFilterGroup,
106+
createSingleFilter,
107+
type FilterGroup,
108+
} from "@fn-sphere/filter";
109+
import { z } from "zod";
110+
111+
const STORAGE_KEY = "my-filter-rule";
112+
113+
const schema = z.object({
114+
name: z.string().describe("Name"),
115+
createdAt: z.date().describe("Created At"),
116+
});
117+
118+
const fallbackRule = createFilterGroup({
119+
op: "and",
120+
conditions: [createSingleFilter()],
121+
});
122+
123+
// --- Serialization ---
124+
125+
function serializeFilterGroup(filterGroup: FilterGroup): string {
126+
const replacer = function (this: any, key: string) {
127+
return this[key] instanceof Date
128+
? { __type: "Date", value: this[key].toISOString() }
129+
: this[key];
130+
};
131+
return JSON.stringify(filterGroup, replacer);
132+
}
133+
134+
function deserializeFilterGroup(serialized: string): FilterGroup {
135+
const deserialized = JSON.parse(serialized, (_, value) => {
136+
if (value && typeof value === "object" && value.__type === "Date") {
137+
return new Date(value.value);
138+
}
139+
return value;
140+
});
141+
if (
142+
!deserialized ||
143+
deserialized.type !== "FilterGroup" ||
144+
!Array.isArray(deserialized.conditions)
145+
) {
146+
throw new Error("Invalid FilterGroup structure");
147+
}
148+
return deserialized;
149+
}
150+
151+
// --- Storage ---
152+
153+
function loadFilterRule(): FilterGroup {
154+
try {
155+
const saved = localStorage.getItem(STORAGE_KEY);
156+
if (!saved) return fallbackRule;
157+
return deserializeFilterGroup(saved);
158+
} catch {
159+
return fallbackRule;
160+
}
161+
}
162+
163+
// --- Component ---
164+
165+
export default function PersistentFilter() {
166+
const { context } = useFilterSphere({
167+
schema,
168+
defaultRule: loadFilterRule(),
169+
onRuleChange: ({ filterRule }) => {
170+
localStorage.setItem(STORAGE_KEY, serializeFilterGroup(filterRule));
171+
},
172+
});
173+
174+
return (
175+
<FilterSphereProvider context={context}>
176+
<FilterBuilder />
177+
</FilterSphereProvider>
178+
);
179+
}
180+
```
181+
182+
## Using Other Serialization Libraries
183+
184+
If you prefer not to write custom serialization, you can use <a href="https://github.com/flightcontrolhq/superjson" target="_blank">superjson</a> which handles `Date`, `Map`, `Set`, `RegExp`, and other types automatically.
185+
186+
import { Aside } from "@astrojs/starlight/components";
187+
188+
<Aside title="Tip">
189+
If your schema has no `Date` fields, you can skip the custom replacer/reviver
190+
and use `JSON.stringify` / `JSON.parse` directly.
191+
</Aside>

0 commit comments

Comments
 (0)