@next-vibe/checker 1.0.11

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.
@@ -0,0 +1,236 @@
1
+ import type {
2
+ Countries,
3
+ CountryInfo,
4
+ CountryLanguage,
5
+ Languages,
6
+ } from "./config";
7
+ import { availableCountries } from "./config";
8
+
9
+ // ================================================================================
10
+ // TYPES
11
+ // ================================================================================
12
+
13
+ /**
14
+ * Type-safe mapping of language codes to their information
15
+ */
16
+ export type LanguageGroupMap = {
17
+ [langCode in Languages]: {
18
+ name: string;
19
+ countries: CountryInfo[];
20
+ };
21
+ };
22
+
23
+ // ================================================================================
24
+ // LANGUAGE MAPPING SINGLETON
25
+ // ================================================================================
26
+
27
+ /**
28
+ * Singleton class for efficient language mapping operations
29
+ */
30
+ let instance: LanguageMapper | null = null;
31
+ class LanguageMapper {
32
+ private _uniqueLanguages:
33
+ | [
34
+ langCode: Languages,
35
+ langInfo: { name: string; countries: CountryInfo[] },
36
+ ][]
37
+ | null = null;
38
+ private _languageGroupMap: LanguageGroupMap | null = null;
39
+
40
+ private constructor() {
41
+ // Private constructor for singleton pattern
42
+ }
43
+
44
+ /**
45
+ * Get the singleton instance
46
+ */
47
+ static getInstance(): LanguageMapper {
48
+ if (!instance) {
49
+ instance = new LanguageMapper();
50
+ }
51
+ return instance;
52
+ }
53
+
54
+ /**
55
+ * Get the language group map (computed once and cached)
56
+ */
57
+ private getLanguageGroupMap(): LanguageGroupMap {
58
+ if (this._languageGroupMap === null) {
59
+ this._languageGroupMap = availableCountries.reduce((acc, curr) => {
60
+ const language = curr.language;
61
+
62
+ if (!acc[language]) {
63
+ acc[language] = {
64
+ name: curr.langName,
65
+ countries: [],
66
+ };
67
+ }
68
+ // Create a mutable copy to avoid readonly issues
69
+ acc[language].countries = [...acc[language].countries, curr];
70
+ return acc;
71
+ }, {} as LanguageGroupMap);
72
+ }
73
+ return this._languageGroupMap;
74
+ }
75
+
76
+ /**
77
+ * Get unique languages with their associated countries (computed once and cached)
78
+ */
79
+ getUniqueLanguages(): [
80
+ langCode: Languages,
81
+ langInfo: { name: string; countries: CountryInfo[] },
82
+ ][] {
83
+ if (this._uniqueLanguages === null) {
84
+ const languageGroupMap = this.getLanguageGroupMap();
85
+ this._uniqueLanguages = Object.entries(languageGroupMap) as [
86
+ Languages,
87
+ { name: string; countries: CountryInfo[] },
88
+ ][];
89
+ }
90
+ return this._uniqueLanguages;
91
+ }
92
+
93
+ /**
94
+ * Get countries for a specific language
95
+ */
96
+ getCountriesForLanguage(language: Languages): CountryInfo[] {
97
+ const languageGroupMap = this.getLanguageGroupMap();
98
+
99
+ return languageGroupMap[language]?.countries || [];
100
+ }
101
+
102
+ /**
103
+ * Get language name for a specific language code
104
+ */
105
+ getLanguageName(language: Languages): string {
106
+ const languageGroupMap = this.getLanguageGroupMap();
107
+
108
+ return languageGroupMap[language]?.name || "";
109
+ }
110
+
111
+ /**
112
+ * Check if a language has multiple countries
113
+ */
114
+ hasMultipleCountries(language: Languages): boolean {
115
+ return this.getCountriesForLanguage(language).length > 1;
116
+ }
117
+
118
+ /**
119
+ * Get the primary country for a language (first one in the list)
120
+ */
121
+ getPrimaryCountryForLanguage(language: Languages): CountryInfo | null {
122
+ const countries = this.getCountriesForLanguage(language);
123
+ return countries.length > 0 ? countries[0] : null;
124
+ }
125
+
126
+ /**
127
+ * Reset the cache (useful for testing or if country data changes)
128
+ */
129
+ resetCache(): void {
130
+ this._uniqueLanguages = null;
131
+ this._languageGroupMap = null;
132
+ }
133
+ }
134
+
135
+ // ================================================================================
136
+ // LANGUAGE UTILITY FUNCTIONS
137
+ // ================================================================================
138
+
139
+ /**
140
+ * Convenience function to get the singleton instance
141
+ */
142
+ export const getLanguageMapper = (): LanguageMapper => {
143
+ return LanguageMapper.getInstance();
144
+ };
145
+
146
+ /**
147
+ * Convenience function to get unique languages (most common use case)
148
+ */
149
+ export const getUniqueLanguages = (): [
150
+ langCode: Languages,
151
+ langInfo: { name: string; countries: CountryInfo[] },
152
+ ][] => {
153
+ return getLanguageMapper().getUniqueLanguages();
154
+ };
155
+
156
+ /**
157
+ * Convenience function to get countries for a specific language
158
+ */
159
+ export const getCountriesForLanguage = (language: Languages): CountryInfo[] => {
160
+ return getLanguageMapper().getCountriesForLanguage(language);
161
+ };
162
+
163
+ /**
164
+ * Convenience function to get language name
165
+ */
166
+ export const getLanguageName = (language: Languages): string => {
167
+ return getLanguageMapper().getLanguageName(language);
168
+ };
169
+
170
+ /**
171
+ * Convenience function to check if a language has multiple countries
172
+ */
173
+ export const hasMultipleCountries = (language: Languages): boolean => {
174
+ return getLanguageMapper().hasMultipleCountries(language);
175
+ };
176
+
177
+ /**
178
+ * Convenience function to get the primary country for a language
179
+ */
180
+ export const getPrimaryCountryForLanguage = (
181
+ language: Languages,
182
+ ): CountryInfo | null => {
183
+ return getLanguageMapper().getPrimaryCountryForLanguage(language);
184
+ };
185
+
186
+ /**
187
+ * Extract country code from locale string
188
+ */
189
+ export function getCountryFromLocale(locale: CountryLanguage): Countries {
190
+ const parts = locale.split("-");
191
+ if (parts.length !== 2 || !parts[1]) {
192
+ // Return a default value instead of throwing
193
+ return "GLOBAL" as Countries;
194
+ }
195
+ return parts[1] as Countries;
196
+ }
197
+
198
+ /**
199
+ * Extract language code from locale string
200
+ */
201
+ export function getLanguageFromLocale(locale: CountryLanguage): Languages {
202
+ const parts = locale.split("-");
203
+ if (parts.length !== 2 || !parts[0]) {
204
+ // Return a default value instead of throwing
205
+ return "en" as Languages;
206
+ }
207
+ return parts[0] as Languages;
208
+ }
209
+
210
+ /**
211
+ * Extract both language and country codes from locale string
212
+ */
213
+ export function getLanguageAndCountryFromLocale(locale: CountryLanguage): {
214
+ language: Languages;
215
+ country: Countries;
216
+ } {
217
+ const parts = locale.split("-");
218
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
219
+ // Return default values instead of throwing
220
+ return {
221
+ language: "en" as Languages,
222
+ country: "GLOBAL" as Countries,
223
+ };
224
+ }
225
+ return {
226
+ language: parts[0] as Languages,
227
+ country: parts[1] as Countries,
228
+ };
229
+ }
230
+
231
+ export function getLocaleFromLanguageAndCountry(
232
+ language: Languages,
233
+ country: Countries,
234
+ ): CountryLanguage {
235
+ return `${language}-${country}` as CountryLanguage;
236
+ }
@@ -0,0 +1,422 @@
1
+ /**
2
+ * Localization Utilities
3
+ * Centralized utilities for date, time, and currency formatting with proper localization
4
+ */
5
+
6
+ import { format } from "date-fns";
7
+ import { de, enUS, type Locale, pl } from "date-fns/locale";
8
+
9
+ import type { CountryLanguage, Currencies } from "./config";
10
+ import { getCountryFromLocale } from "./language-utils";
11
+ import { simpleT } from "./shared";
12
+
13
+ /**
14
+ * Get locale string from CountryLanguage format
15
+ * Converts "en-GLOBAL" to "en-US", "de-DE" to "de-DE", etc.
16
+ */
17
+ export function getLocaleString(countryLanguage: CountryLanguage): string {
18
+ const [lang, country] = countryLanguage.split("-");
19
+
20
+ // Map country codes to proper locale strings
21
+ switch (country) {
22
+ case "DE":
23
+ return "de-DE";
24
+ case "PL":
25
+ return "pl-PL";
26
+ case "AT":
27
+ return "de-AT";
28
+ case "CH":
29
+ return lang === "de" ? "de-CH" : "fr-CH";
30
+ case "GLOBAL":
31
+ case "US":
32
+ default:
33
+ return "en-US";
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Check if locale should use 24-hour time format
39
+ */
40
+ export function shouldUse24HourFormat(
41
+ countryLanguage: CountryLanguage,
42
+ ): boolean {
43
+ const country = countryLanguage.split("-")[1];
44
+ return country === "DE" || country === "PL" || country === "AT";
45
+ }
46
+
47
+ /**
48
+ * Get date-fns locale object based on country language
49
+ */
50
+ export function getDateFnsLocale(countryLanguage: CountryLanguage): Locale {
51
+ const country = countryLanguage.split("-")[1];
52
+ switch (country) {
53
+ case "DE":
54
+ case "AT":
55
+ return de;
56
+ case "PL":
57
+ return pl;
58
+ default:
59
+ return enUS;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Format date with proper localization using date-fns
65
+ */
66
+ export function formatDate(
67
+ date: Date,
68
+ locale: CountryLanguage,
69
+ formatString = "PPP",
70
+ ): string {
71
+ const dateFnsLocale = getDateFnsLocale(locale);
72
+ return format(date, formatString, { locale: dateFnsLocale });
73
+ }
74
+
75
+ /**
76
+ * Format date for display with native Intl.DateTimeFormat
77
+ */
78
+ export function formatDateForDisplay(
79
+ date: Date,
80
+ locale: CountryLanguage,
81
+ ): string {
82
+ const localeString = getLocaleString(locale);
83
+ return date.toLocaleDateString(localeString, {
84
+ weekday: "long",
85
+ year: "numeric",
86
+ month: "long",
87
+ day: "numeric",
88
+ });
89
+ }
90
+
91
+ /**
92
+ * Format time for display with locale-specific formatting
93
+ */
94
+ export function formatTimeForDisplay(
95
+ time: string,
96
+ locale: CountryLanguage,
97
+ ): string {
98
+ const [hours, minutes] = time.split(":").map(Number);
99
+ const date = new Date();
100
+ date.setHours(hours, minutes, 0, 0);
101
+
102
+ const localeString = getLocaleString(locale);
103
+ const use24Hour = shouldUse24HourFormat(locale);
104
+
105
+ return date.toLocaleTimeString(localeString, {
106
+ hour: "2-digit",
107
+ minute: "2-digit",
108
+ hour12: !use24Hour,
109
+ });
110
+ }
111
+
112
+ /**
113
+ * Format timestamp for display with proper localization
114
+ */
115
+ export function formatTimestamp(
116
+ timestamp: string | Date,
117
+ locale: CountryLanguage,
118
+ ): string {
119
+ try {
120
+ const date =
121
+ typeof timestamp === "string" ? new Date(timestamp) : timestamp;
122
+ const localeString = getLocaleString(locale);
123
+ const use24Hour = shouldUse24HourFormat(locale);
124
+
125
+ return new Intl.DateTimeFormat(localeString, {
126
+ month: "short",
127
+ day: "numeric",
128
+ hour: "numeric",
129
+ minute: "numeric",
130
+ hour12: !use24Hour,
131
+ }).format(date);
132
+ } catch {
133
+ // Error formatting timestamp - return raw value
134
+ return typeof timestamp === "string" ? timestamp : timestamp.toString();
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Format currency with proper localization
140
+ */
141
+ export function formatCurrency(
142
+ amount: number,
143
+ currency: Currencies,
144
+ locale: CountryLanguage,
145
+ ): string {
146
+ const localeString = getLocaleString(locale);
147
+ return new Intl.NumberFormat(localeString, {
148
+ style: "currency",
149
+ currency,
150
+ }).format(amount);
151
+ }
152
+
153
+ /**
154
+ * Format currency without decimal places for leads pricing
155
+ */
156
+ export function formatCurrencyNoDecimals(
157
+ amount: number,
158
+ currency: string,
159
+ locale: CountryLanguage,
160
+ ): string {
161
+ const localeString = getLocaleString(locale);
162
+ const formatted = new Intl.NumberFormat(localeString, {
163
+ style: "currency",
164
+ currency,
165
+ minimumFractionDigits: 0,
166
+ maximumFractionDigits: 0,
167
+ }).format(amount);
168
+
169
+ // Remove spaces between currency symbol and amount
170
+ return formatted.replaceAll(/\s/g, "");
171
+ }
172
+
173
+ /**
174
+ * Format time string to HH:MM format (internal use)
175
+ */
176
+ export function formatTimeString(date: Date): string {
177
+ return `${date.getHours().toString().padStart(2, "0")}:${date.getMinutes().toString().padStart(2, "0")}`;
178
+ }
179
+
180
+ /**
181
+ * Format simple date string for locale
182
+ */
183
+ export function formatSimpleDate(
184
+ date: Date | string,
185
+ locale: CountryLanguage,
186
+ ): string {
187
+ const dateObj = typeof date === "string" ? new Date(date) : date;
188
+ const localeString = getLocaleString(locale);
189
+
190
+ return dateObj.toLocaleDateString(localeString, {
191
+ year: "numeric",
192
+ month: "long",
193
+ day: "numeric",
194
+ });
195
+ }
196
+
197
+ /**
198
+ * Format time with timezone support
199
+ */
200
+ export function formatTimeWithTimezone(
201
+ date: Date,
202
+ timezone: string,
203
+ locale: CountryLanguage,
204
+ ): string {
205
+ const localeString = getLocaleString(locale);
206
+ const use24Hour = shouldUse24HourFormat(locale);
207
+
208
+ return date.toLocaleTimeString(localeString, {
209
+ timeZone: timezone,
210
+ hour: "2-digit",
211
+ minute: "2-digit",
212
+ hour12: !use24Hour,
213
+ });
214
+ }
215
+
216
+ /**
217
+ * Get current time in timezone with locale formatting
218
+ */
219
+ export function getCurrentTimeInTimezone(
220
+ timezone: string,
221
+ locale: CountryLanguage,
222
+ ): string {
223
+ try {
224
+ const localeString = getLocaleString(locale);
225
+ const use24Hour = shouldUse24HourFormat(locale);
226
+ return new Date().toLocaleTimeString(localeString, {
227
+ timeZone: timezone,
228
+ hour: "2-digit",
229
+ minute: "2-digit",
230
+ hour12: !use24Hour,
231
+ });
232
+ } catch {
233
+ return "--:--";
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Get default timezone based on locale
239
+ * Returns appropriate timezone for the given locale
240
+ */
241
+ export function getDefaultTimezone(locale: CountryLanguage): string {
242
+ const { t } = simpleT(locale);
243
+ const country = getCountryFromLocale(locale);
244
+ const timezone = t(`config.timezone.${country}` as Parameters<typeof t>[0]);
245
+ return timezone;
246
+ }
247
+
248
+ /**
249
+ * Format date with full weekday, month, day, year
250
+ */
251
+ export function formatFullDate(date: Date, locale: CountryLanguage): string {
252
+ const dateObj = date;
253
+ const localeString = getLocaleString(locale);
254
+
255
+ return dateObj.toLocaleDateString(localeString, {
256
+ weekday: "long",
257
+ year: "numeric",
258
+ month: "long",
259
+ day: "numeric",
260
+ });
261
+ }
262
+
263
+ /**
264
+ * Format date with short format
265
+ */
266
+ export function formatShortDate(date: Date, locale: CountryLanguage): string {
267
+ const dateObj = date;
268
+ const localeString = getLocaleString(locale);
269
+
270
+ return dateObj.toLocaleDateString(localeString, {
271
+ year: "numeric",
272
+ month: "short",
273
+ day: "numeric",
274
+ });
275
+ }
276
+
277
+ /**
278
+ * Format single date to YYYY-MM-DD string with timezone support
279
+ * Uses en-CA locale for consistent YYYY-MM-DD format regardless of user locale
280
+ */
281
+ export function formatSingleDateStringWithTimezone(
282
+ date: Date,
283
+ timezone: string,
284
+ ): string {
285
+ try {
286
+ // Use en-CA format for consistent YYYY-MM-DD output regardless of user locale
287
+ const formatter = new Intl.DateTimeFormat("en-CA", {
288
+ timeZone: timezone,
289
+ year: "numeric",
290
+ month: "2-digit",
291
+ day: "2-digit",
292
+ });
293
+ return formatter.format(date);
294
+ } catch {
295
+ // Fallback to UTC if timezone conversion fails
296
+ return date.toISOString().split("T")[0];
297
+ }
298
+ }
299
+
300
+ /**
301
+ * Format date to YYYY-MM-DD string with timezone support (for date ranges)
302
+ * Uses en-CA locale for consistent YYYY-MM-DD format regardless of user locale
303
+ */
304
+ export function formatDateStringWithTimezone(
305
+ dateRange: {
306
+ startDate: string | number | Date | null;
307
+ endDate: string | number | Date | null;
308
+ },
309
+ timezone: string,
310
+ ): string {
311
+ const date = dateRange.startDate;
312
+ if (!date) {
313
+ return "";
314
+ }
315
+
316
+ const dateObj = date instanceof Date ? date : new Date(date);
317
+ return formatSingleDateStringWithTimezone(dateObj, timezone);
318
+ }
319
+
320
+ /**
321
+ * Format time to HH:MM string with timezone support
322
+ * Always returns 24-hour format for internal processing
323
+ */
324
+ export function formatTimeInTimezone(date: Date, timezone: string): string {
325
+ if (timezone) {
326
+ try {
327
+ // Use 24-hour format for internal time processing
328
+ const formatter = new Intl.DateTimeFormat("en-US", {
329
+ timeZone: timezone,
330
+ hour: "2-digit",
331
+ minute: "2-digit",
332
+ hour12: false,
333
+ });
334
+ return formatter.format(date);
335
+ } catch {
336
+ // Fallback to UTC if timezone conversion fails
337
+ return date.toTimeString().slice(0, 5);
338
+ }
339
+ } else {
340
+ return date.toTimeString().slice(0, 5);
341
+ }
342
+ }
343
+
344
+ /**
345
+ * Format date and time with timezone and locale support
346
+ * Returns localized date and time formatting
347
+ */
348
+ export function formatDateTimeInTimezone(
349
+ date: Date,
350
+ timezone: string,
351
+ locale: CountryLanguage,
352
+ options?: {
353
+ dateStyle?: "full" | "long" | "medium" | "short";
354
+ timeStyle?: "full" | "long" | "medium" | "short";
355
+ includeWeekday?: boolean;
356
+ },
357
+ ): string {
358
+ try {
359
+ const localeString = getLocaleString(locale);
360
+ const use24Hour = shouldUse24HourFormat(locale);
361
+
362
+ const formatOptions: Intl.DateTimeFormatOptions = {
363
+ timeZone: timezone,
364
+ hour12: !use24Hour,
365
+ };
366
+
367
+ if (options?.dateStyle) {
368
+ formatOptions.dateStyle = options.dateStyle;
369
+ } else {
370
+ formatOptions.year = "numeric";
371
+ formatOptions.month = "long";
372
+ formatOptions.day = "numeric";
373
+ if (options?.includeWeekday) {
374
+ formatOptions.weekday = "long";
375
+ }
376
+ }
377
+
378
+ if (options?.timeStyle) {
379
+ formatOptions.timeStyle = options.timeStyle;
380
+ } else {
381
+ formatOptions.hour = "2-digit";
382
+ formatOptions.minute = "2-digit";
383
+ }
384
+
385
+ return new Intl.DateTimeFormat(localeString, formatOptions).format(date);
386
+ } catch {
387
+ // Error formatting date/time with timezone - fallback to basic formatting
388
+ const localeString = getLocaleString(locale);
389
+ return date.toLocaleString(localeString);
390
+ }
391
+ }
392
+
393
+ /**
394
+ * Parse date and time strings into a Date object
395
+ * @param dateString Date in YYYY-MM-DD format
396
+ * @param timeString Time in HH:MM format
397
+ * @returns Date object or null if parsing fails
398
+ */
399
+ export function parseDateTime(
400
+ dateString: string,
401
+ timeString: string,
402
+ ): Date | null {
403
+ try {
404
+ const [year, month, day] = dateString.split("-").map(Number);
405
+ const [hours, minutes] = timeString.split(":").map(Number);
406
+
407
+ if (
408
+ year === undefined ||
409
+ month === undefined ||
410
+ day === undefined ||
411
+ hours === undefined ||
412
+ minutes === undefined
413
+ ) {
414
+ return null;
415
+ }
416
+
417
+ // Create UTC date
418
+ return new Date(Date.UTC(year, month - 1, day, hours, minutes, 0, 0));
419
+ } catch {
420
+ return null;
421
+ }
422
+ }