diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..3e00276 --- /dev/null +++ b/.npmignore @@ -0,0 +1,2 @@ +tsconfig.json +src \ No newline at end of file diff --git a/dist/config.d.ts b/dist/config.d.ts new file mode 100644 index 0000000..af2b8ec --- /dev/null +++ b/dist/config.d.ts @@ -0,0 +1,28 @@ +interface Formats { + number: Record; + date: Record; + time: Record; +} +interface Options { + fallbackLocale: string; + initialLocale: string; + formats: Formats; + loadingDelay: number; + warnOnMissingMessages: boolean; +} +interface GetClientLocaleOptions { + navigator?: boolean; + hash?: string; + search?: string; + pathname?: RegExp; + hostname?: RegExp; +} +interface ConfigureOptions { + fallbackLocale: string; + initialLocale?: string | GetClientLocaleOptions; + formats?: Partial; + loadingDelay?: number; +} +export declare function init(opts: ConfigureOptions): void; +export declare function getOptions(): Options; +export {}; diff --git a/dist/config.js b/dist/config.js new file mode 100644 index 0000000..36e6dcf --- /dev/null +++ b/dist/config.js @@ -0,0 +1,111 @@ +import { currentLocale } from "./stores"; +const defaultFormats = { + number: { + scientific: { notation: "scientific" }, + engineering: { notation: "engineering" }, + compactLong: { notation: "compact", compactDisplay: "long" }, + compactShort: { notation: "compact", compactDisplay: "short" } + }, + date: { + short: { month: "numeric", day: "numeric", year: "2-digit" }, + medium: { month: "short", day: "numeric", year: "numeric" }, + long: { month: "long", day: "numeric", year: "numeric" }, + full: { weekday: "long", month: "long", day: "numeric", year: "numeric" } + }, + time: { + short: { hour: "numeric", minute: "numeric" }, + medium: { hour: "numeric", minute: "numeric", second: "numeric" }, + long: { + hour: "numeric", + minute: "numeric", + second: "numeric", + timeZoneName: "short" + }, + full: { + hour: "numeric", + minute: "numeric", + second: "numeric", + timeZoneName: "short" + } + } +}; +const defaultOptions = { + fallbackLocale: null, + initialLocale: null, + loadingDelay: 200, + formats: defaultFormats, + warnOnMissingMessages: true +}; +const options = defaultOptions; +const getFromQueryString = (queryString, key) => { + const keyVal = queryString.split("&").find(i => i.indexOf(`${key}=`) === 0); + if (keyVal) { + return keyVal.split("=").pop(); + } + return null; +}; +const getFirstMatch = (base, pattern) => { + const match = pattern.exec(base); + // istanbul ignore if + if (!match) + return null; + // istanbul ignore else + return match[1] || null; +}; +function getClientLocale({ navigator, hash, search, pathname, hostname }) { + let locale; + // istanbul ignore next + if (typeof window === "undefined") + return null; + if (hostname) { + locale = getFirstMatch(window.location.hostname, hostname); + if (locale) + return locale; + } + if (pathname) { + locale = getFirstMatch(window.location.pathname, pathname); + if (locale) + return locale; + } + if (navigator) { + // istanbul ignore else + locale = window.navigator.language || window.navigator.languages[0]; + if (locale) + return locale; + } + if (search) { + locale = getFromQueryString(window.location.search.substr(1), search); + if (locale) + return locale; + } + if (hash) { + locale = getFromQueryString(window.location.hash.substr(1), hash); + if (locale) + return locale; + } + return null; +} +export function init(opts) { + const { formats, ...rest } = opts; + const initialLocale = opts.initialLocale + ? typeof opts.initialLocale === "string" + ? opts.initialLocale + : getClientLocale(opts.initialLocale) || opts.fallbackLocale + : opts.fallbackLocale; + Object.assign(options, rest, { initialLocale }); + if (formats) { + if ("number" in formats) { + Object.assign(options.formats.number, formats.number); + } + if ("date" in formats) { + Object.assign(options.formats.date, formats.date); + } + if ("time" in formats) { + Object.assign(options.formats.time, formats.time); + } + } + return currentLocale.set(initialLocale); +} +export function getOptions() { + return options; +} diff --git a/dist/formatters.d.ts b/dist/formatters.d.ts new file mode 100644 index 0000000..6f038a8 --- /dev/null +++ b/dist/formatters.d.ts @@ -0,0 +1,14 @@ +declare type IntlFormatterOptions = T & { + format?: string; + locale?: string; +}; +interface MemoizedIntlFormatter { + (options?: IntlFormatterOptions): T; +} +export declare const getNumberFormatter: MemoizedIntlFormatter; +export declare const getDateFormatter: MemoizedIntlFormatter; +export declare const getTimeFormatter: MemoizedIntlFormatter; +export declare const formatTime: (t: Date, options: Record) => string; +export declare const formatDate: (d: Date, options: Record) => string; +export declare const formatNumber: (n: number, options: Record) => string; +export {}; diff --git a/dist/formatters.js b/dist/formatters.js new file mode 100644 index 0000000..36c3757 --- /dev/null +++ b/dist/formatters.js @@ -0,0 +1,124 @@ +import { getCurrentLocale } from './stores'; +import { getOptions } from './config'; +import { monadicMemoize } from './memoize'; +const getIntlFormatterOptions = (type, name) => { + const formats = getOptions().formats; + if (type in formats && name in formats[type]) { + return formats[type][name]; + } + throw new Error(`[icu-helpers] Unknown "${name}" ${type} format.`); +}; +export const getNumberFormatter = monadicMemoize(({ locale, format, ...options } = {}) => { + locale = locale || getCurrentLocale(); + if (locale == null) { + throw new Error('[icu-helpers] A "locale" must be set to format numbers'); + } + if (format) { + options = getIntlFormatterOptions('number', format); + } + return new Intl.NumberFormat(locale, options); +}); +export const getDateFormatter = monadicMemoize(({ locale, format, ...options } = {}) => { + locale = locale || getCurrentLocale(); + if (locale == null) { + throw new Error('[icu-helpers] A "locale" must be set to format dates'); + } + if (format) + options = getIntlFormatterOptions('date', format); + else if (Object.keys(options).length === 0) { + options = getIntlFormatterOptions('date', 'short'); + } + return new Intl.DateTimeFormat(locale, options); +}); +export const getTimeFormatter = monadicMemoize(({ locale, format, ...options } = {}) => { + locale = locale || getCurrentLocale(); + if (locale == null) { + throw new Error('[svelte-i18n] A "locale" must be set to format time values'); + } + if (format) + options = getIntlFormatterOptions('time', format); + else if (Object.keys(options).length === 0) { + options = getIntlFormatterOptions('time', 'short'); + } + return new Intl.DateTimeFormat(locale, options); +}); +export const formatTime = (t, options) => getTimeFormatter(options).format(t); +export const formatDate = (d, options) => getDateFormatter(options).format(d); +export const formatNumber = (n, options) => getNumberFormatter(options).format(n); +// import { getOptions } from "./config"; +// const CACHED = Object.create(null); +// export function formatterOptions(type, style) { +// return getOptions().formats[type][style] || {}; +// } +// const getIntlFormatterOptions = (type, name) => { +// const formats = getOptions().formats +// if (type in formats && name in formats[type]) { +// return formats[type][name] +// } +// throw new Error(`[icu-helpers] Unknown "${name}" ${type} format.`) +// } +// // const getIntlFormatterOptions = ( +// // type: 'time' | 'number' | 'date', +// // name: string +// // ): any => { +// // const formats = getOptions().formats +// // if (type in formats && name in formats[type]) { +// // return formats[type][name] +// // } +// // throw new Error(`[svelte-i18n] Unknown "${name}" ${type} format.`) +// // } +// // export const getNumberFormatter: MemoizedIntlFormatter< +// // Intl.NumberFormat, +// // Intl.NumberFormatOptions +// // > = monadicMemoize(({ locale, format, ...options } = {}) => { +// // locale = locale || getCurrentLocale() +// // if (locale == null) { +// // throw new Error('[svelte-i18n] A "locale" must be set to format numbers') +// // } +// // if (format) { +// // options = getIntlFormatterOptions('number', format) +// // } +// // return new Intl.NumberFormat(locale, options) +// // }) +// // export const getDateFormatter: MemoizedIntlFormatter< +// // Intl.DateTimeFormat, +// // Intl.DateTimeFormatOptions +// // > = monadicMemoize(({ locale, format, ...options } = {}) => { +// // locale = locale || getCurrentLocale() +// // if (locale == null) { +// // throw new Error('[svelte-i18n] A "locale" must be set to format dates') +// // } +// // if (format) options = getIntlFormatterOptions('date', format) +// // else if (Object.keys(options).length === 0) { +// // options = getIntlFormatterOptions('date', 'short') +// // } +// // return new Intl.DateTimeFormat(locale, options) +// // }) +// // export const getTimeFormatter: MemoizedIntlFormatter< +// // Intl.DateTimeFormat, +// // Intl.DateTimeFormatOptions +// // > = monadicMemoize(({ locale, format, ...options } = {}) => { +// // locale = locale || getCurrentLocale() +// // if (locale == null) { +// // throw new Error( +// // '[svelte-i18n] A "locale" must be set to format time values' +// // ) +// // } +// // if (format) options = getIntlFormatterOptions('time', format) +// // else if (Object.keys(options).length === 0) { +// // options = getIntlFormatterOptions('time', 'short') +// // } +// // return new Intl.DateTimeFormat(locale, options) +// // }) +// // export function getNumberFormatter({ locale, format, ...options }) { +// // let key = "number" + locale + JSON.stringify(opts); +// // return CACHED[key] || (CACHED[key] = new Intl.NumberFormat(locale, opts)); +// // } +// // export function getDateFormatter(locale, opts) { +// // let key = "date" + locale + JSON.stringify(opts); +// // return CACHED[key] || (CACHED[key] = new Intl.DateTimeFormat(locale, opts)); +// // } +// // export function getTimeFormatter(locale, opts) { +// // let key = "time" + locale + JSON.stringify(opts); +// // return CACHED[key] || (CACHED[key] = new Intl.DateTimeFormat(locale, opts)); +// // } diff --git a/dist/index.d.ts b/dist/index.d.ts new file mode 100644 index 0000000..0bdd288 --- /dev/null +++ b/dist/index.d.ts @@ -0,0 +1,14 @@ +import { init } from "./config"; +import { currentLocale, dictionary, locales, LocaleDictionary } from "./stores"; +import { getNumberFormatter, getDateFormatter, getTimeFormatter, formatTime, formatDate, formatNumber } from "./formatters"; +declare type PluralRule = "zero" | "one" | "two" | "few" | "many" | "other" | number; +declare type PluralOptions = Record; +export declare function __interpolate(value: any): any; +export declare function __plural(value: number, offsetOrOptions: number | PluralOptions, opts?: PluralOptions): string; +export declare function __select(value: any, opts: Record): string; +export declare function __number(value: number, format?: string): string; +export declare function __date(value: Date, format?: string): string; +export declare function __time(value: Date, format?: string): string; +export declare function addMessages(locale: string, messages: LocaleDictionary): void; +export declare function lookupMessage(key: string, locale?: string): string; +export { init, currentLocale, dictionary, locales, getNumberFormatter, getDateFormatter, getTimeFormatter, formatTime, formatDate, formatNumber }; diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 0000000..d74cb1c --- /dev/null +++ b/dist/index.js @@ -0,0 +1,46 @@ +import { init } from "./config"; +import { currentLocale, dictionary, locales, getCurrentLocale } from "./stores"; +import { getNumberFormatter, getDateFormatter, getTimeFormatter, formatTime, formatDate, formatNumber } from "./formatters"; +export function __interpolate(value) { + return value === 0 ? 0 : value || ''; +} +const PLURAL_RULES = Object.create(null); +function getLocalPluralFor(v) { + let loc = getCurrentLocale(); + let pluralRules = PLURAL_RULES[loc] || (PLURAL_RULES[loc] = new Intl.PluralRules(loc)); + return pluralRules.select(v); +} +export function __plural(value, offsetOrOptions, opts) { + if (typeof offsetOrOptions === "number") { + return (opts[value] || + opts[getLocalPluralFor(value - offsetOrOptions)] || + ""); + } + else { + return (offsetOrOptions[value] || + offsetOrOptions[getLocalPluralFor(value)] || + ""); + } +} +export function __select(value, opts) { + return opts[value] || opts['other'] || ''; +} +export function __number(value, format) { + return formatNumber(value, { format }); +} +export function __date(value, format = "short") { + return formatDate(value, { format }); +} +export function __time(value, format = "short") { + return formatTime(value, { format }); +} +export function addMessages(locale, messages) { + dictionary.update(value => { + value[locale] = Object.assign(value[locale] || {}, messages); + return value; + }); +} +export function lookupMessage(key, locale = getCurrentLocale()) { + return dictionary._value[locale][key]; +} +export { init, currentLocale, dictionary, locales, getNumberFormatter, getDateFormatter, getTimeFormatter, formatTime, formatDate, formatNumber }; diff --git a/dist/memoize.d.ts b/dist/memoize.d.ts new file mode 100644 index 0000000..dc035a2 --- /dev/null +++ b/dist/memoize.d.ts @@ -0,0 +1,3 @@ +declare type MemoizedFunction = (fn: F) => F; +declare const monadicMemoize: MemoizedFunction; +export { monadicMemoize }; diff --git a/dist/memoize.js b/dist/memoize.js new file mode 100644 index 0000000..116b340 --- /dev/null +++ b/dist/memoize.js @@ -0,0 +1,12 @@ +const monadicMemoize = fn => { + const cache = Object.create(null); + const memoizedFn = (arg) => { + const cacheKey = JSON.stringify(arg); + if (cacheKey in cache) { + return cache[cacheKey]; + } + return (cache[cacheKey] = fn(arg)); + }; + return memoizedFn; +}; +export { monadicMemoize }; diff --git a/dist/stores.d.ts b/dist/stores.d.ts new file mode 100644 index 0000000..dafd1bf --- /dev/null +++ b/dist/stores.d.ts @@ -0,0 +1,14 @@ +declare class WritableStore { + _value: T; + _subscribers: ((T: any) => void)[]; + constructor(v: any); + subscribe(fn: (value: T) => void): () => ((T: any) => void)[]; + set(v: T): void; + update(cb: (value: T) => T): void; +} +export declare type LocaleDictionary = Record; +export declare const currentLocale: WritableStore; +export declare const dictionary: WritableStore>>; +export declare const locales: WritableStore; +export declare function getCurrentLocale(): string; +export {}; diff --git a/dist/stores.js b/dist/stores.js new file mode 100644 index 0000000..9edcac9 --- /dev/null +++ b/dist/stores.js @@ -0,0 +1,29 @@ +// This was a svelte-only library, it could be a writable store +class WritableStore { + constructor(v) { + this._value = v; + this._subscribers = []; + } + subscribe(fn) { + this._subscribers.push(fn); + fn(this._value); + return () => this._subscribers.splice(this._subscribers.indexOf(fn), 1); + } + set(v) { + this._value = v; + this._subscribers.forEach(fn => fn(v)); + } + update(cb) { + this._value = cb(this._value); + this._subscribers.forEach(fn => fn(this._value)); + } +} +export const currentLocale = new WritableStore(undefined); +export const dictionary = new WritableStore({}); +export const locales = new WritableStore([]); +dictionary.subscribe(dict => { + locales.set(Object.keys(dict)); +}); +export function getCurrentLocale() { + return currentLocale._value; +} diff --git a/package-lock.json b/package-lock.json index 79e1a8b..206b290 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1092,6 +1092,25 @@ "@types/yargs": "^13.0.0" } }, + "@rollup/plugin-typescript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-2.1.0.tgz", + "integrity": "sha512-7lXKGY06aofrceVez/YnN2axttFdHSqlUBpCJ6ebzDfxwLDKMgSV5lD4ykBcdgE7aK3egxuLkD/HKyRB5L8Log==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^3.0.0", + "resolve": "^1.13.1" + } + }, + "@rollup/pluginutils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.0.4.tgz", + "integrity": "sha512-buc0oeq2zqQu2mpMyvZgAaQvitikYjT/4JYhA4EXwxX8/g0ZGHoGiX+0AwmfhrNqH4oJv67gn80sTZFQ/jL1bw==", + "dev": true, + "requires": { + "estree-walker": "^0.6.1" + } + }, "@types/babel__core": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.3.tgz", @@ -2026,6 +2045,12 @@ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true }, + "estree-walker": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", + "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", + "dev": true + }, "esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -5392,6 +5417,12 @@ "prelude-ls": "~1.1.2" } }, + "typescript": { + "version": "3.7.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.5.tgz", + "integrity": "sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw==", + "dev": true + }, "unicode-canonical-property-names-ecmascript": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", diff --git a/package.json b/package.json index 606f1ca..790ed5b 100644 --- a/package.json +++ b/package.json @@ -5,13 +5,11 @@ "repository": "", "license": "MIT", "scripts": { + "build": "tsc", "test": "jest test" }, - "publishConfig": { - "access": "public" - }, - "main": "src/index.js", - "module": "src/index.js", + "main": "dist/index.js", + "types": "dist/index.d.ts", "dependencies": {}, "keywords": [ "babel-plugin", @@ -19,6 +17,8 @@ ], "devDependencies": { "@babel/preset-env": "^7.0.0", + "@rollup/plugin-typescript": "^2.1.0", + "typescript": "^3.7.5", "babel-jest": "^24.9.0", "jest": "^24.9.0" } diff --git a/src/config.js b/src/config.ts similarity index 76% rename from src/config.js rename to src/config.ts index 9214e9e..3e8d6f2 100644 --- a/src/config.js +++ b/src/config.ts @@ -1,6 +1,34 @@ import { currentLocale } from "./stores"; +interface Formats { + number: Record + date: Record + time: Record +} + +interface Options { + fallbackLocale: string + initialLocale: string + formats: Formats + loadingDelay: number + warnOnMissingMessages: boolean +} + +interface GetClientLocaleOptions { + navigator?: boolean; + hash?: string; + search?: string; + pathname?: RegExp; + hostname?: RegExp; +} + +interface ConfigureOptions { + fallbackLocale: string; + initialLocale?: string | GetClientLocaleOptions; + formats?: Partial; + loadingDelay?: number; +} -const defaultFormats = { +const defaultFormats: Formats = { number: { scientific: { notation: "scientific" }, engineering: { notation: "engineering" }, @@ -31,14 +59,15 @@ const defaultFormats = { } }; -const defaultOptions = { +const defaultOptions: Options = { fallbackLocale: null, initialLocale: null, loadingDelay: 200, formats: defaultFormats, warnOnMissingMessages: true }; -const options = defaultOptions; +const options: Options = defaultOptions; + const getFromQueryString = (queryString, key) => { const keyVal = queryString.split("&").find(i => i.indexOf(`${key}=`) === 0); @@ -56,7 +85,13 @@ const getFirstMatch = (base, pattern) => { return match[1] || null; }; -function getClientLocale({ navigator, hash, search, pathname, hostname }) { +function getClientLocale({ + navigator, + hash, + search, + pathname, + hostname +}: GetClientLocaleOptions) { let locale; // istanbul ignore next @@ -91,7 +126,7 @@ function getClientLocale({ navigator, hash, search, pathname, hostname }) { return null; } -export function init(opts) { +export function init(opts: ConfigureOptions) { const { formats, ...rest } = opts; const initialLocale = opts.initialLocale ? typeof opts.initialLocale === "string" @@ -116,6 +151,6 @@ export function init(opts) { return currentLocale.set(initialLocale); } -export function getOptions() { +export function getOptions(): Options { return options; } \ No newline at end of file diff --git a/src/formatters.js b/src/formatters.ts similarity index 82% rename from src/formatters.js rename to src/formatters.ts index 88b195f..aef2420 100644 --- a/src/formatters.js +++ b/src/formatters.ts @@ -2,6 +2,15 @@ import { getCurrentLocale } from './stores' import { getOptions } from './config' import { monadicMemoize } from './memoize' +type IntlFormatterOptions = T & { + format?: string + locale?: string +} + +interface MemoizedIntlFormatter { + (options?: IntlFormatterOptions): T; +} + const getIntlFormatterOptions = (type, name) => { const formats = getOptions().formats if (type in formats && name in formats[type]) { @@ -11,7 +20,10 @@ const getIntlFormatterOptions = (type, name) => { throw new Error(`[icu-helpers] Unknown "${name}" ${type} format.`) } -export const getNumberFormatter = monadicMemoize(({ locale, format, ...options } = {}) => { +export const getNumberFormatter: MemoizedIntlFormatter< + Intl.NumberFormat, + Intl.NumberFormatOptions +> = monadicMemoize(({ locale, format, ...options } = {}) => { locale = locale || getCurrentLocale() if (locale == null) { throw new Error('[icu-helpers] A "locale" must be set to format numbers') @@ -24,7 +36,10 @@ export const getNumberFormatter = monadicMemoize(({ locale, format, ...options } return new Intl.NumberFormat(locale, options) }) -export const getDateFormatter = monadicMemoize(({ locale, format, ...options } = {}) => { +export const getDateFormatter: MemoizedIntlFormatter< + Intl.DateTimeFormat, + Intl.DateTimeFormatOptions +> = monadicMemoize(({ locale, format, ...options } = {}) => { locale = locale || getCurrentLocale() if (locale == null) { throw new Error('[icu-helpers] A "locale" must be set to format dates') @@ -38,7 +53,10 @@ export const getDateFormatter = monadicMemoize(({ locale, format, ...options } = return new Intl.DateTimeFormat(locale, options) }) -export const getTimeFormatter = monadicMemoize(({ locale, format, ...options } = {}) => { +export const getTimeFormatter: MemoizedIntlFormatter< + Intl.DateTimeFormat, + Intl.DateTimeFormatOptions +> = monadicMemoize(({ locale, format, ...options } = {}) => { locale = locale || getCurrentLocale() if (locale == null) { throw new Error( @@ -54,9 +72,12 @@ export const getTimeFormatter = monadicMemoize(({ locale, format, ...options } = return new Intl.DateTimeFormat(locale, options) }) -export const formatTime = (t, options) => getTimeFormatter(options).format(t); -export const formatDate = (d, options) => getDateFormatter(options).format(d); -export const formatNumber = (n, options) => getNumberFormatter(options).format(n); +export const formatTime = (t: Date, options: Record) => + getTimeFormatter(options).format(t); +export const formatDate = (d: Date, options: Record) => + getDateFormatter(options).format(d); +export const formatNumber = (n: number, options: Record) => + getNumberFormatter(options).format(n); // import { getOptions } from "./config"; diff --git a/src/index.d.ts b/src/index.d.ts deleted file mode 100644 index 2d17441..0000000 --- a/src/index.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -declare module "icu-helpers" { - type MessageFn = (...args: any) => string - interface WritableStore { - subscribe: (v: any) => () => {} - set(v: any): void - update(cb: () => {}): void - clear(): void - } - export const currentLocale: WritableStore - export const locales: WritableStore - export const dictionary: WritableStore - declare function addMessages(locale: any, messages: any): void - declare function setLocale(locale: string): void - declare function lookupMessage(key: string, locale?: string): string | MessageFn - declare function init(Record): void - declare function getDateFormatter(opts: any): Intl.NumberFormat - declare function getNumberFormatter(opts: any): Intl.DateTimeFormat - declare function getTimeFormatter(opts: any): Intl.DateTimeFormat - declare function formatTime(value: Date): string - declare function formatDate(value: Date): string - declare function formatNumber(value: Number): string -} \ No newline at end of file diff --git a/src/index.js b/src/index.js deleted file mode 100644 index a227979..0000000 --- a/src/index.js +++ /dev/null @@ -1,69 +0,0 @@ - -import { init } from "./config"; -import { currentLocale, dictionary, locales } from './stores'; -import { - // formatterOptions, - getNumberFormatter, - getDateFormatter, - getTimeFormatter, - formatTime, - formatDate, - formatNumber -} from "./formatters"; - -export function __interpolate(value) { - return value === 0 ? 0 : value || ''; -} - -const PLURAL_RULES = Object.create(null); -function getLocalPluralFor(v) { - let pluralRules = PLURAL_RULES[currentLocale._value] || (PLURAL_RULES[currentLocale._value] = new Intl.PluralRules(currentLocale._value)); - return pluralRules.select(v); -} -export function __plural(value, offsetOrOptions, opts) { - if (typeof offsetOrOptions === 'number') { - return opts[value] || opts[getLocalPluralFor(value - offsetOrOptions)] || ""; - } else { - return offsetOrOptions[value] || offsetOrOptions[getLocalPluralFor(value)] || ""; - } -} - -export function __select(value, opts) { - return opts[value] || opts['other']; -} - -export function __number(value, format) { - return formatNumber(value, { format }); -} - -export function __date(value, format = "short") { - return formatDate(value, { format }); -} - -export function __time(value, format = "short") { - return formatTime(value, { format }); -} - -export function addMessages(locale, messages) { - dictionary.update(value => { - value[locale] = Object.assign(value[locale] || {}, messages); - return value; - }); -} - -export function lookupMessage(key, locale = currentLocale._value) { - return dictionary._value[locale][key]; -} - -export { - init, - currentLocale, - dictionary, - locales, - getNumberFormatter, - getDateFormatter, - getTimeFormatter, - formatTime, - formatDate, - formatNumber -}; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..3fa9571 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,89 @@ + +import { init } from "./config"; +import { + currentLocale, + dictionary, + locales, + getCurrentLocale, + LocaleDictionary +} from "./stores"; +import { + getNumberFormatter, + getDateFormatter, + getTimeFormatter, + formatTime, + formatDate, + formatNumber +} from "./formatters"; + +type PluralRule = "zero" | "one" | "two" | "few" | "many" | "other" | number +type PluralOptions = Record +export function __interpolate(value: any) { + return value === 0 ? 0 : value || ''; +} + +const PLURAL_RULES = Object.create(null); +function getLocalPluralFor(v: number): PluralRule { + let loc = getCurrentLocale(); + let pluralRules = PLURAL_RULES[loc] || (PLURAL_RULES[loc] = new Intl.PluralRules(loc)); + return pluralRules.select(v); +} +export function __plural( + value: number, + offsetOrOptions: number | PluralOptions, + opts?: PluralOptions +): string { + if (typeof offsetOrOptions === "number") { + return ( + opts[value] || + opts[getLocalPluralFor(value - offsetOrOptions)] || + "" + ); + } else { + return ( + offsetOrOptions[value] || + offsetOrOptions[getLocalPluralFor(value)] || + "" + ); + } +} + +export function __select(value: any, opts: Record): string { + return opts[value] || opts['other'] || ''; +} + +export function __number(value: number, format?: string): string { + return formatNumber(value, { format }); +} + +export function __date(value: Date, format = "short"): string { + return formatDate(value, { format }); +} + +export function __time(value: Date, format = "short"): string { + return formatTime(value, { format }); +} + +export function addMessages(locale: string, messages: LocaleDictionary): void { + dictionary.update(value => { + value[locale] = Object.assign(value[locale] || {}, messages); + return value; + }); +} + +export function lookupMessage(key: string, locale: string = getCurrentLocale()) { + return dictionary._value[locale][key]; +} + +export { + init, + currentLocale, + dictionary, + locales, + getNumberFormatter, + getDateFormatter, + getTimeFormatter, + formatTime, + formatDate, + formatNumber +}; \ No newline at end of file diff --git a/src/memoize.js b/src/memoize.js deleted file mode 100644 index 7d2f0dd..0000000 --- a/src/memoize.js +++ /dev/null @@ -1,10 +0,0 @@ -export const monadicMemoize = fn => { - const cache = Object.create(null) - return (arg) => { - const cacheKey = JSON.stringify(arg) - if (cacheKey in cache) { - return cache[cacheKey] - } - return (cache[cacheKey] = fn(arg)) - } -} diff --git a/src/memoize.ts b/src/memoize.ts new file mode 100644 index 0000000..c3b0387 --- /dev/null +++ b/src/memoize.ts @@ -0,0 +1,14 @@ +type MemoizedFunction = (fn: F) => F; + +const monadicMemoize: MemoizedFunction = fn => { + const cache = Object.create(null); + const memoizedFn: any = (arg: unknown) => { + const cacheKey = JSON.stringify(arg); + if (cacheKey in cache) { + return cache[cacheKey]; + } + return (cache[cacheKey] = fn(arg)); + }; + return memoizedFn; +}; +export { monadicMemoize }; diff --git a/src/stores.js b/src/stores.js deleted file mode 100644 index ff009e5..0000000 --- a/src/stores.js +++ /dev/null @@ -1,31 +0,0 @@ -// This was a svelte-only library, it could be a writable store -class WritableStore { - constructor(v) { - this._initialValue = v; - this._value = this._initialValue; - this._subscribers = []; - } - subscribe(fn) { - this._subscribers.push(fn); - fn(this._value); - return () => this._subscribers.splice(this._subscribers.indexOf(fn), 1); - } - set(v) { - this._value = v; - this._subscribers.forEach(fn => fn(v)); - } - update(cb) { - this._value = cb(this._value); - this._subscribers.forEach(fn => fn(this._value)); - } -} - -export const currentLocale = new WritableStore(); -export const dictionary = new WritableStore({}); -export const locales = new WritableStore([]); -dictionary.subscribe(dict => { - locales.set(Object.keys(dict)); -}); -export function getCurrentLocale() { - return currentLocale._value; -} diff --git a/src/stores.ts b/src/stores.ts new file mode 100644 index 0000000..298656a --- /dev/null +++ b/src/stores.ts @@ -0,0 +1,34 @@ +// This was a svelte-only library, it could be a writable store +class WritableStore { + _value: T; + _subscribers: ((T) => void)[]; + constructor(v) { + this._value = v; + this._subscribers = []; + } + subscribe(fn: (value: T) => void) { + this._subscribers.push(fn); + fn(this._value); + return () => this._subscribers.splice(this._subscribers.indexOf(fn), 1); + } + set(v: T) { + this._value = v; + this._subscribers.forEach(fn => fn(v)); + } + update(cb: (value: T) => T) { + this._value = cb(this._value); + this._subscribers.forEach(fn => fn(this._value)); + } +} +export type LocaleDictionary = Record; +/*export */ type Dictionary = Record; + +export const currentLocale = new WritableStore(undefined); +export const dictionary = new WritableStore({}); +export const locales = new WritableStore([]); +dictionary.subscribe(dict => { + locales.set(Object.keys(dict)); +}); +export function getCurrentLocale(): string { + return currentLocale._value; +} diff --git a/tests/index.test.js b/tests/index.test.js index 46a5acd..5417de5 100644 --- a/tests/index.test.js +++ b/tests/index.test.js @@ -10,7 +10,7 @@ import { dictionary, locales, init -} from "../src"; +} from "../dist"; beforeEach(() => { currentLocale.set(undefined); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..de97af5 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "module": "ESNext", + "target": "ESNext", + "declaration": true, + "outDir": "./dist" + }, + "include": [ + "src/*" + ], + "exclude": [ + "node_modules/**/*" + ] +} \ No newline at end of file