Initial commit
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
*.md
|
||||
node_modules
|
||||
dist
|
||||
public
|
||||
build
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"printWidth": 150,
|
||||
"tabWidth": 2,
|
||||
"endOfLine": "lf",
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": true,
|
||||
"semi": true,
|
||||
"bracketSameLine": false,
|
||||
"jsxSingleQuote": false,
|
||||
"arrowParens": "always",
|
||||
"importOrderCaseSensitive": false
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
# [←](../../../docs/README.md) @crux-kit/intl-date
|
||||
|
||||
Конфігурація форматів дат, парсинг, форматування
|
||||
|
||||
## Як використовувати
|
||||
|
||||
### Ініціалізація сервісу
|
||||
|
||||
```ts
|
||||
import { intlDateService, EN_LOCALE, UK_LOCALE } from '@crux-kit/intl-date';
|
||||
export const IntlDate = intlDateService(
|
||||
{
|
||||
localeMap: {
|
||||
en: {
|
||||
code: "enUS",
|
||||
locale: EN_LOCALE,
|
||||
displayFormats: {
|
||||
DATE: 'yyyy-MM-dd',
|
||||
...
|
||||
}
|
||||
},
|
||||
uk: {
|
||||
code: "uk",
|
||||
locale: UK_LOCALE,
|
||||
},
|
||||
},
|
||||
language: "uk",
|
||||
},
|
||||
console
|
||||
);
|
||||
```
|
||||
|
||||
1. `localeMap` - Налаштовує локалі
|
||||
|
||||
```ts
|
||||
/*
|
||||
код береться з бібліотеки date-fns
|
||||
import { enUS } from "date-fns/locale";
|
||||
code: enUS.code
|
||||
*/
|
||||
code: "enUS",
|
||||
/*
|
||||
locale береться з бібліотеки date-fns
|
||||
import { enUS } from "date-fns/locale";
|
||||
locale: enUS
|
||||
*/
|
||||
locale: EN_LOCALE,
|
||||
/*
|
||||
displayFormats перезаписує дефолтні формати відображення дати для конкретної мови
|
||||
*/
|
||||
displayFormats: {
|
||||
DATE: 'yyyy-MM-dd',
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
2. `language` - дефолтна мова
|
||||
|
||||
3. `displayFormat` - базовий формат відображення дати. Можна перезаписувати лише окремі поля
|
||||
|
||||
```ts
|
||||
export default {
|
||||
DAY_WITH_STRING_MONTH: 'dd MMMM',
|
||||
DAY_WITH_STRING_SHORT_MONTH: 'dd MMM',
|
||||
DATE: 'dd.MM.yyyy',
|
||||
DATE_WITH_STRING_SHORT_MONTH: 'dd MMM yyyy',
|
||||
DATE_WITH_STRING_MONTH: 'dd MMMM yyyy',
|
||||
DATE_TIME: 'dd.MM.yyyy hh:mm a',
|
||||
DATE_TIME_WITH_SECONDS: 'dd.MM.yyyy HH:mm:ss',
|
||||
DATE_TIME_WITH_STRING_MONTH: 'dd MMMM yyyy HH:mm',
|
||||
DATE_TIME_WITH_STRING_MONTH_WITH_SECONDS: 'dd MMMM yyyy HH:mm:ss',
|
||||
DATE_TIME_WITH_STRING_SHORT_MONTH: 'dd MMM yyyy HH:mm',
|
||||
DATE_TIME_WITH_STRING_SHORT_MONTH_WITH_SECONDS: 'dd MMM yyyy HH:mm:ss',
|
||||
TIME: 'HH:mm',
|
||||
TIME_WITH_SECONDS: 'HH:mm:ss',
|
||||
YEAR: 'yyyy',
|
||||
HOURS: 'HH',
|
||||
SHORT_HOURS: 'H',
|
||||
MINUTES: 'mm',
|
||||
SHORT_MINUTES: 'm',
|
||||
UNIVERSAL_ISO: 'UNIVERSAL_ISO',
|
||||
};
|
||||
```
|
||||
|
||||
4. `sourceFormat` - базові формати, що використовуються при парсингу зовнішніх даних API, form values і тд
|
||||
|
||||
```ts
|
||||
export default {
|
||||
DATE: 'yyyy-MM-dd',
|
||||
UNIVERSAL_ISO: 'UNIVERSAL_ISO',
|
||||
TIME: 'HH:mm',
|
||||
TIME_WITH_SECONDS: 'HH:mm:ss',
|
||||
DATE_TIME: 'yyyy-MM-dd HH:mm',
|
||||
DATE_TIME_WITH_SECONDS: 'yyyy-MM-dd HH:mm:ss',
|
||||
MINUTES_WITH_SECONDS: 'mm:ss',
|
||||
};
|
||||
```
|
||||
|
||||
### Форматування дати
|
||||
|
||||
```ts
|
||||
const date = '2025-11-27T13:17:02.174Z';
|
||||
|
||||
// "27 листопада 2025 13:17:02"
|
||||
IntlDate.parseSource(date, 'UNIVERSAL_ISO').formatToDisplay('DATE_TIME_WITH_STRING_MONTH_WITH_SECONDS');
|
||||
|
||||
// "27 листопада 2025 15:17:02"
|
||||
IntlDate.parseSource(date, 'UNIVERSAL_ISO').formatToDisplay('DATE_TIME_WITH_STRING_MONTH_WITH_SECONDS', { displayLocal: true });
|
||||
```
|
||||
|
||||
### Зміна мови
|
||||
|
||||
```ts
|
||||
IntlDate.setLocaleGlobally('en');
|
||||
```
|
||||
|
||||
Усі формати дати після цього підуть по новій локалі.
|
||||
Потрібно щоб локаль була у `localeMap`!!!
|
||||
|
||||
### Додаткові можливості
|
||||
|
||||
#### Мутація дати
|
||||
|
||||
додати/відняти дату/час від поточної дати
|
||||
|
||||
```ts
|
||||
const display = IntlDate.parseSource(date, 'UNIVERSAL_ISO')
|
||||
.subtract(2, 'years')
|
||||
.add(2, 'days')
|
||||
.formatToDisplay('DATE_TIME_WITH_STRING_MONTH_WITH_SECONDS', {
|
||||
displayLocal: true,
|
||||
});
|
||||
|
||||
type ManipulateUnit = 'years' | 'months' | 'weeks' | 'days' | 'hours' | 'minutes' | 'seconds';
|
||||
```
|
||||
|
||||
#### Мова
|
||||
|
||||
Попередньо налаштовані Укранська, Польська, Англійська мови
|
||||
|
||||
```ts
|
||||
import { EN_LOCALE_CONFIG, UK_LOCALE_CONFIG, PL_LOCALE_CONFIG } from '@crux-kit/intl-date';
|
||||
```
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "@crux-kit/intl-date",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"types": "src/index.ts",
|
||||
"private": true,
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@crux-kit/types": "*"
|
||||
},
|
||||
"devDependencies": {}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
export const INTL_DATE_DISPLAY_FORMAT = {
|
||||
/**
|
||||
* @example 29 December
|
||||
*/
|
||||
DAY_WITH_STRING_MONTH: 'DAY_WITH_STRING_MONTH',
|
||||
/**
|
||||
* @example 29 Dec
|
||||
*/
|
||||
DAY_WITH_STRING_SHORT_MONTH: 'DAY_WITH_STRING_SHORT_MONTH',
|
||||
/**
|
||||
* @example 29.12.2024
|
||||
*/
|
||||
DATE: 'DATE',
|
||||
/**
|
||||
* @example 29 Dec 2024
|
||||
*/
|
||||
DATE_WITH_STRING_SHORT_MONTH: 'DATE_WITH_STRING_SHORT_MONTH',
|
||||
/**
|
||||
* @example 29 December 2000
|
||||
*/
|
||||
DATE_WITH_STRING_MONTH: 'DATE_WITH_STRING_MONTH',
|
||||
/**
|
||||
* @example 29.12.2024 20:30
|
||||
*/
|
||||
DATE_TIME: 'DATE_TIME',
|
||||
/**
|
||||
* @example 29.12.2024 20:30:43
|
||||
*/
|
||||
DATE_TIME_WITH_SECONDS: 'DATE_TIME_WITH_SECONDS',
|
||||
/**
|
||||
* @example 29 December 2024 20:30
|
||||
*/
|
||||
DATE_TIME_WITH_STRING_MONTH: 'DATE_TIME_WITH_STRING_MONTH',
|
||||
/**
|
||||
* @example 29 December 2024 20:30:23
|
||||
*/
|
||||
DATE_TIME_WITH_STRING_MONTH_WITH_SECONDS: 'DATE_TIME_WITH_STRING_MONTH_WITH_SECONDS',
|
||||
/**
|
||||
* @example 29 Dec 2024 20:30
|
||||
*/
|
||||
DATE_TIME_WITH_STRING_SHORT_MONTH: 'DATE_TIME_WITH_STRING_SHORT_MONTH',
|
||||
/**
|
||||
* @example 29 Dec 2024 20:30:23
|
||||
*/
|
||||
DATE_TIME_WITH_STRING_SHORT_MONTH_WITH_SECONDS: 'DATE_TIME_WITH_STRING_SHORT_MONTH_WITH_SECONDS',
|
||||
/**
|
||||
* @example 20:30
|
||||
*/
|
||||
TIME: 'TIME',
|
||||
/**
|
||||
* @example 20:30:59
|
||||
*/
|
||||
TIME_WITH_SECONDS: 'TIME_WITH_SECONDS',
|
||||
/**
|
||||
* @example 2024
|
||||
*/
|
||||
YEAR: 'YEAR',
|
||||
/**
|
||||
* @example 23
|
||||
*/
|
||||
HOURS: 'HOURS',
|
||||
/**
|
||||
* @example 5
|
||||
*/
|
||||
SHORT_HOURS: 'SHORT_HOURS',
|
||||
/**
|
||||
* @example 04
|
||||
*/
|
||||
MINUTES: 'MINUTES',
|
||||
/**
|
||||
* @example 1
|
||||
*/
|
||||
SHORT_MINUTES: 'SHORT_MINUTES',
|
||||
/**
|
||||
* @example 2024-06-03T12:47:04.000000Z
|
||||
*/
|
||||
UNIVERSAL_ISO: 'UNIVERSAL_ISO',
|
||||
} as const;
|
||||
|
||||
export type IntlDateDisplayFormatEnum = (typeof INTL_DATE_DISPLAY_FORMAT)[keyof typeof INTL_DATE_DISPLAY_FORMAT];
|
||||
|
||||
export const INTL_DATE_SOURCE_FORMAT = {
|
||||
/**
|
||||
* @example 2023-11-27
|
||||
*/
|
||||
DATE: 'DATE',
|
||||
/**
|
||||
* @example 2024-06-03T12:47:04.000000Z
|
||||
*/
|
||||
UNIVERSAL_ISO: 'UNIVERSAL_ISO',
|
||||
/**
|
||||
* @example 20:40
|
||||
*/
|
||||
TIME: 'TIME',
|
||||
/**
|
||||
* @example 20:40:33
|
||||
*/
|
||||
TIME_WITH_SECONDS: 'TIME_WITH_SECONDS',
|
||||
/**
|
||||
* @example 2023-11-27 20:40
|
||||
*/
|
||||
DATE_TIME: 'DATE_TIME',
|
||||
/**
|
||||
* @example 2023-11-27 20:40:33
|
||||
*/
|
||||
DATE_TIME_WITH_SECONDS: 'DATE_TIME_WITH_SECONDS',
|
||||
// 40:59
|
||||
/**
|
||||
* @example 40:59
|
||||
*/
|
||||
MINUTES_WITH_SECONDS: 'MINUTES_WITH_SECONDS',
|
||||
} as const;
|
||||
|
||||
export type IntlDateSourceFormatEnum = (typeof INTL_DATE_SOURCE_FORMAT)[keyof typeof INTL_DATE_SOURCE_FORMAT];
|
||||
@@ -0,0 +1,6 @@
|
||||
import SOURCE_FORMAT from './source-format';
|
||||
import UNITS_MAP from './units-map';
|
||||
|
||||
export * from './enums';
|
||||
export * from './system-timezone';
|
||||
export { SOURCE_FORMAT, UNITS_MAP };
|
||||
@@ -0,0 +1,9 @@
|
||||
export default {
|
||||
DATE: 'yyyy-MM-dd',
|
||||
UNIVERSAL_ISO: 'UNIVERSAL_ISO',
|
||||
TIME: 'HH:mm',
|
||||
TIME_WITH_SECONDS: 'HH:mm:ss',
|
||||
DATE_TIME: 'yyyy-MM-dd HH:mm',
|
||||
DATE_TIME_WITH_SECONDS: 'yyyy-MM-dd HH:mm:ss',
|
||||
MINUTES_WITH_SECONDS: 'mm:ss',
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export const SYSTEM_TZ = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
@@ -0,0 +1,9 @@
|
||||
export default {
|
||||
years: undefined,
|
||||
months: undefined,
|
||||
weeks: undefined,
|
||||
days: undefined,
|
||||
hours: undefined,
|
||||
minutes: undefined,
|
||||
seconds: undefined,
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
export * from './main';
|
||||
|
||||
/*
|
||||
* PREDEFINED LOCALES
|
||||
* Ukrainian, English, Polish
|
||||
*/
|
||||
export * from './locales/en';
|
||||
export * from './locales/uk';
|
||||
export * from './locales/pl';
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { InitIntlDateServiceConfig, IntlDateConfig } from './types';
|
||||
import { DEFAULT_CONFIG } from './default-config';
|
||||
|
||||
export function createConfig(config?: InitIntlDateServiceConfig): IntlDateConfig {
|
||||
if (config == null) return DEFAULT_CONFIG;
|
||||
|
||||
const __: IntlDateConfig = Object.assign(DEFAULT_CONFIG);
|
||||
|
||||
if (config.language) {
|
||||
__.language = config.language;
|
||||
}
|
||||
if (config.displayFormat) {
|
||||
__.displayFormat = Object.assign(__.displayFormat, config.displayFormat);
|
||||
}
|
||||
if (config.sourceFormat) {
|
||||
__.sourceFormat = Object.assign(__.sourceFormat, config.sourceFormat);
|
||||
}
|
||||
if (config.localeMap) {
|
||||
__.localeMap = Object.assign(__.localeMap, config.localeMap);
|
||||
}
|
||||
|
||||
return __;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { EN_LOCALE_CONFIG, EN_DISPLAY_FORMAT, EN_LANGUAGE } from '../locales/en';
|
||||
import { SOURCE_FORMAT } from '../constants';
|
||||
import type { IntlDateConfig } from './types';
|
||||
|
||||
export const DEFAULT_CONFIG: IntlDateConfig = {
|
||||
displayFormat: EN_DISPLAY_FORMAT,
|
||||
language: EN_LANGUAGE,
|
||||
sourceFormat: SOURCE_FORMAT,
|
||||
localeMap: {
|
||||
[EN_LANGUAGE]: EN_LOCALE_CONFIG,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './default-config';
|
||||
export * from './create-config';
|
||||
export * from './language';
|
||||
export * from './types';
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { Locale } from 'date-fns';
|
||||
import * as locales from 'date-fns/locale';
|
||||
import type { IntlDateDisplayFormatEnum } from '../constants';
|
||||
|
||||
export type IntlDateLocaleCode = keyof typeof locales;
|
||||
export type IntlDateLanguage = string;
|
||||
|
||||
export type IntlDateLocaleMap = Partial<Record<IntlDateLanguage, IntlDateLocaleConfig>>;
|
||||
|
||||
export type IntlDateLocaleConfig = {
|
||||
code: IntlDateLocaleCode;
|
||||
locale?: Locale;
|
||||
displayFormats?: Partial<Record<IntlDateDisplayFormatEnum, string>>;
|
||||
};
|
||||
|
||||
export function getLocale(lang: IntlDateLocaleCode): Locale {
|
||||
return locales[lang];
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { IntlDateDisplayFormatEnum, IntlDateSourceFormatEnum } from '../constants';
|
||||
import type { IntlDateLanguage, IntlDateLocaleMap } from './language.ts';
|
||||
|
||||
export interface IntlDateConfig {
|
||||
displayFormat: Record<IntlDateDisplayFormatEnum, string>;
|
||||
sourceFormat: Record<IntlDateSourceFormatEnum, string>;
|
||||
language: IntlDateLanguage;
|
||||
localeMap: IntlDateLocaleMap;
|
||||
}
|
||||
|
||||
export type InitIntlDateServiceConfig = {
|
||||
displayFormat?: Partial<Record<IntlDateDisplayFormatEnum, string>>;
|
||||
sourceFormat?: Partial<Record<IntlDateSourceFormatEnum, string>>;
|
||||
} & Partial<Omit<IntlDateConfig, 'displayFormat' | 'sourceFormat'>>;
|
||||
|
||||
export type ManipulateUnit = 'years' | 'months' | 'weeks' | 'days' | 'hours' | 'minutes' | 'seconds';
|
||||
|
||||
type OmitMethods = 'parseDisplay' | 'parseSource' | 'setNow' | 'setSupportedLanguages' | 'setLocaleGlobally' | 'getMonthLong' | 'getMonthShort';
|
||||
|
||||
interface ParseOptions {
|
||||
strict?: boolean;
|
||||
lang?: IntlDateLanguage;
|
||||
}
|
||||
|
||||
type DateServiceMethods = Omit<IntlDateMethods, OmitMethods>;
|
||||
|
||||
export interface IntlDateMethods {
|
||||
parseSource: (date: string, format: IntlDateSourceFormatEnum, options?: ParseOptions) => DateServiceMethods;
|
||||
parseDisplay: (date: string, format: IntlDateDisplayFormatEnum, options?: ParseOptions) => DateServiceMethods;
|
||||
formatToDisplay: (
|
||||
format: IntlDateDisplayFormatEnum,
|
||||
options?: {
|
||||
displayLocal?: boolean;
|
||||
},
|
||||
) => string;
|
||||
formatToSource: (
|
||||
format: IntlDateSourceFormatEnum,
|
||||
options?: {
|
||||
displayLocal?: boolean;
|
||||
},
|
||||
) => string;
|
||||
toDate: () => Date;
|
||||
subtract: (value: number, unit: ManipulateUnit) => DateServiceMethods;
|
||||
add: (value: number, unit: ManipulateUnit) => DateServiceMethods;
|
||||
setNow: () => DateServiceMethods;
|
||||
setSupportedLanguages: (supportedLanguages: IntlDateLocaleMap) => void;
|
||||
setLocaleGlobally: (language: IntlDateLanguage) => void;
|
||||
getMonthLong: (index: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11) => string;
|
||||
getMonthShort: (index: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11) => string;
|
||||
}
|
||||
|
||||
export interface IntlDateInstanceInterface extends IntlDateMethods {
|
||||
(date: Date): IntlDateMethods;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
export default {
|
||||
DAY_WITH_STRING_MONTH: 'MMMM dd',
|
||||
DAY_WITH_STRING_SHORT_MONTH: 'MMM dd',
|
||||
|
||||
DATE: 'MM.dd.yyyy',
|
||||
|
||||
DATE_WITH_STRING_SHORT_MONTH: 'MMM dd, yyyy',
|
||||
DATE_WITH_STRING_MONTH: 'MMMM dd, yyyy',
|
||||
|
||||
DATE_TIME: 'MM.dd.yyyy hh:mm a',
|
||||
DATE_TIME_WITH_SECONDS: 'MM.dd.yyyy HH:mm:ss',
|
||||
|
||||
DATE_TIME_WITH_STRING_MONTH: 'MMMM dd, yyyy HH:mm',
|
||||
DATE_TIME_WITH_STRING_MONTH_WITH_SECONDS: 'MMMM dd, yyyy HH:mm:ss',
|
||||
|
||||
DATE_TIME_WITH_STRING_SHORT_MONTH: 'MMM dd, yyyy HH:mm',
|
||||
DATE_TIME_WITH_STRING_SHORT_MONTH_WITH_SECONDS: 'MMM dd, yyyy HH:mm:ss',
|
||||
|
||||
TIME: 'HH:mm',
|
||||
TIME_WITH_SECONDS: 'HH:mm:ss',
|
||||
|
||||
YEAR: 'yyyy',
|
||||
HOURS: 'HH',
|
||||
SHORT_HOURS: 'H',
|
||||
MINUTES: 'mm',
|
||||
SHORT_MINUTES: 'm',
|
||||
|
||||
UNIVERSAL_ISO: 'UNIVERSAL_ISO',
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import EN_LANGUAGE from './language.ts';
|
||||
import EN_DISPLAY_FORMAT from './display-format.ts';
|
||||
import EN_LOCALE from './locale.ts';
|
||||
import { IntlDateLocaleConfig } from '../../lib';
|
||||
|
||||
const EN_LOCALE_CONFIG: IntlDateLocaleConfig = {
|
||||
code: EN_LOCALE.code as IntlDateLocaleConfig['code'],
|
||||
locale: EN_LOCALE,
|
||||
displayFormats: EN_DISPLAY_FORMAT,
|
||||
};
|
||||
export { EN_LANGUAGE, EN_DISPLAY_FORMAT, EN_LOCALE, EN_LOCALE_CONFIG };
|
||||
@@ -0,0 +1 @@
|
||||
export default 'en';
|
||||
@@ -0,0 +1,3 @@
|
||||
import { enUS } from 'date-fns/locale';
|
||||
|
||||
export default enUS;
|
||||
@@ -0,0 +1,21 @@
|
||||
export default {
|
||||
DAY_WITH_STRING_MONTH: 'dd MMMM',
|
||||
DAY_WITH_STRING_SHORT_MONTH: 'dd MMM',
|
||||
DATE: 'dd.MM.yyyy',
|
||||
DATE_WITH_STRING_SHORT_MONTH: 'dd MMM yyyy',
|
||||
DATE_WITH_STRING_MONTH: 'dd MMMM yyyy',
|
||||
DATE_TIME: 'dd.MM.yyyy HH:mm',
|
||||
DATE_TIME_WITH_SECONDS: 'dd.MM.yyyy HH:mm:ss',
|
||||
DATE_TIME_WITH_STRING_MONTH: 'dd MMMM yyyy HH:mm',
|
||||
DATE_TIME_WITH_STRING_MONTH_WITH_SECONDS: 'dd MMMM yyyy HH:mm:ss',
|
||||
DATE_TIME_WITH_STRING_SHORT_MONTH: 'dd MMM yyyy HH:mm',
|
||||
DATE_TIME_WITH_STRING_SHORT_MONTH_WITH_SECONDS: 'dd MMM yyyy HH:mm:ss',
|
||||
TIME: 'HH:mm',
|
||||
TIME_WITH_SECONDS: 'HH:mm:ss',
|
||||
YEAR: 'yyyy',
|
||||
HOURS: 'HH',
|
||||
SHORT_HOURS: 'H',
|
||||
MINUTES: 'mm',
|
||||
SHORT_MINUTES: 'm',
|
||||
UNIVERSAL_ISO: 'UNIVERSAL_ISO',
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import PL_LANGUAGE from './language.ts';
|
||||
import PL_DISPLAY_FORMAT from './display-format.ts';
|
||||
import PL_LOCALE from './locale.ts';
|
||||
import { IntlDateLocaleConfig } from '../../lib';
|
||||
|
||||
const PL_LOCALE_CONFIG: IntlDateLocaleConfig = {
|
||||
code: PL_LOCALE.code as IntlDateLocaleConfig['code'],
|
||||
locale: PL_LOCALE,
|
||||
displayFormats: PL_DISPLAY_FORMAT,
|
||||
};
|
||||
|
||||
export { PL_LOCALE_CONFIG, PL_LANGUAGE, PL_DISPLAY_FORMAT, PL_LOCALE };
|
||||
@@ -0,0 +1 @@
|
||||
export default 'pl';
|
||||
@@ -0,0 +1,3 @@
|
||||
import { pl } from 'date-fns/locale';
|
||||
|
||||
export default pl;
|
||||
@@ -0,0 +1,21 @@
|
||||
export default {
|
||||
DAY_WITH_STRING_MONTH: 'dd MMMM',
|
||||
DAY_WITH_STRING_SHORT_MONTH: 'dd MMM',
|
||||
DATE: 'dd.MM.yyyy',
|
||||
DATE_WITH_STRING_SHORT_MONTH: 'dd MMM yyyy',
|
||||
DATE_WITH_STRING_MONTH: 'dd MMMM yyyy',
|
||||
DATE_TIME: 'dd.MM.yyyy HH:mm',
|
||||
DATE_TIME_WITH_SECONDS: 'dd.MM.yyyy HH:mm:ss',
|
||||
DATE_TIME_WITH_STRING_MONTH: 'dd MMMM yyyy HH:mm',
|
||||
DATE_TIME_WITH_STRING_MONTH_WITH_SECONDS: 'dd MMMM yyyy HH:mm:ss',
|
||||
DATE_TIME_WITH_STRING_SHORT_MONTH: 'dd MMM yyyy HH:mm',
|
||||
DATE_TIME_WITH_STRING_SHORT_MONTH_WITH_SECONDS: 'dd MMM yyyy HH:mm:ss',
|
||||
TIME: 'HH:mm',
|
||||
TIME_WITH_SECONDS: 'HH:mm:ss',
|
||||
YEAR: 'yyyy',
|
||||
HOURS: 'HH',
|
||||
SHORT_HOURS: 'H',
|
||||
MINUTES: 'mm',
|
||||
SHORT_MINUTES: 'm',
|
||||
UNIVERSAL_ISO: 'UNIVERSAL_ISO',
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import UK_LANGUAGE from './language.ts';
|
||||
import UK_DISPLAY_FORMAT from './display-format.ts';
|
||||
import UK_LOCALE from './locale.ts';
|
||||
import { IntlDateLocaleConfig } from '../../lib';
|
||||
|
||||
const UK_LOCALE_CONFIG = {
|
||||
code: UK_LOCALE.code as IntlDateLocaleConfig['code'],
|
||||
locale: UK_LOCALE,
|
||||
displayFormats: UK_DISPLAY_FORMAT,
|
||||
};
|
||||
|
||||
export { UK_LOCALE_CONFIG, UK_LANGUAGE, UK_DISPLAY_FORMAT, UK_LOCALE };
|
||||
@@ -0,0 +1 @@
|
||||
export default 'uk';
|
||||
@@ -0,0 +1,3 @@
|
||||
import { uk } from 'date-fns/locale';
|
||||
|
||||
export default uk;
|
||||
@@ -0,0 +1,168 @@
|
||||
import { parse, add, sub, type Locale } from 'date-fns';
|
||||
import { toZonedTime, fromZonedTime, formatInTimeZone } from 'date-fns-tz';
|
||||
import { SYSTEM_TZ, UNITS_MAP, type IntlDateDisplayFormatEnum, type IntlDateSourceFormatEnum } from './constants';
|
||||
import { getLocale, createConfig } from './lib';
|
||||
import type {
|
||||
IntlDateConfig,
|
||||
IntlDateInstanceInterface,
|
||||
IntlDateMethods,
|
||||
IntlDateLocaleMap,
|
||||
IntlDateLanguage,
|
||||
InitIntlDateServiceConfig,
|
||||
} from './lib';
|
||||
|
||||
class IntlDateService {
|
||||
private _locale: Locale | undefined;
|
||||
private _date: Date;
|
||||
private _config: IntlDateConfig;
|
||||
private _logger?: Console;
|
||||
|
||||
constructor(config: IntlDateConfig, date: Date, logger?: Console) {
|
||||
this._config = config;
|
||||
this._logger = logger;
|
||||
|
||||
const initialLang = config.localeMap[config.language];
|
||||
|
||||
this._locale = initialLang?.locale || (initialLang ? getLocale(initialLang.code) : undefined);
|
||||
this._date = fromZonedTime(date, 'UTC');
|
||||
}
|
||||
|
||||
_call(date: Date): this {
|
||||
this._date = fromZonedTime(date, 'UTC');
|
||||
return this;
|
||||
}
|
||||
|
||||
public setSupportedLanguages(supportedLanguages: IntlDateLocaleMap): void {
|
||||
this._logger?.log('Set supported languages: ', supportedLanguages);
|
||||
this._config.localeMap = supportedLanguages;
|
||||
}
|
||||
|
||||
public setLocaleGlobally(lang: IntlDateLanguage): void {
|
||||
const language = this._config.localeMap[lang];
|
||||
|
||||
if (!language) {
|
||||
this._logger?.error('Language is not supported. Add it to the supported list.');
|
||||
return;
|
||||
}
|
||||
|
||||
this._locale = language.locale ?? getLocale(language.code);
|
||||
this._config.language = lang;
|
||||
this._logger?.log('Set global language: ', lang);
|
||||
}
|
||||
private _getSourceFormat(fmt: IntlDateSourceFormatEnum): string {
|
||||
return this._config.sourceFormat[fmt];
|
||||
}
|
||||
private _getDisplayFormat(fmt: IntlDateDisplayFormatEnum): string {
|
||||
const localeFormat = this._config.localeMap[this._config.language]?.displayFormats?.[fmt];
|
||||
if (localeFormat) return localeFormat;
|
||||
return this._config.displayFormat[fmt];
|
||||
}
|
||||
private _parseDate(dateStr: string, formatStr: string, options?: { strict?: boolean; lang?: IntlDateLanguage }): Date {
|
||||
if (formatStr === 'UNIVERSAL_ISO') {
|
||||
return new Date(dateStr);
|
||||
}
|
||||
|
||||
const locale = this._config.localeMap[options?.lang ?? this._config.language]?.locale;
|
||||
|
||||
const parsedLocal = parse(dateStr, formatStr, new Date(), {
|
||||
locale: locale,
|
||||
});
|
||||
|
||||
return fromZonedTime(parsedLocal, SYSTEM_TZ);
|
||||
}
|
||||
private _formatDate(date: Date, pattern: string, displayLocal?: boolean): string {
|
||||
if (pattern === 'UNIVERSAL_ISO') {
|
||||
return displayLocal ? toZonedTime(date, SYSTEM_TZ).toISOString() : date.toISOString();
|
||||
}
|
||||
|
||||
if (!displayLocal) {
|
||||
return formatInTimeZone(date, 'UTC', pattern, {
|
||||
locale: this._locale,
|
||||
});
|
||||
}
|
||||
|
||||
return formatInTimeZone(date, SYSTEM_TZ, pattern, {
|
||||
locale: this._locale,
|
||||
});
|
||||
}
|
||||
|
||||
public parseSource: IntlDateMethods['parseSource'] = (date, fmt, options) => {
|
||||
this._date = this._parseDate(date, this._getSourceFormat(fmt), options);
|
||||
return this;
|
||||
};
|
||||
|
||||
public parseDisplay: IntlDateMethods['parseDisplay'] = (date, fmt, options) => {
|
||||
this._date = this._parseDate(date, this._getDisplayFormat(fmt), options);
|
||||
return this;
|
||||
};
|
||||
|
||||
public formatToDisplay: IntlDateMethods['formatToDisplay'] = (fmt, options) => {
|
||||
try {
|
||||
const pattern = this._getDisplayFormat(fmt);
|
||||
return this._formatDate(this._date, pattern, options?.displayLocal);
|
||||
} catch {
|
||||
return 'Invalid date';
|
||||
}
|
||||
};
|
||||
|
||||
public formatToSource: IntlDateMethods['formatToSource'] = (fmt, options) => {
|
||||
try {
|
||||
const pattern = this._getSourceFormat(fmt);
|
||||
return this._formatDate(this._date, pattern, options?.displayLocal);
|
||||
} catch {
|
||||
return 'Invalid date';
|
||||
}
|
||||
};
|
||||
|
||||
public add: IntlDateMethods['add'] = (value, unit) => {
|
||||
const duration = Object.assign(UNITS_MAP, { [unit]: value });
|
||||
const result = add(this._date, duration);
|
||||
this._date = fromZonedTime(result, 'UTC');
|
||||
return this;
|
||||
};
|
||||
|
||||
public subtract: IntlDateMethods['subtract'] = (value, unit) => {
|
||||
const duration = Object.assign(UNITS_MAP, { [unit]: value });
|
||||
const result = sub(this._date, duration);
|
||||
this._date = fromZonedTime(result, 'UTC');
|
||||
return this;
|
||||
};
|
||||
|
||||
public toDate = () => this._date;
|
||||
|
||||
public setNow = () => {
|
||||
this._date = fromZonedTime(new Date(), 'UTC');
|
||||
return this;
|
||||
};
|
||||
|
||||
public getMonthShort = (index: number): string => {
|
||||
const d = new Date(Date.UTC(2020, index, 1));
|
||||
return formatInTimeZone(d, 'UTC', 'MMM', { locale: this._locale });
|
||||
};
|
||||
|
||||
public getMonthLong = (index: number): string => {
|
||||
const d = new Date(Date.UTC(2020, index, 1));
|
||||
return formatInTimeZone(d, 'UTC', 'MMMM', { locale: this._locale });
|
||||
};
|
||||
}
|
||||
|
||||
export const intlDateService = (config?: InitIntlDateServiceConfig, logger?: Console): IntlDateInstanceInterface => {
|
||||
const instance = new IntlDateService(createConfig(config), new Date(), logger);
|
||||
|
||||
const methods: IntlDateMethods = {
|
||||
parseSource: instance.parseSource.bind(instance),
|
||||
parseDisplay: instance.parseDisplay.bind(instance),
|
||||
formatToDisplay: instance.formatToDisplay.bind(instance),
|
||||
formatToSource: instance.formatToSource.bind(instance),
|
||||
toDate: instance.toDate.bind(instance),
|
||||
subtract: instance.subtract.bind(instance),
|
||||
add: instance.add.bind(instance),
|
||||
setNow: instance.setNow.bind(instance),
|
||||
setSupportedLanguages: instance.setSupportedLanguages.bind(instance),
|
||||
setLocaleGlobally: instance.setLocaleGlobally.bind(instance),
|
||||
getMonthShort: instance.getMonthShort.bind(instance),
|
||||
getMonthLong: instance.getMonthLong.bind(instance),
|
||||
};
|
||||
|
||||
return Object.assign((date: Date) => instance._call(date), methods);
|
||||
};
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2023"],
|
||||
"skipLibCheck": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"composite": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
*.md
|
||||
node_modules
|
||||
dist
|
||||
public
|
||||
build
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"printWidth": 150,
|
||||
"tabWidth": 2,
|
||||
"endOfLine": "lf",
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": true,
|
||||
"semi": true,
|
||||
"bracketSameLine": false,
|
||||
"jsxSingleQuote": false,
|
||||
"arrowParens": "always",
|
||||
"importOrderCaseSensitive": false
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
# [←](../../../docs/README.md) @crux-kit/intl-name
|
||||
|
||||
A lightweight TypeScript service for formatting personal names into display-ready strings with locale support.
|
||||
|
||||
The service accepts multiple input formats (objects or strings), normalizes them into a canonical structure, and formats the output according to language-specific patterns.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
```ts
|
||||
import { intlNameService, INTL_NAME_TOKENS } from 'intl-name';
|
||||
|
||||
const IntlName = intlNameService(
|
||||
{
|
||||
language: 'uk',
|
||||
fallbackLanguage: 'uk',
|
||||
localeMap: {
|
||||
uk: {
|
||||
formats: {
|
||||
SURNAME_NAME: `${INTL_NAME_TOKENS.LAST.FULL} ${INTL_NAME_TOKENS.FIRST.FULL}`,
|
||||
INITIALS_LF: `${INTL_NAME_TOKENS.LAST.INITIAL}${INTL_NAME_TOKENS.FIRST.INITIAL}`,
|
||||
},
|
||||
},
|
||||
en: {
|
||||
formats: {
|
||||
SURNAME_NAME: `${INTL_NAME_TOKENS.LAST.FULL} ${INTL_NAME_TOKENS.FIRST.FULL}`,
|
||||
INITIALS_LF: `${INTL_NAME_TOKENS.LAST.INITIAL}${INTL_NAME_TOKENS.FIRST.INITIAL}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
console,
|
||||
);
|
||||
|
||||
IntlName.formatToDisplay({ firstName: 'John', lastName: 'Smith' }, 'SURNAME_NAME');
|
||||
// → "Smith John"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Supported Input Formats
|
||||
|
||||
The service accepts names in three formats:
|
||||
|
||||
### camelCase object
|
||||
|
||||
```ts
|
||||
{ firstName, lastName, patronymic?, middleName?, prefix?, suffix? }
|
||||
```
|
||||
|
||||
### snake_case object
|
||||
|
||||
```ts
|
||||
{ first_name, last_name, patronymic?, middle_name?, prefix?, suffix? }
|
||||
```
|
||||
|
||||
### string
|
||||
|
||||
```ts
|
||||
'First Last Patronymic';
|
||||
```
|
||||
|
||||
All input formats are automatically normalized into a canonical internal representation.
|
||||
|
||||
---
|
||||
|
||||
## Formatting Patterns & Tokens
|
||||
|
||||
Name formatting is driven by **patterns** defined in the `localeMap`.
|
||||
|
||||
Patterns are plain strings composed of **tokens** that represent different parts of a person’s name.
|
||||
At runtime, these tokens are replaced with actual values from the input.
|
||||
|
||||
If a token has no value, it is replaced with an empty string.
|
||||
|
||||
---
|
||||
|
||||
## Conditional Blocks `{ ... }`
|
||||
|
||||
Conditional blocks are **optional helpers** mainly intended to control
|
||||
**punctuation, separators, and surrounding spaces**.
|
||||
|
||||
> Tokens work without conditional blocks.
|
||||
> Missing values are simply replaced with an empty string.
|
||||
|
||||
### Why conditional blocks matter
|
||||
|
||||
Without conditional blocks, punctuation remains even when a value is missing:
|
||||
|
||||
```ts
|
||||
// Pattern without a conditional block
|
||||
`${INTL_NAME_TOKENS.LAST.FULL}, ${INTL_NAME_TOKENS.FIRST.FULL}`;
|
||||
|
||||
// Input: { lastName: "Smith" }
|
||||
// Output: "Smith, "
|
||||
```
|
||||
|
||||
Using a conditional block removes the separator entirely:
|
||||
|
||||
```ts
|
||||
`${INTL_NAME_TOKENS.LAST.FULL}{, ${INTL_NAME_TOKENS.FIRST.FULL}}`;
|
||||
|
||||
// Input: { lastName: "Smith" }
|
||||
// Output: "Smith"
|
||||
```
|
||||
|
||||
The whole block (including commas, dots, or spaces) is rendered
|
||||
**only if at least one token inside has a value**.
|
||||
|
||||
### Typical use cases
|
||||
|
||||
- Commas and dots (`", "`, `"."`)
|
||||
- Extra spaces around optional parts
|
||||
- Optional patronymic or middle name
|
||||
|
||||
Example:
|
||||
|
||||
```ts
|
||||
`${INTL_NAME_TOKENS.LAST.FULL} ${INTL_NAME_TOKENS.FIRST.FULL}{ ${INTL_NAME_TOKENS.PATRONYMIC.FULL}}`;
|
||||
```
|
||||
|
||||
- With patronymic → `"Smith John Ivanovich"`
|
||||
- Without patronymic → `"Smith John"`
|
||||
|
||||
Conditional blocks help keep formatting clean without manual string checks.
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
### `intlNameService(config, logger?)`
|
||||
|
||||
Creates a service instance.
|
||||
|
||||
Returns:
|
||||
|
||||
- `formatToDisplay(input, formatKey)` — formats the name for display
|
||||
- `toObject(input)` — converts input into a canonical object
|
||||
- `setLanguage(lang)` — changes the active language
|
||||
- `setSupportedLanguages(map)` — replaces the supported locale map
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "@crux-kit/intl-name",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"types": "src/index.ts",
|
||||
"private": true,
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@crux-kit/types": "*"
|
||||
},
|
||||
"devDependencies": {}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { NameFormatInputType, NameFormatInputTypeCamel, NameFormatInputTypeSnake, NameFormatInputTypeString } from './name-input-types';
|
||||
|
||||
export const isSnakeCaseInputType = (input: NameFormatInputType): input is NameFormatInputTypeSnake => {
|
||||
return typeof input === 'object' && input !== null && 'first_name' in input;
|
||||
};
|
||||
|
||||
export const isCamelCaseInputType = (input: NameFormatInputType): input is NameFormatInputTypeCamel => {
|
||||
return typeof input === 'object' && input !== null && 'firstName' in input;
|
||||
};
|
||||
|
||||
export const isStringInputType = (input: NameFormatInputType): input is NameFormatInputTypeString => {
|
||||
return typeof input === 'string';
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import { isCamelCaseInputType, isSnakeCaseInputType, isStringInputType } from './check-input-type';
|
||||
import type { CanonicalName, NameFormatInputType } from './name-input-types';
|
||||
|
||||
export function convertInputToCamel(input: NameFormatInputType): CanonicalName {
|
||||
if (isCamelCaseInputType(input)) {
|
||||
return {
|
||||
firstName: input.firstName ?? '',
|
||||
lastName: input.lastName ?? '',
|
||||
patronymic: input.patronymic,
|
||||
prefix: input.prefix,
|
||||
suffix: input.suffix,
|
||||
middleName: input.middleName,
|
||||
};
|
||||
}
|
||||
|
||||
if (isSnakeCaseInputType(input)) {
|
||||
return {
|
||||
firstName: input.first_name ?? '',
|
||||
lastName: input.last_name ?? '',
|
||||
patronymic: input.patronymic,
|
||||
middleName: input.middle_name,
|
||||
prefix: input.prefix,
|
||||
suffix: input.suffix,
|
||||
};
|
||||
}
|
||||
|
||||
if (isStringInputType(input)) {
|
||||
const parts = input.trim().split(/\s+/);
|
||||
return {
|
||||
firstName: parts[0] ?? '',
|
||||
lastName: parts[1] ?? '',
|
||||
patronymic: parts[2],
|
||||
middleName: undefined,
|
||||
prefix: undefined,
|
||||
suffix: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return { firstName: '', lastName: '' };
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { CanonicalName } from './name-input-types';
|
||||
import { INTL_NAME_TOKENS } from './tokens';
|
||||
|
||||
export const UNIVERSAL_NAME_PATTERN = 'UNIVERSAL' as const;
|
||||
|
||||
function initial(value?: string) {
|
||||
return (value?.[0] ?? '').toUpperCase();
|
||||
}
|
||||
function valueOrEmpty(v?: string) {
|
||||
return v ?? '';
|
||||
}
|
||||
|
||||
export function formatNameByPattern(pattern: string, ctx: CanonicalName): string {
|
||||
if (pattern === UNIVERSAL_NAME_PATTERN) {
|
||||
return [ctx.firstName, ctx.lastName, ctx.patronymic].filter(Boolean).join(' ').trim();
|
||||
}
|
||||
|
||||
const TOKEN_REGEX = /(Pr|Su|FF|LL|PP|MM|F|L|P|M)/g;
|
||||
|
||||
const resolveToken = (token: string): string => {
|
||||
switch (token) {
|
||||
case INTL_NAME_TOKENS.FIRST.FULL:
|
||||
return valueOrEmpty(ctx.firstName);
|
||||
case INTL_NAME_TOKENS.FIRST.INITIAL:
|
||||
return initial(ctx.firstName);
|
||||
|
||||
case INTL_NAME_TOKENS.LAST.FULL:
|
||||
return valueOrEmpty(ctx.lastName);
|
||||
case INTL_NAME_TOKENS.LAST.INITIAL:
|
||||
return initial(ctx.lastName);
|
||||
|
||||
case INTL_NAME_TOKENS.MIDDLE.FULL:
|
||||
return valueOrEmpty(ctx.middleName);
|
||||
case INTL_NAME_TOKENS.MIDDLE.INITIAL:
|
||||
return initial(ctx.middleName);
|
||||
|
||||
case INTL_NAME_TOKENS.PATRONYMIC.FULL:
|
||||
return valueOrEmpty(ctx.patronymic);
|
||||
case INTL_NAME_TOKENS.PATRONYMIC.INITIAL:
|
||||
return initial(ctx.patronymic);
|
||||
|
||||
case INTL_NAME_TOKENS.PREFIX:
|
||||
return valueOrEmpty(ctx.prefix);
|
||||
|
||||
case INTL_NAME_TOKENS.SUFFIX:
|
||||
return valueOrEmpty(ctx.suffix);
|
||||
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const renderTokensWithPresence = (str: string): { text: string; hasValue: boolean } => {
|
||||
let hasValue = false;
|
||||
const text = str.replace(TOKEN_REGEX, (token) => {
|
||||
const value = resolveToken(token);
|
||||
if (value !== '') hasValue = true;
|
||||
return value;
|
||||
});
|
||||
return { text, hasValue };
|
||||
};
|
||||
|
||||
let out = pattern;
|
||||
|
||||
const blockRe = /\{([^{}]*)\}/g;
|
||||
|
||||
while (true) {
|
||||
const before = out;
|
||||
out = out.replace(blockRe, (_, inner: string) => {
|
||||
const { text, hasValue } = renderTokensWithPresence(inner);
|
||||
return hasValue ? text : '';
|
||||
});
|
||||
if (out === before) break;
|
||||
}
|
||||
|
||||
out = renderTokensWithPresence(out).text;
|
||||
return out.replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { intlNameService } from './main';
|
||||
export { INTL_NAME_TOKENS } from './tokens';
|
||||
@@ -0,0 +1,53 @@
|
||||
import { convertInputToCamel } from './convert-input';
|
||||
import { formatNameByPattern } from './formatter';
|
||||
import { NameFormatInputType } from './name-input-types';
|
||||
import { InitIntlNameServiceConfig, IntlNameInstanceInterface, IntlNameLocaleMap } from './types';
|
||||
|
||||
export function intlNameService<LM extends IntlNameLocaleMap, FallbackLang extends keyof LM>(
|
||||
config: InitIntlNameServiceConfig<LM, FallbackLang>,
|
||||
logger?: Console,
|
||||
): IntlNameInstanceInterface<LM, FallbackLang> {
|
||||
let language = config.language;
|
||||
let localeMap = config.localeMap;
|
||||
|
||||
const getPattern = (fmt: string): string => {
|
||||
const langPattern = localeMap[language]?.formats?.[fmt];
|
||||
if (langPattern) return langPattern;
|
||||
|
||||
const fallbackPattern = localeMap[config.fallbackLanguage]?.formats?.[fmt];
|
||||
if (fallbackPattern) return fallbackPattern;
|
||||
|
||||
return 'UNIVERSAL';
|
||||
};
|
||||
|
||||
return {
|
||||
setLanguage: (lang) => {
|
||||
if (!localeMap[lang]) {
|
||||
logger?.error('Language is not supported. Add it to the supported list.');
|
||||
return;
|
||||
}
|
||||
language = lang;
|
||||
logger?.log('Set global language: ', lang);
|
||||
},
|
||||
|
||||
setSupportedLanguages: (map) => {
|
||||
localeMap = map;
|
||||
logger?.log('Set supported languages: ', map);
|
||||
},
|
||||
|
||||
formatToDisplay: (input: NameFormatInputType, fmt) => {
|
||||
const name = convertInputToCamel(input);
|
||||
const pattern = getPattern(fmt);
|
||||
|
||||
return formatNameByPattern(pattern, {
|
||||
firstName: name.firstName,
|
||||
lastName: name.lastName,
|
||||
middleName: name.patronymic,
|
||||
prefix: name.prefix,
|
||||
suffix: name.suffix,
|
||||
});
|
||||
},
|
||||
|
||||
toObject: (input: NameFormatInputType) => convertInputToCamel(input),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
export type NameFormatInputTypeSnake = {
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
patronymic?: string;
|
||||
middle_name?: string;
|
||||
prefix?: string;
|
||||
suffix?: string;
|
||||
};
|
||||
|
||||
export type NameFormatInputTypeCamel = {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
patronymic?: string;
|
||||
middleName?: string;
|
||||
prefix?: string;
|
||||
suffix?: string;
|
||||
};
|
||||
|
||||
export type NameFormatInputTypeString = string;
|
||||
|
||||
export type NameFormatInputType = NameFormatInputTypeSnake | NameFormatInputTypeCamel | NameFormatInputTypeString;
|
||||
|
||||
export type CanonicalName = {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
patronymic?: string;
|
||||
middleName?: string;
|
||||
prefix?: string;
|
||||
suffix?: string;
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
export const INTL_NAME_TOKENS = {
|
||||
FIRST: {
|
||||
INITIAL: 'F',
|
||||
FULL: 'FF',
|
||||
},
|
||||
LAST: {
|
||||
INITIAL: 'L',
|
||||
FULL: 'LL',
|
||||
},
|
||||
PATRONYMIC: {
|
||||
INITIAL: 'P',
|
||||
FULL: 'PP',
|
||||
},
|
||||
MIDDLE: {
|
||||
INITIAL: 'M',
|
||||
FULL: 'MM',
|
||||
},
|
||||
PREFIX: 'Pr',
|
||||
SUFFIX: 'Su',
|
||||
} as const;
|
||||
@@ -0,0 +1,23 @@
|
||||
import { CanonicalName, NameFormatInputType } from './name-input-types';
|
||||
|
||||
export type IntlNameLocaleMap = Record<
|
||||
string,
|
||||
{
|
||||
formats: Record<string, string>;
|
||||
}
|
||||
>;
|
||||
|
||||
type InferFormatKeys<LM extends IntlNameLocaleMap, FallbackLang extends keyof LM> = keyof LM[FallbackLang]['formats'] & string;
|
||||
|
||||
export type InitIntlNameServiceConfig<LM extends IntlNameLocaleMap, FallbackLang extends keyof LM> = {
|
||||
fallbackLanguage: FallbackLang;
|
||||
language: keyof LM & string;
|
||||
localeMap: LM;
|
||||
};
|
||||
|
||||
export type IntlNameInstanceInterface<LM extends IntlNameLocaleMap, FallbackLang extends keyof LM> = {
|
||||
setLanguage: (lang: keyof LM & string) => void;
|
||||
setSupportedLanguages: (map: LM) => void;
|
||||
formatToDisplay: (input: NameFormatInputType, fmt: InferFormatKeys<LM, FallbackLang>) => string;
|
||||
toObject: (input: NameFormatInputType) => CanonicalName;
|
||||
};
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2023"],
|
||||
"skipLibCheck": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"composite": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
*.md
|
||||
node_modules
|
||||
dist
|
||||
public
|
||||
build
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"printWidth": 150,
|
||||
"tabWidth": 2,
|
||||
"endOfLine": "lf",
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": true,
|
||||
"semi": true,
|
||||
"bracketSameLine": false,
|
||||
"jsxSingleQuote": false,
|
||||
"arrowParens": "always",
|
||||
"importOrderCaseSensitive": false
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
# [←](../../../docs/README.md) @crux-kit/local-storage
|
||||
|
||||
Сервіс для роботи з `localStorage`: збереження, читання, видалення даних із серіалізацією/десеріалізацією та підтримкою TTL і namespace-префіксів.
|
||||
|
||||
## Як використовувати
|
||||
|
||||
### Ініціалізація
|
||||
|
||||
```ts
|
||||
import { LocalStorageService } from '@crux-kit/local-storage';
|
||||
|
||||
export const storage = new LocalStorageService({
|
||||
prefix: 'app',
|
||||
logger: console,
|
||||
});
|
||||
```
|
||||
|
||||
- `prefix` — необов'язковий. Усі ключі зберігаються як `prefix:key`, що дозволяє уникнути конфліктів між різними модулями.
|
||||
- `logger` — необов'язковий. Передається `console` або будь-який об'єкт із методами `error` / `log`.
|
||||
|
||||
### Запис і читання
|
||||
|
||||
```ts
|
||||
storage.set('user', { id: 1, name: 'Randy Marsh' });
|
||||
|
||||
const user = storage.get<{ id: number; name: string }>('user');
|
||||
```
|
||||
|
||||
Дані автоматично серіалізуються в JSON при записі та десеріалізуються при читанні.
|
||||
|
||||
### TTL (час життя)
|
||||
|
||||
```ts
|
||||
// значення стане недоступним через 30 хвилин
|
||||
storage.set('session', data, { ttlMs: 30 * 60 * 1000 });
|
||||
|
||||
// get поверне undefined і автоматично видалить ключ після закінчення TTL
|
||||
const session = storage.get('session');
|
||||
```
|
||||
|
||||
### getOrSet
|
||||
|
||||
Повертає збережене значення, або записує та повертає `fallback`, якщо ключ відсутній.
|
||||
|
||||
```ts
|
||||
const lang = storage.getOrSet('lang', 'en');
|
||||
|
||||
// також підтримує TTL
|
||||
const theme = storage.getOrSet('theme', 'dark', { ttlMs: 60 * 60 * 1000 });
|
||||
```
|
||||
|
||||
### Перевірка існування ключа
|
||||
|
||||
```ts
|
||||
storage.has('token'); // true | false
|
||||
```
|
||||
|
||||
### Видалення
|
||||
|
||||
```ts
|
||||
storage.remove('token');
|
||||
```
|
||||
|
||||
### Всі значення як об'єкт
|
||||
|
||||
Повертає всі збережені записи поточного namespace у вигляді одного об'єкта. Ключі з вичерпаним TTL автоматично виключаються.
|
||||
|
||||
```ts
|
||||
const lss = new LocalStorageService({ prefix: 'settings' });
|
||||
|
||||
lss.set('view', 'grid');
|
||||
lss.set('sortBy', 'name');
|
||||
lss.set('showHidden', true);
|
||||
|
||||
type Settings = {
|
||||
view: string;
|
||||
sortBy: string;
|
||||
showHidden: boolean;
|
||||
};
|
||||
|
||||
lss.getAll<Settings>();
|
||||
// → Partial<Settings> — { view: 'grid', sortBy: 'name', showHidden: true }
|
||||
```
|
||||
|
||||
### Список ключів
|
||||
|
||||
Повертає всі ключі. Якщо заданий `prefix` — повертає лише ключі поточного namespace без префікса.
|
||||
|
||||
```ts
|
||||
storage.keys(); // ['user', 'session', 'lang']
|
||||
```
|
||||
|
||||
### Очищення
|
||||
|
||||
```ts
|
||||
storage.clear();
|
||||
```
|
||||
|
||||
Якщо заданий `prefix` — видаляє лише ключі поточного namespace. Без `prefix` — очищає весь `localStorage`.
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "@crux-kit/local-storage",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"types": "src/index.ts",
|
||||
"private": true,
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './main';
|
||||
@@ -0,0 +1,125 @@
|
||||
interface StorageEnvelope<T> {
|
||||
value: T;
|
||||
expiresAt?: number;
|
||||
}
|
||||
|
||||
interface LocalStorageServiceConfig {
|
||||
prefix?: string;
|
||||
logger?: Console;
|
||||
}
|
||||
|
||||
interface SetOptions {
|
||||
ttlMs?: number;
|
||||
}
|
||||
|
||||
export class LocalStorageService {
|
||||
private _prefix?: string;
|
||||
private _logger?: Console;
|
||||
|
||||
constructor(config?: LocalStorageServiceConfig) {
|
||||
this._prefix = config?.prefix;
|
||||
this._logger = config?.logger;
|
||||
}
|
||||
|
||||
private _buildKey(key: string): string {
|
||||
return this._prefix ? `${this._prefix}:${key}` : key;
|
||||
}
|
||||
|
||||
private _stripPrefix(rawKey: string): string {
|
||||
return this._prefix ? rawKey.slice(this._prefix.length + 1) : rawKey;
|
||||
}
|
||||
|
||||
public get<T>(key: string): T | undefined {
|
||||
try {
|
||||
const raw = localStorage.getItem(this._buildKey(key));
|
||||
|
||||
if (raw == null) return undefined;
|
||||
|
||||
const envelope = JSON.parse(raw) as StorageEnvelope<T>;
|
||||
|
||||
if (envelope.expiresAt != null && Date.now() > envelope.expiresAt) {
|
||||
this.remove(key);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return envelope.value;
|
||||
} catch (error) {
|
||||
this._logger?.error(`[LocalStorageService] get "${key}":`, error);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public set<T>(key: string, value: T, options?: SetOptions): void {
|
||||
try {
|
||||
const envelope: StorageEnvelope<T> = {
|
||||
value,
|
||||
expiresAt: options?.ttlMs != null ? Date.now() + options.ttlMs : undefined,
|
||||
};
|
||||
|
||||
localStorage.setItem(this._buildKey(key), JSON.stringify(envelope));
|
||||
} catch (error) {
|
||||
this._logger?.error(`[LocalStorageService] set "${key}":`, error);
|
||||
}
|
||||
}
|
||||
|
||||
public getOrSet<T>(key: string, fallback: T, options?: SetOptions): T {
|
||||
const existing = this.get<T>(key);
|
||||
|
||||
if (existing != null) return existing;
|
||||
|
||||
this.set(key, fallback, options);
|
||||
return fallback;
|
||||
}
|
||||
|
||||
public has(key: string): boolean {
|
||||
return localStorage.getItem(this._buildKey(key)) != null;
|
||||
}
|
||||
|
||||
public remove(key: string): void {
|
||||
try {
|
||||
localStorage.removeItem(this._buildKey(key));
|
||||
} catch (error) {
|
||||
this._logger?.error(`[LocalStorageService] remove "${key}":`, error);
|
||||
}
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
try {
|
||||
if (this._prefix) {
|
||||
this.keys().forEach((key) => this.remove(key));
|
||||
} else {
|
||||
localStorage.clear();
|
||||
}
|
||||
} catch (error) {
|
||||
this._logger?.error('[LocalStorageService] clear:', error);
|
||||
}
|
||||
}
|
||||
|
||||
public getAll<T extends object>(): Partial<T> {
|
||||
return this.keys().reduce<T>((acc, key) => {
|
||||
const value = this.get(key);
|
||||
|
||||
if (value != null) {
|
||||
acc[key as keyof T] = value as T[keyof T];
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {} as T);
|
||||
}
|
||||
|
||||
public keys(): string[] {
|
||||
const result: string[] = [];
|
||||
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const rawKey = localStorage.key(i);
|
||||
|
||||
if (rawKey == null) continue;
|
||||
|
||||
if (this._prefix && !rawKey.startsWith(`${this._prefix}:`)) continue;
|
||||
|
||||
result.push(this._stripPrefix(rawKey));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2023", "DOM"],
|
||||
"skipLibCheck": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"composite": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Reference in New Issue
Block a user