@mim/histui 0.1.0
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/README.md +167 -0
- package/package.json +35 -0
- package/src/analytics.js +25 -0
- package/src/default-config.js +120 -0
- package/src/filters.js +61 -0
- package/src/i18n.js +184 -0
- package/src/index.d.ts +137 -0
- package/src/index.js +457 -0
- package/src/paststruct.js +270 -0
- package/src/styles.css +780 -0
- package/src/theme.js +27 -0
- package/src/timeline-view.js +2062 -0
package/README.md
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Histui
|
|
2
|
+
|
|
3
|
+
Histui is a reusable, framework-agnostic interactive history timeline package. It can render PastStruct datasets or already-normalized records into a zoomable, pannable, responsive timeline with LOD, clustering, a zoom navigator, hover-linked connectors, axis placement controls, themes, Persian/English UI strings, and explode mode.
|
|
4
|
+
|
|
5
|
+
## Files
|
|
6
|
+
|
|
7
|
+
- `src/index.js` - public package API
|
|
8
|
+
- `src/index.d.ts` - public TypeScript declarations
|
|
9
|
+
- `src/styles.css` - required component styles
|
|
10
|
+
- `src/timeline-view.js` - low-level timeline renderer
|
|
11
|
+
- `src/paststruct.js` - PastStruct normalization helpers
|
|
12
|
+
- `examples/basic.html` - no-build browser example
|
|
13
|
+
|
|
14
|
+
## Basic Usage
|
|
15
|
+
|
|
16
|
+
```html
|
|
17
|
+
<link rel="stylesheet" href="/path/to/histui/src/styles.css">
|
|
18
|
+
<div id="timeline" style="height: 720px"></div>
|
|
19
|
+
<script type="module">
|
|
20
|
+
import { createHistuiTimeline } from "/path/to/histui/src/index.js";
|
|
21
|
+
|
|
22
|
+
const histui = createHistuiTimeline({
|
|
23
|
+
container: "#timeline",
|
|
24
|
+
data: pastStructDataset,
|
|
25
|
+
language: "en",
|
|
26
|
+
themeId: "obsidian-lab",
|
|
27
|
+
explodeEnabled: false,
|
|
28
|
+
onSelect(record) {
|
|
29
|
+
console.log("selected", record.id);
|
|
30
|
+
},
|
|
31
|
+
onViewportChange(viewport) {
|
|
32
|
+
console.log(viewport);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
histui.setExplodeEnabled(true);
|
|
37
|
+
histui.setFilters({ minSignificance: 7 });
|
|
38
|
+
</script>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Local Development
|
|
42
|
+
|
|
43
|
+
Run the package directly from this repository when you want to develop `histui` itself and see source changes immediately in the browser:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm run dev
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Open [http://127.0.0.1:5175](http://127.0.0.1:5175). The dev server serves `examples/basic.html`, which imports `../src/index.js` and `../src/styles.css`, so it always uses the local package source instead of a published build.
|
|
50
|
+
|
|
51
|
+
Changes in `src/`, `examples/`, `README.md`, `PUBLISHING.md`, or `package.json` trigger an automatic browser reload. The server disables caching so style and JavaScript edits show up on the next reload without extra build steps.
|
|
52
|
+
|
|
53
|
+
Use a custom port when needed:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
PORT=5180 npm run dev
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Keep this server running while editing package files. For testing the package inside `histui-app-2`, keep the app pointed at the local file dependency (`"histui": "file:../histui"`), run the app dev server in that repo, and reinstall there only after changing package metadata or dependency wiring.
|
|
60
|
+
|
|
61
|
+
## Public API
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
import {
|
|
65
|
+
HistuiTimeline,
|
|
66
|
+
createHistuiTimeline,
|
|
67
|
+
normalizeTimelineData,
|
|
68
|
+
normalizePastStruct,
|
|
69
|
+
createDefaultFilters,
|
|
70
|
+
filterRecords,
|
|
71
|
+
DEFAULT_HISTUI_CONFIG
|
|
72
|
+
} from "histui";
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### `createHistuiTimeline(options)`
|
|
76
|
+
|
|
77
|
+
Creates and mounts a timeline instance.
|
|
78
|
+
|
|
79
|
+
Common options:
|
|
80
|
+
|
|
81
|
+
- `container`: CSS selector or element. Required.
|
|
82
|
+
- `data`: PastStruct dataset document, single PastStruct record, or array of records.
|
|
83
|
+
- `records`: normalized records or raw PastStruct record array.
|
|
84
|
+
- `config`: partial config merged with `DEFAULT_HISTUI_CONFIG`.
|
|
85
|
+
- `language`: default `"en"`.
|
|
86
|
+
- `direction`: optional text direction override.
|
|
87
|
+
- `themeId` or `theme`: built-in theme id or custom theme object.
|
|
88
|
+
- `controls`: render built-in timeline controls. Default `true`.
|
|
89
|
+
- `replace`: clear the container before mounting. Default `true`.
|
|
90
|
+
- `filters`: initial filter object.
|
|
91
|
+
- `orientation`: `"auto"`, `"horizontal"`, or `"vertical"`.
|
|
92
|
+
- `axisPlacement`: `{ horizontal, vertical }`, each `"center"`, `"side-start"`, or `"side-end"`.
|
|
93
|
+
- `lodEnabled`: boolean.
|
|
94
|
+
- `explodeEnabled`: boolean.
|
|
95
|
+
- `analytics.measurementId`: optional Google Analytics measurement id.
|
|
96
|
+
- `onSelect(record, instance)`: event callback.
|
|
97
|
+
- `onViewportChange(viewport, instance)`: event callback.
|
|
98
|
+
- `onRecordsChange(records, instance)`: event callback.
|
|
99
|
+
- `onTrack(name, payload, instance)`: analytics/telemetry callback.
|
|
100
|
+
|
|
101
|
+
### Instance Methods
|
|
102
|
+
|
|
103
|
+
- `setData(data, options)`
|
|
104
|
+
- `setRecords(records, options)`
|
|
105
|
+
- `setFilters(filters, options)`
|
|
106
|
+
- `resetFilters(options)`
|
|
107
|
+
- `select(recordId, options)`
|
|
108
|
+
- `fit(options)`
|
|
109
|
+
- `zoomBy(factor)`
|
|
110
|
+
- `setViewRange(start, end, options)`
|
|
111
|
+
- `setOrientation(orientation)`
|
|
112
|
+
- `setAxisPlacement(orientation, placement)`
|
|
113
|
+
- `setLodEnabled(enabled)`
|
|
114
|
+
- `setExplodeEnabled(enabled)`
|
|
115
|
+
- `setLanguage(language, direction)`
|
|
116
|
+
- `setTheme(themeOrId)`
|
|
117
|
+
- `getState()`
|
|
118
|
+
- `destroy()`
|
|
119
|
+
|
|
120
|
+
## Filters
|
|
121
|
+
|
|
122
|
+
`setFilters()` accepts the same filter shape used internally:
|
|
123
|
+
|
|
124
|
+
```js
|
|
125
|
+
histui.setFilters({
|
|
126
|
+
search: "revolution",
|
|
127
|
+
recordTypes: ["event", "period"],
|
|
128
|
+
types: ["political"],
|
|
129
|
+
minSignificance: 6,
|
|
130
|
+
mediaOnly: false,
|
|
131
|
+
uncertainOnly: false,
|
|
132
|
+
fromYear: 1800,
|
|
133
|
+
toYear: 2026
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Set-like fields can be arrays or `Set` instances.
|
|
138
|
+
|
|
139
|
+
## Config
|
|
140
|
+
|
|
141
|
+
The package exposes `DEFAULT_HISTUI_CONFIG`. You can override only the keys you need:
|
|
142
|
+
|
|
143
|
+
```js
|
|
144
|
+
createHistuiTimeline({
|
|
145
|
+
container,
|
|
146
|
+
data,
|
|
147
|
+
config: {
|
|
148
|
+
timeline: {
|
|
149
|
+
explode: {
|
|
150
|
+
maxVisible: 42,
|
|
151
|
+
layers: 8,
|
|
152
|
+
animationMs: 700
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Check
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
npm run check
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Publishing
|
|
166
|
+
|
|
167
|
+
See [PUBLISHING.md](./PUBLISHING.md) for the npm publishing checklist.
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mim/histui",
|
|
3
|
+
"publishConfig": {
|
|
4
|
+
"access": "public"
|
|
5
|
+
},
|
|
6
|
+
"version": "0.1.0",
|
|
7
|
+
"description": "Reusable Histui interactive timeline package for PastStruct and normalized historical records.",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"main": "./src/index.js",
|
|
10
|
+
"module": "./src/index.js",
|
|
11
|
+
"types": "./src/index.d.ts",
|
|
12
|
+
"style": "./src/styles.css",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": "./src/index.js",
|
|
15
|
+
"./styles.css": "./src/styles.css"
|
|
16
|
+
},
|
|
17
|
+
"sideEffects": [
|
|
18
|
+
"./src/styles.css"
|
|
19
|
+
],
|
|
20
|
+
"files": [
|
|
21
|
+
"src",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"dev": "node scripts/dev-server.mjs",
|
|
26
|
+
"check": "node scripts/check.mjs"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"history",
|
|
30
|
+
"timeline",
|
|
31
|
+
"paststruct",
|
|
32
|
+
"visualization"
|
|
33
|
+
],
|
|
34
|
+
"license": "UNLICENSED"
|
|
35
|
+
}
|
package/src/analytics.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
let analyticsReady = false;
|
|
2
|
+
|
|
3
|
+
export function initializeAnalytics({ measurementId, appName = "Histui" } = {}) {
|
|
4
|
+
const id = String(measurementId || "").trim();
|
|
5
|
+
if (!id || analyticsReady || typeof window === "undefined") return;
|
|
6
|
+
|
|
7
|
+
window.dataLayer = window.dataLayer || [];
|
|
8
|
+
window.gtag = function gtag() {
|
|
9
|
+
window.dataLayer.push(arguments);
|
|
10
|
+
};
|
|
11
|
+
window.gtag("js", new Date());
|
|
12
|
+
window.gtag("config", id, { app_name: appName });
|
|
13
|
+
|
|
14
|
+
const script = document.createElement("script");
|
|
15
|
+
script.async = true;
|
|
16
|
+
script.src = `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(id)}`;
|
|
17
|
+
document.head.append(script);
|
|
18
|
+
analyticsReady = true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function trackAnalyticsEvent(name, params = {}) {
|
|
22
|
+
if (typeof window === "undefined" || typeof window.gtag !== "function") return;
|
|
23
|
+
window.gtag("event", name, params);
|
|
24
|
+
}
|
|
25
|
+
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
export const DEFAULT_HISTUI_CONFIG = {
|
|
2
|
+
app: {
|
|
3
|
+
name: "Histui",
|
|
4
|
+
defaultLanguage: "en",
|
|
5
|
+
languages: ["en", "fa"],
|
|
6
|
+
defaultTheme: "obsidian-lab",
|
|
7
|
+
orientation: "auto",
|
|
8
|
+
axisPlacement: {
|
|
9
|
+
horizontal: "center",
|
|
10
|
+
vertical: "side-start"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
analytics: {
|
|
14
|
+
googleAnalyticsMeasurementId: ""
|
|
15
|
+
},
|
|
16
|
+
timeline: {
|
|
17
|
+
minZoomSpanYears: 2,
|
|
18
|
+
maxZoomMultiplier: 2.5,
|
|
19
|
+
defaultPaddingRatio: 0.08,
|
|
20
|
+
inertia: {
|
|
21
|
+
enabled: true,
|
|
22
|
+
friction: 0.92,
|
|
23
|
+
wheelFriction: 0.86,
|
|
24
|
+
minVelocity: 0.02
|
|
25
|
+
},
|
|
26
|
+
navigator: {
|
|
27
|
+
enabled: true,
|
|
28
|
+
animationMs: 420,
|
|
29
|
+
trackInsetPx: 18,
|
|
30
|
+
minSelectionPixels: 10
|
|
31
|
+
},
|
|
32
|
+
lod: {
|
|
33
|
+
enabled: true,
|
|
34
|
+
thresholds: [
|
|
35
|
+
{ spanYears: 2600, minSignificance: 9, labelMode: "icon" },
|
|
36
|
+
{ spanYears: 1100, minSignificance: 8, labelMode: "short" },
|
|
37
|
+
{ spanYears: 450, minSignificance: 7, labelMode: "short" },
|
|
38
|
+
{ spanYears: 170, minSignificance: 5, labelMode: "standard" },
|
|
39
|
+
{ spanYears: 0, minSignificance: 1, labelMode: "full" }
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
explode: {
|
|
43
|
+
enabled: false,
|
|
44
|
+
maxVisible: 34,
|
|
45
|
+
minVisible: 10,
|
|
46
|
+
layers: 6,
|
|
47
|
+
densityPixels: 8800,
|
|
48
|
+
animationMs: 620
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
themes: [
|
|
52
|
+
{
|
|
53
|
+
id: "obsidian-lab",
|
|
54
|
+
label: {
|
|
55
|
+
en: "Obsidian Lab",
|
|
56
|
+
fa: "آزمایشگاه آبسیدین"
|
|
57
|
+
},
|
|
58
|
+
colors: {
|
|
59
|
+
background: "#0f1412",
|
|
60
|
+
surface: "#151b18",
|
|
61
|
+
surfaceRaised: "#202821",
|
|
62
|
+
panel: "#111714",
|
|
63
|
+
text: "#edf2ea",
|
|
64
|
+
muted: "#9ba89d",
|
|
65
|
+
line: "#35433b",
|
|
66
|
+
grid: "#26322c",
|
|
67
|
+
accent: "#4fb7a5",
|
|
68
|
+
accent2: "#d4b45f",
|
|
69
|
+
accent3: "#f0705a",
|
|
70
|
+
accent4: "#81c7d4",
|
|
71
|
+
shadow: "rgba(0, 0, 0, 0.42)"
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: "museum-glass",
|
|
76
|
+
label: {
|
|
77
|
+
en: "Museum Glass",
|
|
78
|
+
fa: "شیشه موزه"
|
|
79
|
+
},
|
|
80
|
+
colors: {
|
|
81
|
+
background: "#f4f2ec",
|
|
82
|
+
surface: "#fffdf8",
|
|
83
|
+
surfaceRaised: "#ffffff",
|
|
84
|
+
panel: "#ebe7dd",
|
|
85
|
+
text: "#242820",
|
|
86
|
+
muted: "#697163",
|
|
87
|
+
line: "#cec7b8",
|
|
88
|
+
grid: "#ded8cb",
|
|
89
|
+
accent: "#257c78",
|
|
90
|
+
accent2: "#a66b2c",
|
|
91
|
+
accent3: "#b84740",
|
|
92
|
+
accent4: "#4a7c9f",
|
|
93
|
+
shadow: "rgba(67, 54, 32, 0.16)"
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: "graphite-citrus",
|
|
98
|
+
label: {
|
|
99
|
+
en: "Graphite Citrus",
|
|
100
|
+
fa: "گرافیت مرکباتی"
|
|
101
|
+
},
|
|
102
|
+
colors: {
|
|
103
|
+
background: "#191a18",
|
|
104
|
+
surface: "#22231f",
|
|
105
|
+
surfaceRaised: "#2d3029",
|
|
106
|
+
panel: "#141512",
|
|
107
|
+
text: "#f5f7ed",
|
|
108
|
+
muted: "#a9b09d",
|
|
109
|
+
line: "#474b3f",
|
|
110
|
+
grid: "#33382e",
|
|
111
|
+
accent: "#b5d342",
|
|
112
|
+
accent2: "#42b6a3",
|
|
113
|
+
accent3: "#e86f4f",
|
|
114
|
+
accent4: "#f0bd52",
|
|
115
|
+
shadow: "rgba(0, 0, 0, 0.38)"
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
]
|
|
119
|
+
};
|
|
120
|
+
|
package/src/filters.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export function getDatasetBounds(records) {
|
|
2
|
+
if (!records.length) {
|
|
3
|
+
const year = new Date().getUTCFullYear();
|
|
4
|
+
return { start: year - 10, end: year + 10 };
|
|
5
|
+
}
|
|
6
|
+
const starts = records.map((record) => record.__meta.start).filter(Number.isFinite);
|
|
7
|
+
const ends = records.map((record) => record.__meta.end).filter(Number.isFinite);
|
|
8
|
+
return {
|
|
9
|
+
start: Math.min(...starts, ...ends),
|
|
10
|
+
end: Math.max(...starts, ...ends)
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function createDefaultFilters(records, facets = {}) {
|
|
15
|
+
const bounds = getDatasetBounds(records);
|
|
16
|
+
return {
|
|
17
|
+
search: "",
|
|
18
|
+
recordTypes: new Set((facets.recordTypes || []).map((item) => item.key)),
|
|
19
|
+
types: new Set((facets.types || []).map((item) => item.key)),
|
|
20
|
+
factuality: new Set((facets.factuality || []).map((item) => item.key)),
|
|
21
|
+
confidence: new Set((facets.confidence || []).map((item) => item.key)),
|
|
22
|
+
scopes: new Set((facets.scopes || []).map((item) => item.key)),
|
|
23
|
+
categories: new Set((facets.categories || []).map((item) => item.key)),
|
|
24
|
+
countries: new Set((facets.countries || []).map((item) => item.key)),
|
|
25
|
+
minSignificance: 1,
|
|
26
|
+
mediaOnly: false,
|
|
27
|
+
uncertainOnly: false,
|
|
28
|
+
fromYear: Math.floor(bounds.start),
|
|
29
|
+
toYear: Math.ceil(bounds.end)
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function normalizeFilters(filters = {}, baseFilters = {}) {
|
|
34
|
+
const merged = { ...baseFilters, ...filters };
|
|
35
|
+
for (const key of ["recordTypes", "types", "factuality", "confidence", "scopes", "categories", "countries"]) {
|
|
36
|
+
if (Array.isArray(merged[key])) merged[key] = new Set(merged[key]);
|
|
37
|
+
else if (!(merged[key] instanceof Set)) merged[key] = new Set(merged[key] ? [merged[key]] : []);
|
|
38
|
+
}
|
|
39
|
+
return merged;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function filterRecords(records, filters) {
|
|
43
|
+
if (!filters) return records;
|
|
44
|
+
const query = String(filters.search || "").trim().toLocaleLowerCase();
|
|
45
|
+
return records.filter((record) => {
|
|
46
|
+
if (query && !record.__meta.searchText.includes(query)) return false;
|
|
47
|
+
if (filters.recordTypes instanceof Set && !filters.recordTypes.has(record.recordType)) return false;
|
|
48
|
+
if (record.type && filters.types instanceof Set && !filters.types.has(record.type)) return false;
|
|
49
|
+
if (filters.factuality instanceof Set && !filters.factuality.has(record.factuality || "unknown")) return false;
|
|
50
|
+
if (filters.confidence instanceof Set && !filters.confidence.has(record.__meta.confidence || "unknown")) return false;
|
|
51
|
+
if (filters.scopes instanceof Set && !filters.scopes.has(record.__meta.scope || "local")) return false;
|
|
52
|
+
if (record.__meta.categories.length && filters.categories instanceof Set && !record.__meta.categories.some((category) => filters.categories.has(category))) return false;
|
|
53
|
+
if (record.__meta.countries.length && filters.countries instanceof Set && !record.__meta.countries.some((country) => filters.countries.has(country))) return false;
|
|
54
|
+
if (record.__meta.importance < Number(filters.minSignificance || 1)) return false;
|
|
55
|
+
if (filters.mediaOnly && !record.__meta.hasMedia) return false;
|
|
56
|
+
if (filters.uncertainOnly && !record.__meta.temporalUncertainty) return false;
|
|
57
|
+
if (Number.isFinite(filters.fromYear) && record.__meta.end < filters.fromYear) return false;
|
|
58
|
+
if (Number.isFinite(filters.toYear) && record.__meta.start > filters.toYear) return false;
|
|
59
|
+
return true;
|
|
60
|
+
});
|
|
61
|
+
}
|
package/src/i18n.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
export const UI_STRINGS = {
|
|
2
|
+
en: {
|
|
3
|
+
dataset: "Dataset",
|
|
4
|
+
language: "Language",
|
|
5
|
+
theme: "Theme",
|
|
6
|
+
orientation: "Orientation",
|
|
7
|
+
auto: "Auto",
|
|
8
|
+
horizontal: "Horizontal",
|
|
9
|
+
vertical: "Vertical",
|
|
10
|
+
axis: "Axis",
|
|
11
|
+
middle: "Middle",
|
|
12
|
+
sideStart: "Side start",
|
|
13
|
+
sideEnd: "Side end",
|
|
14
|
+
filters: "Filters",
|
|
15
|
+
search: "Search",
|
|
16
|
+
searchPlaceholder: "Search records",
|
|
17
|
+
recordTypes: "Record types",
|
|
18
|
+
specificTypes: "Specific types",
|
|
19
|
+
factuality: "Factuality",
|
|
20
|
+
confidence: "Confidence",
|
|
21
|
+
scope: "Scope",
|
|
22
|
+
categories: "Categories",
|
|
23
|
+
countries: "Countries",
|
|
24
|
+
dateRange: "Date range",
|
|
25
|
+
from: "From",
|
|
26
|
+
to: "To",
|
|
27
|
+
significance: "Significance",
|
|
28
|
+
mediaOnly: "Media only",
|
|
29
|
+
uncertainOnly: "Uncertain dates",
|
|
30
|
+
lod: "Adaptive detail",
|
|
31
|
+
explode: "Explode",
|
|
32
|
+
resetFilters: "Reset",
|
|
33
|
+
zoomIn: "Zoom in",
|
|
34
|
+
zoomOut: "Zoom out",
|
|
35
|
+
fit: "Fit",
|
|
36
|
+
resetView: "Reset view",
|
|
37
|
+
selected: "Selected",
|
|
38
|
+
noSelection: "Select a record",
|
|
39
|
+
records: "records",
|
|
40
|
+
visible: "visible",
|
|
41
|
+
hidden: "reduced",
|
|
42
|
+
span: "span",
|
|
43
|
+
details: "Details",
|
|
44
|
+
overview: "Overview",
|
|
45
|
+
dates: "Dates",
|
|
46
|
+
entities: "Entities",
|
|
47
|
+
places: "Places",
|
|
48
|
+
relationships: "Relationships",
|
|
49
|
+
interpretations: "Interpretations",
|
|
50
|
+
sources: "Sources",
|
|
51
|
+
funFacts: "Fun facts",
|
|
52
|
+
media: "Media",
|
|
53
|
+
extensions: "Extensions",
|
|
54
|
+
notes: "Notes",
|
|
55
|
+
type: "Type",
|
|
56
|
+
category: "Category",
|
|
57
|
+
rank: "Rank",
|
|
58
|
+
method: "Method",
|
|
59
|
+
basis: "Basis",
|
|
60
|
+
reliability: "Reliability",
|
|
61
|
+
noRecords: "No records match the filters.",
|
|
62
|
+
loading: "Loading",
|
|
63
|
+
all: "All",
|
|
64
|
+
clear: "Clear",
|
|
65
|
+
apply: "Apply",
|
|
66
|
+
ongoing: "present",
|
|
67
|
+
circa: "c.",
|
|
68
|
+
bce: "BCE",
|
|
69
|
+
ce: "CE",
|
|
70
|
+
lessDetail: "compact",
|
|
71
|
+
fullDetail: "full",
|
|
72
|
+
mediaOpen: "Open media",
|
|
73
|
+
sourceOpen: "Open source",
|
|
74
|
+
hiddenEvents: "hidden events",
|
|
75
|
+
clusterHint: "Click to expand temporarily",
|
|
76
|
+
clusterExpanded: "{count} hidden events expanded",
|
|
77
|
+
timelineOverview: "Timeline overview",
|
|
78
|
+
zoomWindow: "Current viewing window",
|
|
79
|
+
zoomSelection: "Selected zoom range",
|
|
80
|
+
currentView: "{count} of {total} records",
|
|
81
|
+
zoomLevel: "{span} years",
|
|
82
|
+
attestation: "Attestation",
|
|
83
|
+
originalDate: "Original",
|
|
84
|
+
statusReady: "{visible} visible, {hidden} reduced by detail level"
|
|
85
|
+
},
|
|
86
|
+
fa: {
|
|
87
|
+
dataset: "مجموعه",
|
|
88
|
+
language: "زبان",
|
|
89
|
+
theme: "پوسته",
|
|
90
|
+
orientation: "جهت",
|
|
91
|
+
auto: "خودکار",
|
|
92
|
+
horizontal: "افقی",
|
|
93
|
+
vertical: "عمودی",
|
|
94
|
+
axis: "محور",
|
|
95
|
+
middle: "میانه",
|
|
96
|
+
sideStart: "کنار آغاز",
|
|
97
|
+
sideEnd: "کنار پایان",
|
|
98
|
+
filters: "فیلترها",
|
|
99
|
+
search: "جستجو",
|
|
100
|
+
searchPlaceholder: "جستجوی رخدادها",
|
|
101
|
+
recordTypes: "گونه رکورد",
|
|
102
|
+
specificTypes: "نوع های دقیق",
|
|
103
|
+
factuality: "اعتبار تاریخی",
|
|
104
|
+
confidence: "اطمینان",
|
|
105
|
+
scope: "گستره",
|
|
106
|
+
categories: "دسته ها",
|
|
107
|
+
countries: "کشورها",
|
|
108
|
+
dateRange: "بازه زمانی",
|
|
109
|
+
from: "از",
|
|
110
|
+
to: "تا",
|
|
111
|
+
significance: "اهمیت",
|
|
112
|
+
mediaOnly: "فقط رسانه",
|
|
113
|
+
uncertainOnly: "تاریخ نامطمئن",
|
|
114
|
+
lod: "جزئیات تطبیقی",
|
|
115
|
+
explode: "نمای انفجاری",
|
|
116
|
+
resetFilters: "بازنشانی",
|
|
117
|
+
zoomIn: "بزرگ نمایی",
|
|
118
|
+
zoomOut: "کوچک نمایی",
|
|
119
|
+
fit: "نمای کامل",
|
|
120
|
+
resetView: "بازنشانی نما",
|
|
121
|
+
selected: "انتخاب شده",
|
|
122
|
+
noSelection: "یک رکورد را انتخاب کنید",
|
|
123
|
+
records: "رکورد",
|
|
124
|
+
visible: "نمایان",
|
|
125
|
+
hidden: "کاهش یافته",
|
|
126
|
+
span: "بازه",
|
|
127
|
+
details: "جزئیات",
|
|
128
|
+
overview: "نمای کلی",
|
|
129
|
+
dates: "تاریخ ها",
|
|
130
|
+
entities: "کنشگران",
|
|
131
|
+
places: "مکان ها",
|
|
132
|
+
relationships: "روابط",
|
|
133
|
+
interpretations: "تفسیرها",
|
|
134
|
+
sources: "منابع",
|
|
135
|
+
funFacts: "نکته ها",
|
|
136
|
+
media: "رسانه",
|
|
137
|
+
extensions: "افزونه ها",
|
|
138
|
+
notes: "یادداشت",
|
|
139
|
+
type: "نوع",
|
|
140
|
+
category: "دسته",
|
|
141
|
+
rank: "رتبه",
|
|
142
|
+
method: "روش",
|
|
143
|
+
basis: "پشتوانه",
|
|
144
|
+
reliability: "اعتبار",
|
|
145
|
+
noRecords: "هیچ رکوردی با فیلترها سازگار نیست.",
|
|
146
|
+
loading: "در حال بارگذاری",
|
|
147
|
+
all: "همه",
|
|
148
|
+
clear: "پاک کردن",
|
|
149
|
+
apply: "اعمال",
|
|
150
|
+
ongoing: "اکنون",
|
|
151
|
+
circa: "حدود",
|
|
152
|
+
bce: "پیش از میلاد",
|
|
153
|
+
ce: "میلادی",
|
|
154
|
+
lessDetail: "فشرده",
|
|
155
|
+
fullDetail: "کامل",
|
|
156
|
+
mediaOpen: "باز کردن رسانه",
|
|
157
|
+
sourceOpen: "باز کردن منبع",
|
|
158
|
+
hiddenEvents: "رخداد پنهان",
|
|
159
|
+
clusterHint: "برای باز شدن موقت کلیک کنید",
|
|
160
|
+
clusterExpanded: "{count} رخداد پنهان باز شده",
|
|
161
|
+
timelineOverview: "نمای کلی زمان",
|
|
162
|
+
zoomWindow: "پنجره نمای کنونی",
|
|
163
|
+
zoomSelection: "بازه بزرگ نمایی انتخاب شده",
|
|
164
|
+
currentView: "{count} از {total} رکورد",
|
|
165
|
+
zoomLevel: "{span} سال",
|
|
166
|
+
attestation: "گواهی تاریخی",
|
|
167
|
+
originalDate: "اصل تاریخ",
|
|
168
|
+
statusReady: "{visible} نمایان، {hidden} با سطح جزئیات کاهش یافته"
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export function dirForLanguage(language) {
|
|
173
|
+
return /^(fa|ar|he|ur)(-|$)/i.test(language) ? "rtl" : "ltr";
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function makeTranslator(language) {
|
|
177
|
+
const strings = UI_STRINGS[language] || UI_STRINGS.en;
|
|
178
|
+
return function translate(key, values = {}) {
|
|
179
|
+
const template = strings[key] || UI_STRINGS.en[key] || key;
|
|
180
|
+
return template.replace(/\{([a-zA-Z0-9_]+)\}/g, (_, name) => {
|
|
181
|
+
return Object.prototype.hasOwnProperty.call(values, name) ? String(values[name]) : "";
|
|
182
|
+
});
|
|
183
|
+
};
|
|
184
|
+
}
|