@regardio/js 0.2.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/LICENSE +9 -0
- package/README.md +156 -0
- package/dist/async/delay.d.ts +2 -0
- package/dist/async/delay.d.ts.map +1 -0
- package/dist/async/delay.js +5 -0
- package/dist/async/delay.test.d.ts +2 -0
- package/dist/async/delay.test.d.ts.map +1 -0
- package/dist/async/delay.test.js +35 -0
- package/dist/browser/base64.d.ts +2 -0
- package/dist/browser/base64.d.ts.map +1 -0
- package/dist/browser/base64.js +10 -0
- package/dist/format/bytes.d.ts +2 -0
- package/dist/format/bytes.d.ts.map +1 -0
- package/dist/format/bytes.js +10 -0
- package/dist/format/bytes.test.d.ts +2 -0
- package/dist/format/bytes.test.d.ts.map +1 -0
- package/dist/format/bytes.test.js +49 -0
- package/dist/format/measure.d.ts +2 -0
- package/dist/format/measure.d.ts.map +1 -0
- package/dist/format/measure.js +14 -0
- package/dist/format/measure.test.d.ts +2 -0
- package/dist/format/measure.test.d.ts.map +1 -0
- package/dist/format/measure.test.js +50 -0
- package/dist/http/cookie.d.ts +9 -0
- package/dist/http/cookie.d.ts.map +1 -0
- package/dist/http/cookie.js +46 -0
- package/dist/http/domain.d.ts +2 -0
- package/dist/http/domain.d.ts.map +1 -0
- package/dist/http/domain.js +13 -0
- package/dist/http/domain.test.d.ts +2 -0
- package/dist/http/domain.test.d.ts.map +1 -0
- package/dist/http/domain.test.js +29 -0
- package/dist/http/request-helpers.d.ts +2 -0
- package/dist/http/request-helpers.d.ts.map +1 -0
- package/dist/http/request-helpers.js +7 -0
- package/dist/http/request-helpers.test.d.ts +2 -0
- package/dist/http/request-helpers.test.d.ts.map +1 -0
- package/dist/http/request-helpers.test.js +37 -0
- package/dist/intl/language-detector.d.ts +33 -0
- package/dist/intl/language-detector.d.ts.map +1 -0
- package/dist/intl/language-detector.js +114 -0
- package/dist/intl/language-detector.test.d.ts +2 -0
- package/dist/intl/language-detector.test.d.ts.map +1 -0
- package/dist/intl/language-detector.test.js +186 -0
- package/dist/intl/locale.d.ts +4 -0
- package/dist/intl/locale.d.ts.map +1 -0
- package/dist/intl/locale.js +22 -0
- package/dist/intl/locale.test.d.ts +2 -0
- package/dist/intl/locale.test.d.ts.map +1 -0
- package/dist/intl/locale.test.js +75 -0
- package/dist/time/time.d.ts +39 -0
- package/dist/time/time.d.ts.map +1 -0
- package/dist/time/time.js +132 -0
- package/dist/time/time.test.d.ts +2 -0
- package/dist/time/time.test.d.ts.map +1 -0
- package/dist/time/time.test.js +202 -0
- package/dist/validation/invariant.d.ts +3 -0
- package/dist/validation/invariant.d.ts.map +1 -0
- package/dist/validation/invariant.js +13 -0
- package/dist/validation/invariant.test.d.ts +2 -0
- package/dist/validation/invariant.test.d.ts.map +1 -0
- package/dist/validation/invariant.test.js +110 -0
- package/dist/validation/verify-file-accept.d.ts +2 -0
- package/dist/validation/verify-file-accept.d.ts.map +1 -0
- package/dist/validation/verify-file-accept.js +4 -0
- package/dist/validation/verify-file-accept.test.d.ts +2 -0
- package/dist/validation/verify-file-accept.test.d.ts.map +1 -0
- package/dist/validation/verify-file-accept.test.js +71 -0
- package/package.json +114 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { parseAcceptLanguage } from 'intl-parse-accept-language';
|
|
2
|
+
import { getClientLocales } from './locale';
|
|
3
|
+
export class LanguageDetectorLingui {
|
|
4
|
+
detector;
|
|
5
|
+
options;
|
|
6
|
+
constructor(options) {
|
|
7
|
+
this.options = options;
|
|
8
|
+
this.detector = new LanguageDetector(this.options.detection);
|
|
9
|
+
}
|
|
10
|
+
async getLocale(request) {
|
|
11
|
+
return await this.detector.detect(request);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export class LanguageDetector {
|
|
15
|
+
options;
|
|
16
|
+
constructor(options) {
|
|
17
|
+
this.options = options;
|
|
18
|
+
this.isSessionOnly(this.options);
|
|
19
|
+
this.isCookieOnly(this.options);
|
|
20
|
+
}
|
|
21
|
+
async detect(request) {
|
|
22
|
+
const order = this.options.order ?? ['urlPath', 'cookie', 'session', 'searchParams', 'header'];
|
|
23
|
+
for (const method of order) {
|
|
24
|
+
let locale = null;
|
|
25
|
+
if (method === 'urlPath') {
|
|
26
|
+
locale = this.fromUrlPath(request);
|
|
27
|
+
}
|
|
28
|
+
if (method === 'searchParams') {
|
|
29
|
+
locale = this.fromSearchParams(request);
|
|
30
|
+
}
|
|
31
|
+
if (method === 'cookie') {
|
|
32
|
+
locale = await this.fromCookie(request);
|
|
33
|
+
}
|
|
34
|
+
if (method === 'session') {
|
|
35
|
+
locale = await this.fromSessionStorage(request);
|
|
36
|
+
}
|
|
37
|
+
if (method === 'header') {
|
|
38
|
+
locale = this.fromHeader(request);
|
|
39
|
+
}
|
|
40
|
+
if (locale)
|
|
41
|
+
return locale;
|
|
42
|
+
}
|
|
43
|
+
return this.options.fallbackLanguage;
|
|
44
|
+
}
|
|
45
|
+
isSessionOnly(options) {
|
|
46
|
+
if (options.order?.length === 1 && options.order[0] === 'session' && !options.sessionStorage) {
|
|
47
|
+
throw new Error('You need a sessionStorage if you want to only get the locale from the session');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
isCookieOnly(options) {
|
|
51
|
+
if (options.order?.length === 1 && options.order[0] === 'cookie' && !options.cookie) {
|
|
52
|
+
throw new Error('You need a cookie if you want to only get the locale from the cookie');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
fromUrlPath(request) {
|
|
56
|
+
const url = new URL(request.url);
|
|
57
|
+
const pathSegments = url.pathname.split('/').filter(Boolean);
|
|
58
|
+
if (pathSegments.length > 0) {
|
|
59
|
+
const firstSegment = pathSegments[0];
|
|
60
|
+
if (firstSegment) {
|
|
61
|
+
return this.fromSupported(firstSegment);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
fromSearchParams(request) {
|
|
67
|
+
const url = new URL(request.url);
|
|
68
|
+
if (!url.searchParams.has(this.options.searchParamKey ?? 'lng')) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
return this.fromSupported(url.searchParams.get(this.options.searchParamKey ?? 'lng'));
|
|
72
|
+
}
|
|
73
|
+
async fromCookie(request) {
|
|
74
|
+
if (!this.options.cookie)
|
|
75
|
+
return null;
|
|
76
|
+
const cookie = this.options.cookie;
|
|
77
|
+
try {
|
|
78
|
+
const lng = await cookie.parse(request.headers.get('Cookie'));
|
|
79
|
+
if (typeof lng !== 'string' || !lng)
|
|
80
|
+
return null;
|
|
81
|
+
return this.fromSupported(lng);
|
|
82
|
+
}
|
|
83
|
+
catch (_error) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async fromSessionStorage(request) {
|
|
88
|
+
if (!this.options.sessionStorage)
|
|
89
|
+
return null;
|
|
90
|
+
const session = await this.options.sessionStorage.getSession(request.headers.get('Cookie'));
|
|
91
|
+
const lng = session.get(this.options.sessionKey ?? 'lng');
|
|
92
|
+
if (!lng)
|
|
93
|
+
return null;
|
|
94
|
+
return this.fromSupported(lng);
|
|
95
|
+
}
|
|
96
|
+
fromHeader(request) {
|
|
97
|
+
const locales = getClientLocales(request);
|
|
98
|
+
if (!locales)
|
|
99
|
+
return null;
|
|
100
|
+
if (Array.isArray(locales))
|
|
101
|
+
return this.fromSupported(locales.join(','));
|
|
102
|
+
return this.fromSupported(locales);
|
|
103
|
+
}
|
|
104
|
+
fromSupported(language) {
|
|
105
|
+
if (!language)
|
|
106
|
+
return this.options.fallbackLanguage;
|
|
107
|
+
const languageStr = Array.isArray(language) ? language.join(',') : language;
|
|
108
|
+
const parsed = parseAcceptLanguage(languageStr, {
|
|
109
|
+
ignoreWildcard: true,
|
|
110
|
+
validate: (locale) => (this.options.supportedLanguages.includes(locale) ? locale : null),
|
|
111
|
+
});
|
|
112
|
+
return parsed[0] || this.options.fallbackLanguage;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"language-detector.test.d.ts","sourceRoot":"","sources":["../../src/intl/language-detector.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { describe, expect, test, vi } from 'vitest';
|
|
2
|
+
import { LanguageDetector, LanguageDetectorLingui } from './language-detector';
|
|
3
|
+
describe('LanguageDetector', () => {
|
|
4
|
+
const defaultOptions = {
|
|
5
|
+
fallbackLanguage: 'en',
|
|
6
|
+
supportedLanguages: ['en', 'de'],
|
|
7
|
+
};
|
|
8
|
+
describe('detect', () => {
|
|
9
|
+
test('should detect language from search params', async () => {
|
|
10
|
+
const detector = new LanguageDetector(defaultOptions);
|
|
11
|
+
const request = new Request('https://example.com?lng=de');
|
|
12
|
+
const language = await detector.detect(request);
|
|
13
|
+
expect(language).toBe('de');
|
|
14
|
+
});
|
|
15
|
+
test('should detect language from cookie', async () => {
|
|
16
|
+
const mockCookie = {
|
|
17
|
+
isSigned: false,
|
|
18
|
+
name: 'lng',
|
|
19
|
+
parse: vi.fn(() => Promise.resolve('de')),
|
|
20
|
+
secrets: ['secret'],
|
|
21
|
+
serialize: vi.fn(() => Promise.resolve('serialized-cookie')),
|
|
22
|
+
};
|
|
23
|
+
const detector = new LanguageDetector({
|
|
24
|
+
...defaultOptions,
|
|
25
|
+
cookie: mockCookie,
|
|
26
|
+
});
|
|
27
|
+
const request = new Request('https://example.com', {
|
|
28
|
+
headers: {
|
|
29
|
+
cookie: 'lng=de',
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
const language = await detector.detect(request);
|
|
33
|
+
expect(language).toBe('de');
|
|
34
|
+
});
|
|
35
|
+
test('should detect language from session', async () => {
|
|
36
|
+
const mockSession = {
|
|
37
|
+
commitSession: vi.fn(() => Promise.resolve('')),
|
|
38
|
+
destroySession: vi.fn(() => Promise.resolve('')),
|
|
39
|
+
getSession: vi.fn(() => Promise.resolve({
|
|
40
|
+
data: { lng: 'de' },
|
|
41
|
+
flash: vi.fn(() => { }),
|
|
42
|
+
get: (key) => (key === 'lng' ? 'de' : undefined),
|
|
43
|
+
has: (key) => key === 'lng',
|
|
44
|
+
id: '123',
|
|
45
|
+
set: vi.fn(() => { }),
|
|
46
|
+
unset: vi.fn(() => { }),
|
|
47
|
+
})),
|
|
48
|
+
};
|
|
49
|
+
const detector = new LanguageDetector({
|
|
50
|
+
...defaultOptions,
|
|
51
|
+
sessionStorage: mockSession,
|
|
52
|
+
});
|
|
53
|
+
const request = new Request('https://example.com');
|
|
54
|
+
const language = await detector.detect(request);
|
|
55
|
+
expect(language).toBe('de');
|
|
56
|
+
});
|
|
57
|
+
test('should detect language from Accept-Language header', async () => {
|
|
58
|
+
const detector = new LanguageDetector(defaultOptions);
|
|
59
|
+
const request = new Request('https://example.com', {
|
|
60
|
+
headers: {
|
|
61
|
+
'accept-language': 'de,en;q=0.9,en;q=0.8',
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
const language = await detector.detect(request);
|
|
65
|
+
expect(language).toBe('de');
|
|
66
|
+
});
|
|
67
|
+
test('should fall back to default language when no supported language is found', async () => {
|
|
68
|
+
const detector = new LanguageDetector(defaultOptions);
|
|
69
|
+
const request = new Request('https://example.com', {
|
|
70
|
+
headers: {
|
|
71
|
+
'accept-language': 'fr-FR,es-ES;q=0.9',
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
const language = await detector.detect(request);
|
|
75
|
+
expect(language).toBe('en');
|
|
76
|
+
});
|
|
77
|
+
test('should handle cookie parsing error', async () => {
|
|
78
|
+
const mockCookie = {
|
|
79
|
+
isSigned: false,
|
|
80
|
+
name: 'lng',
|
|
81
|
+
parse: vi.fn(() => Promise.reject(new Error('Cookie parsing failed'))),
|
|
82
|
+
secrets: ['secret'],
|
|
83
|
+
serialize: vi.fn(() => Promise.resolve('serialized-cookie')),
|
|
84
|
+
};
|
|
85
|
+
const detector = new LanguageDetector({
|
|
86
|
+
...defaultOptions,
|
|
87
|
+
cookie: mockCookie,
|
|
88
|
+
});
|
|
89
|
+
const request = new Request('https://example.com', {
|
|
90
|
+
headers: {
|
|
91
|
+
cookie: 'lng=de',
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
const language = await detector.detect(request);
|
|
95
|
+
expect(language).toBe('en');
|
|
96
|
+
});
|
|
97
|
+
test('should handle multiple language preferences in session', async () => {
|
|
98
|
+
const mockSession = {
|
|
99
|
+
commitSession: vi.fn(() => Promise.resolve('')),
|
|
100
|
+
destroySession: vi.fn(() => Promise.resolve('')),
|
|
101
|
+
getSession: vi.fn(() => Promise.resolve({
|
|
102
|
+
data: { lng: ['de', 'en'] },
|
|
103
|
+
flash: vi.fn(() => { }),
|
|
104
|
+
get: (key) => (key === 'lng' ? ['de', 'en'] : undefined),
|
|
105
|
+
has: (key) => key === 'lng',
|
|
106
|
+
id: '123',
|
|
107
|
+
set: vi.fn(() => { }),
|
|
108
|
+
unset: vi.fn(() => { }),
|
|
109
|
+
})),
|
|
110
|
+
};
|
|
111
|
+
const detector = new LanguageDetector({
|
|
112
|
+
...defaultOptions,
|
|
113
|
+
sessionStorage: mockSession,
|
|
114
|
+
});
|
|
115
|
+
const request = new Request('https://example.com');
|
|
116
|
+
const language = await detector.detect(request);
|
|
117
|
+
expect(language).toBe('de');
|
|
118
|
+
});
|
|
119
|
+
test('should handle invalid language code in search params', async () => {
|
|
120
|
+
const detector = new LanguageDetector(defaultOptions);
|
|
121
|
+
const request = new Request('https://example.com?lng=invalid-code');
|
|
122
|
+
const language = await detector.detect(request);
|
|
123
|
+
expect(language).toBe('en');
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
describe('language detection from header', () => {
|
|
127
|
+
test('should return first supported language from Accept-Language header', async () => {
|
|
128
|
+
const detector = new LanguageDetector(defaultOptions);
|
|
129
|
+
const request = new Request('https://example.com', {
|
|
130
|
+
headers: {
|
|
131
|
+
'accept-language': 'fr-FR,de;q=0.9,en;q=0.8',
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
const language = await detector.detect(request);
|
|
135
|
+
expect(language).toBe('de');
|
|
136
|
+
});
|
|
137
|
+
test('should return fallback language when no supported language in header', async () => {
|
|
138
|
+
const detector = new LanguageDetector(defaultOptions);
|
|
139
|
+
const request = new Request('https://example.com', {
|
|
140
|
+
headers: {
|
|
141
|
+
'accept-language': 'fr-FR,es-ES;q=0.9',
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
const language = await detector.detect(request);
|
|
145
|
+
expect(language).toBe(defaultOptions.fallbackLanguage);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
describe('language detection from search params', () => {
|
|
149
|
+
test('should return language from URL search params', async () => {
|
|
150
|
+
const detector = new LanguageDetector(defaultOptions);
|
|
151
|
+
const request = new Request('https://example.com?lng=de');
|
|
152
|
+
const language = await detector.detect(request);
|
|
153
|
+
expect(language).toBe('de');
|
|
154
|
+
});
|
|
155
|
+
test('should return fallback language when no language param exists', async () => {
|
|
156
|
+
const detector = new LanguageDetector(defaultOptions);
|
|
157
|
+
const request = new Request('https://example.com');
|
|
158
|
+
const language = await detector.detect(request);
|
|
159
|
+
expect(language).toBe(defaultOptions.fallbackLanguage);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
describe('LanguageDetectorLingui', () => {
|
|
164
|
+
const defaultOptions = {
|
|
165
|
+
detection: {
|
|
166
|
+
fallbackLanguage: 'en',
|
|
167
|
+
supportedLanguages: ['en', 'de'],
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
test('should detect language using underlying LanguageDetector', async () => {
|
|
171
|
+
const detector = new LanguageDetectorLingui(defaultOptions);
|
|
172
|
+
const request = new Request('https://example.com?lng=de');
|
|
173
|
+
const language = await detector.getLocale(request);
|
|
174
|
+
expect(language).toBe('de');
|
|
175
|
+
});
|
|
176
|
+
test('should fall back to default language when no supported language found', async () => {
|
|
177
|
+
const detector = new LanguageDetectorLingui(defaultOptions);
|
|
178
|
+
const request = new Request('https://example.com', {
|
|
179
|
+
headers: {
|
|
180
|
+
'accept-language': 'fr-FR,es-ES;q=0.9',
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
const language = await detector.getLocale(request);
|
|
184
|
+
expect(language).toBe('en');
|
|
185
|
+
});
|
|
186
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"locale.d.ts","sourceRoot":"","sources":["../../src/intl/locale.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,OAAO,GAAG,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC;AAiBpD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC;AAC5D,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { parseAcceptLanguage } from 'intl-parse-accept-language';
|
|
2
|
+
export function getClientLocales(requestOrHeaders) {
|
|
3
|
+
const headers = getHeaders(requestOrHeaders);
|
|
4
|
+
const acceptLanguage = headers.get('Accept-Language');
|
|
5
|
+
if (!acceptLanguage)
|
|
6
|
+
return undefined;
|
|
7
|
+
const locales = parseAcceptLanguage(acceptLanguage, {
|
|
8
|
+
ignoreWildcard: true,
|
|
9
|
+
validate: Intl.DateTimeFormat.supportedLocalesOf,
|
|
10
|
+
});
|
|
11
|
+
if (locales.length === 0)
|
|
12
|
+
return undefined;
|
|
13
|
+
if (locales.length === 1)
|
|
14
|
+
return locales[0];
|
|
15
|
+
return locales;
|
|
16
|
+
}
|
|
17
|
+
function getHeaders(requestOrHeaders) {
|
|
18
|
+
if (requestOrHeaders instanceof Request) {
|
|
19
|
+
return requestOrHeaders.headers;
|
|
20
|
+
}
|
|
21
|
+
return requestOrHeaders;
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"locale.test.d.ts","sourceRoot":"","sources":["../../src/intl/locale.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
import { getClientLocales } from './locale';
|
|
3
|
+
describe('getClientLocales', () => {
|
|
4
|
+
describe('with Request object', () => {
|
|
5
|
+
test('should return undefined when Accept-Language header is missing', () => {
|
|
6
|
+
const request = new Request('https://example.com');
|
|
7
|
+
expect(getClientLocales(request)).toBeUndefined();
|
|
8
|
+
});
|
|
9
|
+
test('should return single locale when only one is provided', () => {
|
|
10
|
+
const request = new Request('https://example.com', {
|
|
11
|
+
headers: { 'Accept-Language': 'en-US' },
|
|
12
|
+
});
|
|
13
|
+
expect(getClientLocales(request)).toBe('en-US');
|
|
14
|
+
});
|
|
15
|
+
test('should return array of locales sorted by quality', () => {
|
|
16
|
+
const request = new Request('https://example.com', {
|
|
17
|
+
headers: { 'Accept-Language': 'en-US,de;q=0.9,fr;q=0.8' },
|
|
18
|
+
});
|
|
19
|
+
const result = getClientLocales(request);
|
|
20
|
+
expect(Array.isArray(result)).toBe(true);
|
|
21
|
+
expect(result).toContain('en-US');
|
|
22
|
+
expect(result).toContain('de');
|
|
23
|
+
});
|
|
24
|
+
test('should return undefined for invalid locales', () => {
|
|
25
|
+
const request = new Request('https://example.com', {
|
|
26
|
+
headers: { 'Accept-Language': 'invalid-locale-xyz' },
|
|
27
|
+
});
|
|
28
|
+
expect(getClientLocales(request)).toBeUndefined();
|
|
29
|
+
});
|
|
30
|
+
test('should ignore wildcard (*)', () => {
|
|
31
|
+
const request = new Request('https://example.com', {
|
|
32
|
+
headers: { 'Accept-Language': '*' },
|
|
33
|
+
});
|
|
34
|
+
expect(getClientLocales(request)).toBeUndefined();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
describe('with Headers object', () => {
|
|
38
|
+
test('should return undefined when Accept-Language header is missing', () => {
|
|
39
|
+
const headers = new Headers();
|
|
40
|
+
expect(getClientLocales(headers)).toBeUndefined();
|
|
41
|
+
});
|
|
42
|
+
test('should return single locale when only one is provided', () => {
|
|
43
|
+
const headers = new Headers({ 'Accept-Language': 'de-DE' });
|
|
44
|
+
expect(getClientLocales(headers)).toBe('de-DE');
|
|
45
|
+
});
|
|
46
|
+
test('should return array of locales sorted by quality', () => {
|
|
47
|
+
const headers = new Headers({ 'Accept-Language': 'fr-FR,en;q=0.9,de;q=0.8' });
|
|
48
|
+
const result = getClientLocales(headers);
|
|
49
|
+
expect(Array.isArray(result)).toBe(true);
|
|
50
|
+
expect(result).toContain('fr-FR');
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
describe('quality value handling', () => {
|
|
54
|
+
test('should prioritize higher quality values', () => {
|
|
55
|
+
const request = new Request('https://example.com', {
|
|
56
|
+
headers: { 'Accept-Language': 'de;q=0.5,en;q=0.9' },
|
|
57
|
+
});
|
|
58
|
+
const result = getClientLocales(request);
|
|
59
|
+
expect(Array.isArray(result)).toBe(true);
|
|
60
|
+
if (Array.isArray(result)) {
|
|
61
|
+
expect(result[0]).toBe('en');
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
test('should handle locales without explicit quality (default q=1)', () => {
|
|
65
|
+
const request = new Request('https://example.com', {
|
|
66
|
+
headers: { 'Accept-Language': 'en,de;q=0.9' },
|
|
67
|
+
});
|
|
68
|
+
const result = getClientLocales(request);
|
|
69
|
+
expect(Array.isArray(result)).toBe(true);
|
|
70
|
+
if (Array.isArray(result)) {
|
|
71
|
+
expect(result[0]).toBe('en');
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export declare function timeAgo(input: Date | string): string;
|
|
2
|
+
export declare const oneWeekFromNow: () => Date;
|
|
3
|
+
export declare const oneDayFromNow: () => Date;
|
|
4
|
+
export declare const oneMinuteFromNow: () => Date;
|
|
5
|
+
export declare function friendlyDuration(minutes: number | null, locale: string, short?: boolean): {
|
|
6
|
+
key: string;
|
|
7
|
+
vars: {
|
|
8
|
+
hours: string;
|
|
9
|
+
minutes: string;
|
|
10
|
+
count?: undefined;
|
|
11
|
+
value?: undefined;
|
|
12
|
+
};
|
|
13
|
+
} | {
|
|
14
|
+
key: string;
|
|
15
|
+
vars: {
|
|
16
|
+
hours: string;
|
|
17
|
+
minutes?: undefined;
|
|
18
|
+
count?: undefined;
|
|
19
|
+
value?: undefined;
|
|
20
|
+
};
|
|
21
|
+
} | {
|
|
22
|
+
key: string;
|
|
23
|
+
vars: {
|
|
24
|
+
minutes: string;
|
|
25
|
+
hours?: undefined;
|
|
26
|
+
count?: undefined;
|
|
27
|
+
value?: undefined;
|
|
28
|
+
};
|
|
29
|
+
} | {
|
|
30
|
+
key: string;
|
|
31
|
+
vars: {
|
|
32
|
+
count: number;
|
|
33
|
+
value: string;
|
|
34
|
+
hours?: undefined;
|
|
35
|
+
minutes?: undefined;
|
|
36
|
+
};
|
|
37
|
+
} | null;
|
|
38
|
+
export declare const dateTimeInUnix: (date: number) => number;
|
|
39
|
+
//# sourceMappingURL=time.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"time.d.ts","sourceRoot":"","sources":["../../src/time/time.ts"],"names":[],"mappings":"AAEA,wBAAgB,OAAO,CAAC,KAAK,EAAE,IAAI,GAAG,MAAM,UAuB3C;AAED,eAAO,MAAM,cAAc,YAI1B,CAAC;AAEF,eAAO,MAAM,aAAa,YAIzB,CAAC;AAEF,eAAO,MAAM,gBAAgB,YAI5B,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,UAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;SA4GrF;AAED,eAAO,MAAM,cAAc,GAAI,MAAM,MAAM,WAE1C,CAAC"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
export function timeAgo(input) {
|
|
2
|
+
const date = new Date(input);
|
|
3
|
+
const formatter = new Intl.RelativeTimeFormat('en');
|
|
4
|
+
const ranges = {
|
|
5
|
+
days: 3600 * 24,
|
|
6
|
+
hours: 3600,
|
|
7
|
+
minutes: 60,
|
|
8
|
+
months: 3600 * 24 * 30,
|
|
9
|
+
seconds: 1,
|
|
10
|
+
weeks: 3600 * 24 * 7,
|
|
11
|
+
years: 3600 * 24 * 365,
|
|
12
|
+
};
|
|
13
|
+
const secondsElapsed = (date.getTime() - Date.now()) / 1000;
|
|
14
|
+
for (const key of Object.keys(ranges)) {
|
|
15
|
+
if (ranges[key] < Math.abs(secondsElapsed)) {
|
|
16
|
+
const delta = secondsElapsed / ranges[key];
|
|
17
|
+
return formatter.format(Math.round(delta), key);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return 'Just now';
|
|
21
|
+
}
|
|
22
|
+
export const oneWeekFromNow = () => {
|
|
23
|
+
const now = new Date();
|
|
24
|
+
return new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
|
|
25
|
+
};
|
|
26
|
+
export const oneDayFromNow = () => {
|
|
27
|
+
const now = new Date();
|
|
28
|
+
return new Date(now.getTime() + 1 * 24 * 60 * 60 * 1000);
|
|
29
|
+
};
|
|
30
|
+
export const oneMinuteFromNow = () => {
|
|
31
|
+
const now = new Date();
|
|
32
|
+
return new Date(now.getTime() + 1 * 60 * 1000);
|
|
33
|
+
};
|
|
34
|
+
export function friendlyDuration(minutes, locale, short = false) {
|
|
35
|
+
const numberFormatter = new Intl.NumberFormat(locale);
|
|
36
|
+
if (!minutes) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
if (short) {
|
|
40
|
+
if (minutes > 120) {
|
|
41
|
+
const hours = Math.floor(minutes / 60);
|
|
42
|
+
const remainingMinutes = minutes % 60;
|
|
43
|
+
if (remainingMinutes > 0) {
|
|
44
|
+
return {
|
|
45
|
+
key: 'common:duration.hoursAndMinutesShort',
|
|
46
|
+
vars: {
|
|
47
|
+
hours: numberFormatter.format(hours),
|
|
48
|
+
minutes: numberFormatter.format(remainingMinutes),
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
key: 'common:duration.hoursShort',
|
|
54
|
+
vars: { hours: numberFormatter.format(hours) },
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
key: 'common:duration.minutesShort',
|
|
59
|
+
vars: { minutes: numberFormatter.format(minutes) },
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (minutes >= 525600) {
|
|
63
|
+
const years = Math.floor(minutes / 525600);
|
|
64
|
+
return {
|
|
65
|
+
key: 'common:duration.years',
|
|
66
|
+
vars: {
|
|
67
|
+
count: years,
|
|
68
|
+
value: numberFormatter.format(years),
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (minutes >= 43200) {
|
|
73
|
+
const months = Math.floor(minutes / 43200);
|
|
74
|
+
return {
|
|
75
|
+
key: 'common:duration.months',
|
|
76
|
+
vars: {
|
|
77
|
+
count: months,
|
|
78
|
+
value: numberFormatter.format(months),
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (minutes >= 10080) {
|
|
83
|
+
const weeks = Math.floor(minutes / 10080);
|
|
84
|
+
return {
|
|
85
|
+
key: 'common:duration.weeks',
|
|
86
|
+
vars: {
|
|
87
|
+
count: weeks,
|
|
88
|
+
value: numberFormatter.format(weeks),
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (minutes >= 1440) {
|
|
93
|
+
const days = Math.floor(minutes / 1440);
|
|
94
|
+
return {
|
|
95
|
+
key: 'common:duration.days',
|
|
96
|
+
vars: {
|
|
97
|
+
count: days,
|
|
98
|
+
value: numberFormatter.format(days),
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
if (minutes >= 240) {
|
|
103
|
+
const hours = Math.floor(minutes / 60);
|
|
104
|
+
const remainingMinutes = minutes % 60;
|
|
105
|
+
if (remainingMinutes > 0) {
|
|
106
|
+
return {
|
|
107
|
+
key: 'common:duration.hoursAndMinutes',
|
|
108
|
+
vars: {
|
|
109
|
+
hours: numberFormatter.format(hours),
|
|
110
|
+
minutes: numberFormatter.format(remainingMinutes),
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
key: 'common:duration.hours',
|
|
116
|
+
vars: {
|
|
117
|
+
count: hours,
|
|
118
|
+
value: numberFormatter.format(hours),
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
key: 'common:duration.minutes',
|
|
124
|
+
vars: {
|
|
125
|
+
count: minutes,
|
|
126
|
+
value: numberFormatter.format(minutes),
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
export const dateTimeInUnix = (date) => {
|
|
131
|
+
return Math.floor(date / 1000);
|
|
132
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"time.test.d.ts","sourceRoot":"","sources":["../../src/time/time.test.ts"],"names":[],"mappings":""}
|