@easyoref/shared 1.21.1
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/dist/config.d.ts +128 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +158 -0
- package/dist/config.js.map +1 -0
- package/dist/helpers.d.ts +6 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +15 -0
- package/dist/helpers.js.map +1 -0
- package/dist/i18n.d.ts +51 -0
- package/dist/i18n.d.ts.map +1 -0
- package/dist/i18n.js +248 -0
- package/dist/i18n.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/redis.d.ts +6 -0
- package/dist/redis.d.ts.map +1 -0
- package/dist/redis.js +21 -0
- package/dist/redis.js.map +1 -0
- package/dist/schemas.d.ts +1496 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +556 -0
- package/dist/schemas.js.map +1 -0
- package/dist/store.d.ts +55 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +162 -0
- package/dist/store.js.map +1 -0
- package/package.json +22 -0
- package/src/config.ts +248 -0
- package/src/helpers.ts +17 -0
- package/src/i18n.ts +306 -0
- package/src/index.ts +6 -0
- package/src/redis.ts +23 -0
- package/src/schemas.ts +712 -0
- package/src/store.ts +232 -0
- package/tsconfig.json +9 -0
package/src/i18n.ts
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EasyOref — Internationalization (i18n)
|
|
3
|
+
*
|
|
4
|
+
* Supported languages: Russian (ru), English (en), Hebrew (he), Arabic (ar)
|
|
5
|
+
* Default: ru (built for diaspora families)
|
|
6
|
+
*
|
|
7
|
+
* Message format:
|
|
8
|
+
* <b>⚠️ Title</b>
|
|
9
|
+
* Description
|
|
10
|
+
* <blockquote>
|
|
11
|
+
* <b>Key:</b> Value
|
|
12
|
+
* ...
|
|
13
|
+
* </blockquote>
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export type Language = "ru" | "en" | "he" | "ar";
|
|
17
|
+
|
|
18
|
+
// ── Alert metadata types ─────────────────────────────────
|
|
19
|
+
|
|
20
|
+
export type AlertKind = "early" | "red_alert" | "resolved";
|
|
21
|
+
|
|
22
|
+
export interface AlertLocales {
|
|
23
|
+
emoji: string;
|
|
24
|
+
title: string;
|
|
25
|
+
description: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface I18nLabels {
|
|
29
|
+
area: string;
|
|
30
|
+
timeToImpact: string;
|
|
31
|
+
earlyEta: string;
|
|
32
|
+
redAlertEta: string;
|
|
33
|
+
monitoring: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface LanguagePack {
|
|
37
|
+
alerts: Record<AlertKind, AlertLocales>;
|
|
38
|
+
labels: I18nLabels;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
42
|
+
// Language Packs — structured alert metadata + labels
|
|
43
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
44
|
+
|
|
45
|
+
const ruPack: LanguagePack = {
|
|
46
|
+
alerts: {
|
|
47
|
+
early: {
|
|
48
|
+
emoji: "⚠️",
|
|
49
|
+
title: "Раннее предупреждение",
|
|
50
|
+
description: "Обнаружены запуски ракет по Израилю.",
|
|
51
|
+
},
|
|
52
|
+
red_alert: {
|
|
53
|
+
emoji: "🚨",
|
|
54
|
+
title: "Цева Адом",
|
|
55
|
+
description: "",
|
|
56
|
+
},
|
|
57
|
+
resolved: {
|
|
58
|
+
emoji: "😮💨",
|
|
59
|
+
title: "Инцидент завершён",
|
|
60
|
+
description: "Можно покинуть защищённое помещение.",
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
labels: {
|
|
64
|
+
area: "Район",
|
|
65
|
+
timeToImpact: "Подлёт",
|
|
66
|
+
earlyEta: "~5–12 мин",
|
|
67
|
+
redAlertEta: "1.5 мин",
|
|
68
|
+
monitoring:
|
|
69
|
+
'<tg-emoji emoji-id="5258052328455424397">⏳</tg-emoji> Сообщение обновляется...',
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const enPack: LanguagePack = {
|
|
74
|
+
alerts: {
|
|
75
|
+
early: {
|
|
76
|
+
emoji: "⚠️",
|
|
77
|
+
title: "Early Warning",
|
|
78
|
+
description: "Rocket launches detected. Stay near a protected space.",
|
|
79
|
+
},
|
|
80
|
+
red_alert: {
|
|
81
|
+
emoji: "🚨",
|
|
82
|
+
title: "Siren Alert",
|
|
83
|
+
description: "Enter a protected space immediately.",
|
|
84
|
+
},
|
|
85
|
+
resolved: {
|
|
86
|
+
emoji: "😮💨",
|
|
87
|
+
title: "Incident Over",
|
|
88
|
+
description: "You may leave the protected space.",
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
labels: {
|
|
92
|
+
area: "Area",
|
|
93
|
+
timeToImpact: "Time to impact",
|
|
94
|
+
earlyEta: "~5–12 min",
|
|
95
|
+
redAlertEta: "1.5 min",
|
|
96
|
+
monitoring:
|
|
97
|
+
'<tg-emoji emoji-id="5258052328455424397">⏳</tg-emoji> Message updating...',
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const hePack: LanguagePack = {
|
|
102
|
+
alerts: {
|
|
103
|
+
early: {
|
|
104
|
+
emoji: "⚠️",
|
|
105
|
+
title: "התרעה מוקדמת",
|
|
106
|
+
description: "זוהו שיגורים. הישארו בקרבת מרחב מוגן.",
|
|
107
|
+
},
|
|
108
|
+
red_alert: {
|
|
109
|
+
emoji: "🚨",
|
|
110
|
+
title: "צבע אדום",
|
|
111
|
+
description: "היכנסו למרחב מוגן.",
|
|
112
|
+
},
|
|
113
|
+
resolved: {
|
|
114
|
+
emoji: "😮💨",
|
|
115
|
+
title: "האירוע הסתיים",
|
|
116
|
+
description: "ניתן לצאת מהמרחב המוגן.",
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
labels: {
|
|
120
|
+
area: "אזור",
|
|
121
|
+
timeToImpact: "זמן מעוף",
|
|
122
|
+
earlyEta: "~5–12 דקות",
|
|
123
|
+
redAlertEta: "1.5 דקות",
|
|
124
|
+
monitoring:
|
|
125
|
+
'<tg-emoji emoji-id="5258052328455424397">⏳</tg-emoji> ההודעה מתעדכנת...',
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const arPack: LanguagePack = {
|
|
130
|
+
alerts: {
|
|
131
|
+
early: {
|
|
132
|
+
emoji: "⚠️",
|
|
133
|
+
title: "إنذار مبكر",
|
|
134
|
+
description: "تم رصد إطلاق صواريخ. ابقوا بالقرب من الملجأ.",
|
|
135
|
+
},
|
|
136
|
+
red_alert: {
|
|
137
|
+
emoji: "🚨",
|
|
138
|
+
title: "صفارة إنذار",
|
|
139
|
+
description: "ادخلوا إلى الملجأ فوراً.",
|
|
140
|
+
},
|
|
141
|
+
resolved: {
|
|
142
|
+
emoji: "😮💨",
|
|
143
|
+
title: "انتهى الحادث",
|
|
144
|
+
description: "يمكنكم مغادرة الملجأ.",
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
labels: {
|
|
148
|
+
area: "المنطقة",
|
|
149
|
+
timeToImpact: "وقت الوصول",
|
|
150
|
+
earlyEta: "~5–12 دقيقة",
|
|
151
|
+
redAlertEta: "1.5 دقيقة",
|
|
152
|
+
monitoring:
|
|
153
|
+
'<tg-emoji emoji-id="5258052328455424397">⏳</tg-emoji> الرسالة قيد التحديث...',
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const packs: Record<Language, LanguagePack> = {
|
|
158
|
+
ru: ruPack,
|
|
159
|
+
en: enPack,
|
|
160
|
+
he: hePack,
|
|
161
|
+
ar: arPack,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
165
|
+
// Area Name Translation — loaded from pikud-haoref-api/cities.json
|
|
166
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
167
|
+
|
|
168
|
+
const CITIES_JSON_URL =
|
|
169
|
+
"https://raw.githubusercontent.com/eladnava/pikud-haoref-api/master/cities.json";
|
|
170
|
+
|
|
171
|
+
interface CityEntry {
|
|
172
|
+
id: number;
|
|
173
|
+
name: string;
|
|
174
|
+
name_en: string;
|
|
175
|
+
name_ru: string;
|
|
176
|
+
name_ar: string;
|
|
177
|
+
zone: string;
|
|
178
|
+
zone_en: string;
|
|
179
|
+
zone_ru: string;
|
|
180
|
+
zone_ar: string;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
type LangKey = "en" | "ru" | "ar";
|
|
184
|
+
|
|
185
|
+
/** Hebrew city name → { en, ru, ar } */
|
|
186
|
+
const cityMap = new Map<string, Record<LangKey, string>>();
|
|
187
|
+
/** Hebrew zone name → { en, ru, ar } */
|
|
188
|
+
const zoneMap = new Map<string, Record<LangKey, string>>();
|
|
189
|
+
/** City ID → Hebrew name (for YAML city_ids resolution) */
|
|
190
|
+
const idToNameMap = new Map<number, string>();
|
|
191
|
+
|
|
192
|
+
/** Country name translation map (Source: agent extraction names) */
|
|
193
|
+
const COUNTRY_NAMES: Record<string, Record<Exclude<Language, "he">, string>> = {
|
|
194
|
+
Iran: { ru: "Иран", en: "Iran", ar: "إيران" },
|
|
195
|
+
Yemen: { ru: "Йемен", en: "Yemen", ar: "اليمن" },
|
|
196
|
+
Lebanon: { ru: "Ливан", en: "Lebanon", ar: "لبنان" },
|
|
197
|
+
Gaza: { ru: "Газа", en: "Gaza", ar: "غزة" },
|
|
198
|
+
Iraq: { ru: "Ирак", en: "Iraq", ar: "العراق" },
|
|
199
|
+
Syria: { ru: "Сирия", en: "Syria", ar: "سوريا" },
|
|
200
|
+
Hezbollah: { ru: "Хезболла", en: "Hezbollah", ar: "حزب الله" },
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Known bad translations in upstream cities.json.
|
|
205
|
+
* Applied as post-processing corrections after loading.
|
|
206
|
+
* Key: Hebrew name, Value: { lang: corrected_name }
|
|
207
|
+
*/
|
|
208
|
+
const TRANSLATION_FIXES: Record<string, Partial<Record<LangKey, string>>> = {
|
|
209
|
+
"תל אביב - דרום העיר ויפו": {
|
|
210
|
+
ru: "Тель-Авив — Южный район и Яффо",
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Load and cache translations from pikud-haoref-api.
|
|
216
|
+
* Must be called once at startup (before first alert).
|
|
217
|
+
* Falls back silently to Hebrew names on fetch failure.
|
|
218
|
+
*/
|
|
219
|
+
export async function initTranslations(): Promise<void> {
|
|
220
|
+
try {
|
|
221
|
+
const res = await fetch(CITIES_JSON_URL);
|
|
222
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
223
|
+
const data: CityEntry[] = (await res.json()) as CityEntry[];
|
|
224
|
+
|
|
225
|
+
for (const c of data) {
|
|
226
|
+
if (!c.name || c.name === "בחר הכל") continue;
|
|
227
|
+
cityMap.set(c.name, { en: c.name_en, ru: c.name_ru, ar: c.name_ar });
|
|
228
|
+
if (c.id) idToNameMap.set(c.id, c.name);
|
|
229
|
+
if (c.zone && !zoneMap.has(c.zone)) {
|
|
230
|
+
zoneMap.set(c.zone, { en: c.zone_en, ru: c.zone_ru, ar: c.zone_ar });
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Apply known corrections over upstream data
|
|
235
|
+
for (const [heName, fixes] of Object.entries(TRANSLATION_FIXES)) {
|
|
236
|
+
const existing = cityMap.get(heName);
|
|
237
|
+
if (existing) {
|
|
238
|
+
for (const [lang, corrected] of Object.entries(fixes)) {
|
|
239
|
+
existing[lang as LangKey] = corrected!;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// eslint-disable-next-line no-console
|
|
245
|
+
console.log(
|
|
246
|
+
`[i18n] Loaded ${cityMap.size} city + ${zoneMap.size} zone translations`,
|
|
247
|
+
);
|
|
248
|
+
} catch (err) {
|
|
249
|
+
// eslint-disable-next-line no-console
|
|
250
|
+
console.warn(
|
|
251
|
+
"[i18n] Failed to load cities.json — area names will stay in Hebrew",
|
|
252
|
+
err,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Translate comma-separated Hebrew area names to target language */
|
|
258
|
+
export function translateAreas(areas: string, lang: Language): string {
|
|
259
|
+
if (lang === "he") return areas;
|
|
260
|
+
const key: LangKey = lang;
|
|
261
|
+
return areas
|
|
262
|
+
.split(", ")
|
|
263
|
+
.map((a) => {
|
|
264
|
+
const city = cityMap.get(a);
|
|
265
|
+
if (city?.[key]) return city[key];
|
|
266
|
+
const zone = zoneMap.get(a);
|
|
267
|
+
if (zone?.[key]) return zone[key];
|
|
268
|
+
return a; // fallback: Hebrew as-is
|
|
269
|
+
})
|
|
270
|
+
.join(", ");
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Translate country name from English (LLM extraction result) to target language */
|
|
274
|
+
export function translateCountry(name: string, lang: Language): string {
|
|
275
|
+
if (lang === "he") return name; // Fallback: no Hebrew country map yet
|
|
276
|
+
const entry = COUNTRY_NAMES[name];
|
|
277
|
+
if (entry) return entry[lang];
|
|
278
|
+
return name;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Resolve numeric city IDs to Hebrew area names.
|
|
283
|
+
* Call AFTER initTranslations().
|
|
284
|
+
* Unknown IDs are logged as warnings and skipped.
|
|
285
|
+
*/
|
|
286
|
+
export function resolveCityIds(ids: number[]): string[] {
|
|
287
|
+
const names: string[] = [];
|
|
288
|
+
for (const id of ids) {
|
|
289
|
+
const name = idToNameMap.get(id);
|
|
290
|
+
if (name) {
|
|
291
|
+
names.push(name);
|
|
292
|
+
} else {
|
|
293
|
+
// eslint-disable-next-line no-console
|
|
294
|
+
console.warn(`[i18n] Unknown city ID: ${id} — skipping`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return names;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function getLanguagePack(lang: Language): LanguagePack {
|
|
301
|
+
return packs[lang] ?? packs.ru;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function isValidLanguage(s: string): s is Language {
|
|
305
|
+
return s === "ru" || s === "en" || s === "he" || s === "ar";
|
|
306
|
+
}
|
package/src/index.ts
ADDED
package/src/redis.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Redis } from "ioredis";
|
|
2
|
+
import { config } from "./config.js";
|
|
3
|
+
|
|
4
|
+
let redis: Redis | undefined;
|
|
5
|
+
|
|
6
|
+
/** Get a lazy-connected Redis instance */
|
|
7
|
+
export function getRedis(): Redis {
|
|
8
|
+
if (!redis) {
|
|
9
|
+
redis = new Redis(config.agent.redisUrl, {
|
|
10
|
+
maxRetriesPerRequest: null, // Required by BullMQ
|
|
11
|
+
lazyConnect: true,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
return redis;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Close the Redis connection (for clean shutdown) */
|
|
18
|
+
export async function closeRedis(): Promise<void> {
|
|
19
|
+
if (redis) {
|
|
20
|
+
await redis.quit();
|
|
21
|
+
redis = undefined;
|
|
22
|
+
}
|
|
23
|
+
}
|