@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.
- package/.dist/LICENSE +674 -0
- package/.dist/README.md +458 -0
- package/.dist/bin/vibe-runtime.js +55482 -0
- package/.dist/bin/vibe-runtime.js.map +431 -0
- package/LICENSE +10 -0
- package/README.md +458 -0
- package/check.config.ts +989 -0
- package/package.json +129 -0
- package/src/config/constants.ts +9 -0
- package/src/config/debug.ts +30 -0
- package/src/config/env-client.ts +62 -0
- package/src/config/env.ts +59 -0
- package/src/config/i18n/de/index.ts +3 -0
- package/src/config/i18n/en/index.ts +60 -0
- package/src/config/i18n/pl/index.ts +3 -0
- package/src/i18n/core/config.ts +229 -0
- package/src/i18n/core/language-utils.ts +236 -0
- package/src/i18n/core/localization-utils.ts +422 -0
- package/src/i18n/core/scoped-translation.ts +120 -0
- package/src/i18n/core/shared-component.tsx +30 -0
- package/src/i18n/core/shared-translation-utils.ts +97 -0
- package/src/i18n/core/shared.ts +44 -0
- package/src/i18n/core/static-types.ts +72 -0
- package/src/i18n/core/translation-utils.ts +218 -0
- package/src/i18n/de/index.ts +8 -0
- package/src/i18n/en/index.ts +7 -0
- package/src/i18n/index.ts +100 -0
- package/src/i18n/pl/index.ts +7 -0
|
@@ -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
|
+
}
|