├── src ├── locale │ ├── locale.ts │ ├── locales.ts │ └── locale-data.ts ├── test262.ts ├── list │ └── list.ts ├── numbering-system │ └── numbering-system.ts ├── relevant-extension-key │ └── relevant-extension-key.ts ├── support │ └── supports-relative-time-format.ts ├── numeric │ └── numeric.ts ├── relative-time-format │ ├── matcher │ │ ├── matcher-result.ts │ │ ├── matcher-options.ts │ │ ├── best-fit-matcher │ │ │ └── best-fit-matcher.ts │ │ ├── best-available-locale │ │ │ └── best-available-locale.ts │ │ └── lookup-matcher │ │ │ └── lookup-matcher.ts │ ├── supported-locales │ │ ├── supported-locales-options.ts │ │ ├── best-fit-supported-locales.ts │ │ ├── lookup-supported-locales.ts │ │ └── supported-locales.ts │ ├── resolve-locale │ │ ├── resolve-locale-result.ts │ │ ├── resolve-locale-options.ts │ │ └── resolve-locale.ts │ ├── relative-time-format │ │ ├── relative-time-format-options.ts │ │ ├── resolved-relative-time-format-options.ts │ │ └── relative-time-format.ts │ ├── internal-slot │ │ ├── relative-time-format-static-internals.ts │ │ ├── relative-time-format-instance-internals.ts │ │ └── internal-slot.ts │ ├── format-relative-time-to-parts │ │ └── format-relative-time-to-parts.ts │ ├── default-locale │ │ └── get-default-locale.ts │ ├── format-relative-time │ │ └── format-relative-time.ts │ ├── resolve-plural │ │ └── resolve-plural.ts │ ├── numbering-systems │ │ └── numbering-systems.ts │ ├── unicode-extension │ │ └── unicode-extension.ts │ ├── make-parts-list │ │ └── make-parts-list.ts │ └── partition-relative-time-pattern │ │ └── partition-relative-time-pattern.ts ├── style │ └── style.ts ├── index.ts ├── locale-matcher │ └── locale-matcher.ts ├── unit │ ├── relative-time-unit.ts │ └── singular-relative-time-unit.ts ├── assert │ ├── is-record.ts │ └── is-list.ts ├── util │ ├── element-of.ts │ ├── to-number.ts │ ├── to-string.ts │ ├── to-boolean.ts │ ├── is-property-key.ts │ ├── get.ts │ ├── to-object.ts │ ├── get-option.ts │ └── same-value.ts ├── patch │ └── patch.ts ├── relative-time-format-part │ └── relative-time-format-part.ts ├── intl-object │ └── intl-object.ts └── typings.d.ts ├── prettier.config.js ├── .gitmodules ├── documentation └── asset │ ├── logo.png │ └── logo.svg ├── tslint.json ├── scaffold.config.js ├── tsconfig.json ├── scripts └── build-data │ ├── tsconfig.json │ ├── ts │ └── create-program-from-sources.ts │ └── build-data.ts ├── test ├── supported-locales-of.test.ts ├── resolved-options.test.ts ├── format-to-parts.test.ts └── format.test.ts ├── CONTRIBUTING.md ├── .gitignore ├── LICENSE.md ├── test262-runner.js ├── CHANGELOG.md ├── rollup.config.js ├── CODE_OF_CONDUCT.md ├── package.json └── README.md /src/locale/locale.ts: -------------------------------------------------------------------------------- 1 | export type Locale = string; 2 | -------------------------------------------------------------------------------- /src/test262.ts: -------------------------------------------------------------------------------- 1 | import {patch} from "./patch/patch"; 2 | patch(); 3 | -------------------------------------------------------------------------------- /src/list/list.ts: -------------------------------------------------------------------------------- 1 | export type List = Record | T[]; 2 | -------------------------------------------------------------------------------- /src/numbering-system/numbering-system.ts: -------------------------------------------------------------------------------- 1 | export type NumberingSystem = string; 2 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require("@wessberg/ts-config/prettier.config"); 2 | -------------------------------------------------------------------------------- /src/relevant-extension-key/relevant-extension-key.ts: -------------------------------------------------------------------------------- 1 | export type RelevantExtensionKey = "nu"; 2 | -------------------------------------------------------------------------------- /src/locale/locales.ts: -------------------------------------------------------------------------------- 1 | import {Locale} from "./locale"; 2 | 3 | export type Locales = Locale[]; 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "test262"] 2 | path = test262 3 | url = https://github.com/tc39/test262.git 4 | -------------------------------------------------------------------------------- /src/support/supports-relative-time-format.ts: -------------------------------------------------------------------------------- 1 | export const SUPPORTS_RELATIVE_TIME_FORMAT = "RelativeTimeFormat" in Intl; 2 | -------------------------------------------------------------------------------- /documentation/asset/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wessberg/intl-relative-time-format/HEAD/documentation/asset/logo.png -------------------------------------------------------------------------------- /src/numeric/numeric.ts: -------------------------------------------------------------------------------- 1 | import {ElementOf} from "../util/element-of"; 2 | 3 | export const NUMERIC = ["always", "auto"] as const; 4 | 5 | export type Numeric = ElementOf; 6 | -------------------------------------------------------------------------------- /src/relative-time-format/matcher/matcher-result.ts: -------------------------------------------------------------------------------- 1 | import {Locale} from "../../locale/locale"; 2 | 3 | export interface MatcherResult { 4 | locale: Locale; 5 | extension?: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/style/style.ts: -------------------------------------------------------------------------------- 1 | import {ElementOf} from "../util/element-of"; 2 | 3 | export const STYLE = ["long", "short", "narrow"] as const; 4 | 5 | export type Style = ElementOf; 6 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/@wessberg/ts-config/tslint.json", 3 | "rules": { 4 | "no-gratuitous-expressions": false, 5 | "no-duplicate-string": false 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import {SUPPORTS_RELATIVE_TIME_FORMAT} from "./support/supports-relative-time-format"; 2 | import {patch} from "./patch/patch"; 3 | 4 | if (!SUPPORTS_RELATIVE_TIME_FORMAT) { 5 | patch(); 6 | } 7 | -------------------------------------------------------------------------------- /src/relative-time-format/matcher/matcher-options.ts: -------------------------------------------------------------------------------- 1 | import {Locales} from "../../locale/locales"; 2 | 3 | export interface MatcherOptions { 4 | availableLocales: Locales; 5 | requestedLocales: Locales; 6 | } 7 | -------------------------------------------------------------------------------- /src/locale-matcher/locale-matcher.ts: -------------------------------------------------------------------------------- 1 | import {ElementOf} from "../util/element-of"; 2 | 3 | export const LOCALE_MATCHER = ["lookup", "best fit"] as const; 4 | 5 | export type LocaleMatcher = ElementOf; 6 | -------------------------------------------------------------------------------- /src/relative-time-format/supported-locales/supported-locales-options.ts: -------------------------------------------------------------------------------- 1 | import {LocaleMatcher} from "../../locale-matcher/locale-matcher"; 2 | 3 | export interface SupportedLocalesOptions { 4 | localeMatcher: LocaleMatcher; 5 | } 6 | -------------------------------------------------------------------------------- /src/unit/relative-time-unit.ts: -------------------------------------------------------------------------------- 1 | import {SingularRelativeTimeUnit} from "./singular-relative-time-unit"; 2 | 3 | export type RelativeTimeUnit = SingularRelativeTimeUnit | "seconds" | "minutes" | "hours" | "days" | "weeks" | "months" | "quarters" | "years"; 4 | -------------------------------------------------------------------------------- /scaffold.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require("@wessberg/ts-config/scaffold.config"), 3 | logo: { 4 | url: 5 | "https://raw.githubusercontent.com/wessberg/intl-relative-time-format/master/documentation/asset/logo.png", 6 | height: 50 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@wessberg/ts-config", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "target": "es2020", 6 | "declaration": true, 7 | "lib": ["dom", "es5", "es2015", "es2015.collection", "es2016.array.include", "es2017"] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/assert/is-record.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns true if the given item is a record 3 | * @param {T} item 4 | * @return {item is T} 5 | */ 6 | export function isRecord(item: T): item is Exclude { 7 | return Object.prototype.toString.call(item) === "[object Object]"; 8 | } 9 | -------------------------------------------------------------------------------- /src/util/element-of.ts: -------------------------------------------------------------------------------- 1 | export type ElementOf = IterableType extends (infer ElementType)[] 2 | ? ElementType 3 | : IterableType extends readonly (infer ReadonlyElementType)[] 4 | ? ReadonlyElementType 5 | : IterableType extends Set 6 | ? SetElementType 7 | : never; 8 | -------------------------------------------------------------------------------- /src/util/to-number.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The abstract operation ToNumber converts argument to a value of type Number 3 | * https://tc39.es/ecma262/#sec-tonumber 4 | * @param {*} argument 5 | * @returns {boolean} 6 | */ 7 | export function toNumber(argument: unknown): number { 8 | return Number(argument); 9 | } 10 | -------------------------------------------------------------------------------- /src/util/to-string.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The abstract operation ToString converts argument to a value of type String 3 | * https://tc39.es/ecma262/#sec-tostring 4 | * @param {*} argument 5 | * @returns {boolean} 6 | */ 7 | export function toString(argument: unknown): string { 8 | return argument + ""; 9 | } 10 | -------------------------------------------------------------------------------- /src/util/to-boolean.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The abstract operation ToBoolean converts argument to a value of type Boolean 3 | * https://tc39.es/ecma262/#sec-toboolean 4 | * @param {*} argument 5 | * @returns {boolean} 6 | */ 7 | export function toBoolean(argument: unknown): boolean { 8 | return Boolean(argument); 9 | } 10 | -------------------------------------------------------------------------------- /scripts/build-data/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@wessberg/ts-config", 3 | "compilerOptions": { 4 | "target": "es2017", 5 | "module": "commonjs", 6 | "outDir": "compiled", 7 | "lib": [ 8 | "dom", 9 | "es5", 10 | "es2015", 11 | "es2015.collection", 12 | "es2016.array.include", 13 | "es2017" 14 | ] 15 | } 16 | } -------------------------------------------------------------------------------- /src/assert/is-list.ts: -------------------------------------------------------------------------------- 1 | import {isRecord} from "./is-record"; 2 | import {List} from "../list/list"; 3 | 4 | /** 5 | * Returns true if the given item is a List 6 | * @param {T} item 7 | * @return {item is T} 8 | */ 9 | export function isList(item: unknown): item is List { 10 | return Array.isArray(item) || isRecord(item); 11 | } 12 | -------------------------------------------------------------------------------- /src/relative-time-format/resolve-locale/resolve-locale-result.ts: -------------------------------------------------------------------------------- 1 | import {Locale} from "../../locale/locale"; 2 | import {RelevantExtensionKey} from "../../relevant-extension-key/relevant-extension-key"; 3 | 4 | export type ResolveLocaleResult = {[Key in RelevantExtensionKey | "dataLocale" | "locale"]: Key extends RelevantExtensionKey ? string : Locale}; 5 | -------------------------------------------------------------------------------- /src/relative-time-format/relative-time-format/relative-time-format-options.ts: -------------------------------------------------------------------------------- 1 | import {LocaleMatcher} from "../../locale-matcher/locale-matcher"; 2 | import {Style} from "../../style/style"; 3 | import {Numeric} from "../../numeric/numeric"; 4 | 5 | export interface RelativeTimeFormatOptions { 6 | localeMatcher: LocaleMatcher; 7 | style: Style; 8 | numeric: Numeric; 9 | } 10 | -------------------------------------------------------------------------------- /test/supported-locales-of.test.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import test from "ava"; 3 | import "../src/test262"; 4 | import "../locale-data/en"; 5 | 6 | // tslint:disable 7 | 8 | test("Can properly resolve supported locales based on the given options. #1", t => { 9 | t.deepEqual(Intl.RelativeTimeFormat.supportedLocalesOf(["foo", "bar", "en-US"]), ["en-US"]); 10 | }); 11 | -------------------------------------------------------------------------------- /src/relative-time-format/internal-slot/relative-time-format-static-internals.ts: -------------------------------------------------------------------------------- 1 | import {RelevantExtensionKey} from "../../relevant-extension-key/relevant-extension-key"; 2 | import {LocaleData} from "../../locale/locale-data"; 3 | import {Locales} from "../../locale/locales"; 4 | 5 | export interface RelativeTimeFormatStaticInternals { 6 | relevantExtensionKeys: RelevantExtensionKey[]; 7 | localeData: LocaleData; 8 | availableLocales: Locales; 9 | } 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | You are more than welcome to contribute to `intl-relative-time-format` in any way you please, including: 2 | 3 | - Updating documentation. 4 | - Fixing spelling and grammar 5 | - Adding tests 6 | - Fixing issues and suggesting new features 7 | - Blogging, tweeting, and creating tutorials about `intl-relative-time-format` 8 | - Reaching out to [@FredWessberg](https://twitter.com/FredWessberg) on Twitter 9 | - Submit an issue or a Pull Request 10 | -------------------------------------------------------------------------------- /src/relative-time-format/resolve-locale/resolve-locale-options.ts: -------------------------------------------------------------------------------- 1 | import {RelevantExtensionKey} from "../../relevant-extension-key/relevant-extension-key"; 2 | import {ExtendedSingularRelativeTimeUnit} from "../../unit/singular-relative-time-unit"; 3 | import {LocaleMatcher} from "../../locale-matcher/locale-matcher"; 4 | 5 | export interface ResolveLocaleOptions { 6 | key?: ExtendedSingularRelativeTimeUnit | RelevantExtensionKey; 7 | localeMatcher: LocaleMatcher; 8 | } 9 | -------------------------------------------------------------------------------- /src/relative-time-format/relative-time-format/resolved-relative-time-format-options.ts: -------------------------------------------------------------------------------- 1 | import {NumberingSystem} from "../../numbering-system/numbering-system"; 2 | import {Style} from "../../style/style"; 3 | import {Numeric} from "../../numeric/numeric"; 4 | import {Locale} from "../../locale/locale"; 5 | 6 | export interface ResolvedRelativeTimeFormatOptions { 7 | numberingSystem: NumberingSystem; 8 | locale: Locale; 9 | style: Style; 10 | numeric: Numeric; 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /compiled/ 3 | /locale-data/ 4 | /test262-polyfill.js* 5 | scripts/build-data/compiled/ 6 | /dist/ 7 | /typings/ 8 | package-lock.json 9 | /.idea/ 10 | /.cache/ 11 | /.vscode/ 12 | *.log 13 | /logs/ 14 | npm-debug.log* 15 | /lib-cov/ 16 | /coverage/ 17 | /.nyc_output/ 18 | /.grunt/ 19 | *.7z 20 | *.dmg 21 | *.gz 22 | *.iso 23 | *.jar 24 | *.rar 25 | *.tar 26 | *.zip 27 | .tgz 28 | .env 29 | .DS_Store 30 | .DS_Store? 31 | ._* 32 | .Spotlight-V100 33 | .Trashes 34 | ehthumbs.db 35 | Thumbs.db 36 | *.pem 37 | *.p12 38 | *.crt 39 | *.csr -------------------------------------------------------------------------------- /src/patch/patch.ts: -------------------------------------------------------------------------------- 1 | import {RelativeTimeFormat} from "../relative-time-format/relative-time-format/relative-time-format"; 2 | 3 | /** 4 | * Patches Intl with Intl.RelativeTimeFormat 5 | */ 6 | export function patch(): void { 7 | if (typeof Intl === "undefined") { 8 | throw new TypeError( 9 | `Could not define Intl.RelativeTimeFormat: Expected 'Intl' to exist. Remember to include polyfills for Intl.NumberFormat, Intl.getCanonicalLocales, and Intl.PluralRules before applying this polyfill` 10 | ); 11 | } 12 | Intl.RelativeTimeFormat = RelativeTimeFormat; 13 | } 14 | -------------------------------------------------------------------------------- /src/relative-time-format-part/relative-time-format-part.ts: -------------------------------------------------------------------------------- 1 | import {SingularRelativeTimeUnit} from "../unit/singular-relative-time-unit"; 2 | 3 | export interface RelativeTimeFormatNonLiteralPart extends Intl.NumberFormatPart { 4 | type: "currency" | "decimal" | "fraction" | "group" | "infinity" | "integer" | "minusSign" | "nan" | "plusSign" | "percentSign"; 5 | unit: SingularRelativeTimeUnit; 6 | } 7 | 8 | export interface RelativeTimeFormatLiteralPart extends Intl.NumberFormatPart { 9 | type: "literal"; 10 | } 11 | 12 | export type RelativeTimeFormatPart = RelativeTimeFormatNonLiteralPart | RelativeTimeFormatLiteralPart; 13 | -------------------------------------------------------------------------------- /src/util/is-property-key.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The abstract operation IsPropertyKey determines if argument, which must be an ECMAScript language value, is a value that may be used as a property key. 3 | * https://tc39.es/ecma262/#sec-ispropertykey 4 | * @param {*} argument 5 | * @returns {boolean} 6 | */ 7 | export function isPropertyKey(argument: unknown): argument is PropertyKey { 8 | // If Type(argument) is String, return true. 9 | if (typeof argument === "string") return true; 10 | // If Type(argument) is Symbol, return true. 11 | if (typeof argument === "symbol") return true; 12 | // Return false. 13 | return false; 14 | } 15 | -------------------------------------------------------------------------------- /src/locale/locale-data.ts: -------------------------------------------------------------------------------- 1 | import {Locale} from "./locale"; 2 | import {ExtendedSingularRelativeTimeUnit} from "../unit/singular-relative-time-unit"; 3 | import {RelevantExtensionKey} from "../relevant-extension-key/relevant-extension-key"; 4 | 5 | export type LocaleDataEntryValue = { 6 | future: Record; 7 | past: Record; 8 | } & Record; 9 | 10 | export type LocaleDataEntry = { 11 | [Key in ExtendedSingularRelativeTimeUnit | RelevantExtensionKey]: Key extends ExtendedSingularRelativeTimeUnit ? LocaleDataEntryValue | undefined : string[] | undefined 12 | }; 13 | 14 | export interface InputLocaleDataEntry { 15 | locale: Locale; 16 | data: LocaleDataEntry; 17 | } 18 | 19 | export type LocaleData = {[Key in Locale]?: LocaleDataEntry}; 20 | -------------------------------------------------------------------------------- /src/util/get.ts: -------------------------------------------------------------------------------- 1 | import {isPropertyKey} from "./is-property-key"; 2 | 3 | /** 4 | * The abstract operation Get is used to retrieve the value of a specific property of an object. The operation is called with arguments O and P where O is the object and P is the property key. 5 | * https://tc39.es/ecma262/#sec-get-o-p 6 | * @param {O} o 7 | * @param {P} p 8 | * @returns {O[P]} 9 | */ 10 | export function get(o: O, p: P): O[P] { 11 | // Assert: Type(O) is Object. 12 | if (typeof o !== "object") { 13 | throw new TypeError(`Given argument ${o} must be of type Object`); 14 | } 15 | 16 | // Assert: IsPropertyKey(P) is true. 17 | if (!isPropertyKey(p)) { 18 | throw new TypeError(`Given argument ${p} must be a PropertyKey`); 19 | } 20 | return o[p]; 21 | } 22 | -------------------------------------------------------------------------------- /src/intl-object/intl-object.ts: -------------------------------------------------------------------------------- 1 | import {Locale} from "../locale/locale"; 2 | 3 | export interface IntlObjectResolvedOptions { 4 | minimumIntegerDigits: number; 5 | minimumFractionDigits: number; 6 | maximumFractionDigits: number; 7 | minimumSignificantDigits?: number; 8 | maximumSignificantDigits?: number; 9 | } 10 | 11 | export interface IntlPluralRulesResolvedOptions extends IntlObjectResolvedOptions { 12 | locale: Locale; 13 | type: string; 14 | } 15 | 16 | export interface IntlObject { 17 | resolvedOptions(): IntlObjectResolvedOptions; 18 | } 19 | 20 | export interface IntlPluralRules extends IntlObject { 21 | select(n: number): string; 22 | resolvedOptions(): IntlPluralRulesResolvedOptions; 23 | } 24 | 25 | export interface IntlPluralRulesConstructor { 26 | new (locale: Locale): IntlPluralRules; 27 | } 28 | -------------------------------------------------------------------------------- /src/relative-time-format/internal-slot/relative-time-format-instance-internals.ts: -------------------------------------------------------------------------------- 1 | import {Locale} from "../../locale/locale"; 2 | import {NumberingSystem} from "../../numbering-system/numbering-system"; 3 | import {Style} from "../../style/style"; 4 | import {Numeric} from "../../numeric/numeric"; 5 | import {LocaleDataEntry} from "../../locale/locale-data"; 6 | import {RelativeTimeFormat} from "../relative-time-format/relative-time-format"; 7 | import {IntlPluralRules} from "../../intl-object/intl-object"; 8 | 9 | export interface RelativeTimeFormatInstanceInternals { 10 | locale: Locale; 11 | numberingSystem: NumberingSystem; 12 | style: Style; 13 | numeric: Numeric; 14 | fields: LocaleDataEntry; 15 | pluralRules: IntlPluralRules; 16 | numberFormat: Intl.NumberFormat; 17 | initializedRelativeTimeFormat: RelativeTimeFormat; 18 | } 19 | -------------------------------------------------------------------------------- /src/relative-time-format/supported-locales/best-fit-supported-locales.ts: -------------------------------------------------------------------------------- 1 | import {Locales} from "../../locale/locales"; 2 | import {lookupSupportedLocales} from "./lookup-supported-locales"; 3 | 4 | /** 5 | * The BestFitSupportedLocales abstract operation returns the subset of the provided BCP 47 language priority list 6 | * requestedLocales for which availableLocales has a matching locale when using the Best Fit Matcher algorithm. 7 | * Locales appear in the same order in the returned list as in requestedLocales. 8 | * 9 | * https://tc39.github.io/ecma402/#sec-bestfitsupportedlocales 10 | * @param {Locales} availableLocales 11 | * @param {Locales} requestedLocales 12 | * @return {Locales} 13 | */ 14 | export function bestFitSupportedLocales(availableLocales: Locales, requestedLocales: Locales): Locales { 15 | return lookupSupportedLocales(availableLocales, requestedLocales); 16 | } 17 | -------------------------------------------------------------------------------- /src/util/to-object.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:use-primitive-type no-construct no-any */ 2 | 3 | /** 4 | * The abstract operation ToObject converts argument to a value of type Object. 5 | * 6 | * https://tc39.github.io/ecma262/#sec-toobject 7 | * @param {T} argument 8 | * @return {T extends boolean ? Boolean : T extends number ? Number : T extends string ? String : T extends symbol ? symbol : T} 9 | */ 10 | export function toObject(argument: T): T extends boolean ? Boolean : T extends number ? Number : T extends string ? String : T extends symbol ? Symbol : T { 11 | if (argument == null) { 12 | throw new TypeError(`Argument ${argument} cannot be converted to an Object`); 13 | } 14 | 15 | if (typeof argument === "boolean") { 16 | return new Boolean(argument) as any; 17 | } 18 | 19 | if (typeof argument === "number") { 20 | return new Number(argument) as any; 21 | } 22 | 23 | if (typeof argument === "string") { 24 | return new String(argument) as any; 25 | } 26 | 27 | if (typeof argument === "symbol") { 28 | return new Object(argument) as any; 29 | } 30 | 31 | return argument as any; 32 | } 33 | -------------------------------------------------------------------------------- /src/relative-time-format/format-relative-time-to-parts/format-relative-time-to-parts.ts: -------------------------------------------------------------------------------- 1 | import {RelativeTimeFormat} from "../relative-time-format/relative-time-format"; 2 | import {RelativeTimeUnit} from "../../unit/relative-time-unit"; 3 | import {partitionRelativeTimePattern} from "../partition-relative-time-pattern/partition-relative-time-pattern"; 4 | import {RelativeTimeFormatPart} from "../../relative-time-format-part/relative-time-format-part"; 5 | 6 | /** 7 | * The FormatRelativeTimeToParts abstract operation is called with arguments relativeTimeFormat 8 | * (which must be an object initialized as a RelativeTimeFormat), value (which must be a Number value), 9 | * and unit (which must be a String denoting the value unit) 10 | * 11 | * http://tc39.github.io/proposal-intl-relative-time/#sec-FormatRelativeTimeToParts 12 | * @param {RelativeTimeFormat} relativeTimeFormat 13 | * @param {number} value 14 | * @param {RelativeTimeUnit} unit 15 | * @return {RelativeTimeFormatPart[]} 16 | */ 17 | export function formatRelativeTimeToParts(relativeTimeFormat: RelativeTimeFormat, value: number, unit: RelativeTimeUnit): RelativeTimeFormatPart[] { 18 | return partitionRelativeTimePattern(relativeTimeFormat, value, unit); 19 | } 20 | -------------------------------------------------------------------------------- /test/resolved-options.test.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import test from "ava"; 3 | import "../src/test262"; 4 | import "../locale-data/en"; 5 | import "../locale-data/en-GB"; 6 | 7 | // tslint:disable 8 | 9 | test("Can properly resolve options based on the given options. #1", t => { 10 | const rtf = new Intl.RelativeTimeFormat(); 11 | const result = rtf.resolvedOptions(); 12 | t.deepEqual(result, { 13 | locale: "en", 14 | style: "long", // Default value, 15 | numeric: "always", // Default value 16 | numberingSystem: "latn" // Default numbering system for 'en' 17 | }); 18 | }); 19 | 20 | test("Can properly resolve options based on the given options. #2", t => { 21 | const rtf = new Intl.RelativeTimeFormat("en", { 22 | numeric: "auto", 23 | style: "narrow" 24 | }); 25 | const result = rtf.resolvedOptions(); 26 | t.deepEqual(result, { 27 | locale: "en", 28 | style: "narrow", 29 | numeric: "auto", 30 | numberingSystem: "latn" // Default numbering system for 'en' 31 | }); 32 | }); 33 | 34 | test.only("Can properly resolve options based on the given options. #3", t => { 35 | const rtf = new Intl.RelativeTimeFormat("en-GB-oed"); 36 | const result = rtf.resolvedOptions(); 37 | 38 | t.deepEqual(result.locale, "en-GB"); 39 | }); 40 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2019 [Frederik Wessberg](mailto:frederikwessberg@hotmail.com) ([@FredWessberg](https://twitter.com/FredWessberg)) ([Website](https://github.com/wessberg)) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /src/relative-time-format/default-locale/get-default-locale.ts: -------------------------------------------------------------------------------- 1 | import {Locale} from "../../locale/locale"; 2 | 3 | /** 4 | * Must represent the structurally valid (6.2.2) and canonicalized (6.2.3) BCP 47 language tag for the host environment's current locale. 5 | * 6 | * https://tc39.github.io/ecma402/#sec-defaultlocale 7 | * @type {Locale?} 8 | */ 9 | let _defaultLocale: Locale | undefined; 10 | 11 | /** 12 | * Sets the default locale 13 | * @param {Locale} locale 14 | */ 15 | export function setDefaultLocale(locale: Locale): void { 16 | _defaultLocale = locale; 17 | } 18 | 19 | /** 20 | * The DefaultLocale abstract operation returns a String value representing the structurally valid (6.2.2) and canonicalized (6.2.3) BCP 47 language tag for the host environment's current locale. 21 | * https://tc39.github.io/ecma402/#sec-defaultlocale 22 | * @return {Locale | undefined} 23 | */ 24 | export function getDefaultLocale(): Locale | undefined { 25 | return _defaultLocale; 26 | } 27 | 28 | /** 29 | * Retrieves the default locale if it is set, and throws otherwise 30 | * @return {Locale} 31 | */ 32 | export function ensureDefaultLocale(): Locale { 33 | if (_defaultLocale == null) { 34 | throw new ReferenceError(`Could not determine locale: No default locale has been configured`); 35 | } 36 | return _defaultLocale; 37 | } 38 | -------------------------------------------------------------------------------- /src/relative-time-format/format-relative-time/format-relative-time.ts: -------------------------------------------------------------------------------- 1 | import {RelativeTimeFormat} from "../relative-time-format/relative-time-format"; 2 | import {RelativeTimeUnit} from "../../unit/relative-time-unit"; 3 | import {partitionRelativeTimePattern} from "../partition-relative-time-pattern/partition-relative-time-pattern"; 4 | 5 | /** 6 | * The FormatRelativeTime abstract operation is called with arguments relativeTimeFormat 7 | * (which must be an object initialized as a RelativeTimeFormat), value (which must be a Number value), 8 | * and unit (which must be a String denoting the value unit) and performs the following steps: 9 | * 10 | * http://tc39.github.io/proposal-intl-relative-time/#sec-FormatRelativeTime 11 | * @param {RelativeTimeFormat} relativeTimeFormat 12 | * @param {number} value 13 | * @param {RelativeTimeUnit} unit 14 | * @return {string} 15 | */ 16 | export function formatRelativeTime(relativeTimeFormat: RelativeTimeFormat, value: number, unit: RelativeTimeUnit): string { 17 | // Let parts be ? PartitionRelativeTimePattern(relativeTimeFormat, value, unit). 18 | const parts = partitionRelativeTimePattern(relativeTimeFormat, value, unit); 19 | 20 | // Let result be an empty String. 21 | let result = ""; 22 | 23 | // For each part in parts, do 24 | for (const part of parts) { 25 | // Set result to the string-concatenation of result and part.[[Value]]. 26 | result += part.value; 27 | } 28 | 29 | // Return result. 30 | return result; 31 | } 32 | -------------------------------------------------------------------------------- /test262-runner.js: -------------------------------------------------------------------------------- 1 | const {spawnSync} = require("child_process"); 2 | const {resolve} = require("path"); 3 | const {cpus} = require("os"); 4 | const pkg = require("./package.json"); 5 | 6 | const args = [ 7 | "--hostArgs", 8 | `"--icu-data-dir=node_modules/full-icu"`, 9 | "--reporter-keys", 10 | "file,attrs,result", 11 | "-t", 12 | String(cpus().length), 13 | "--prelude", 14 | pkg.test262, 15 | "-r", 16 | "json", 17 | "test262/test/intl402/RelativeTimeFormat/**/*.*" 18 | ]; 19 | console.log(`Running "test262-harness ${args.join(" ")}"`); 20 | const result = spawnSync("test262-harness", args, { 21 | env: process.env, 22 | encoding: "utf-8" 23 | }); 24 | if (result.status || result.stderr || result.error) { 25 | console.error(result.stderr); 26 | console.error(result.error); 27 | process.exit(result.status || 1); 28 | } 29 | 30 | const json = JSON.parse(result.stdout); 31 | const failedTests = json.filter(r => !r.result.pass); 32 | json.forEach(t => { 33 | if (t.result.pass) { 34 | console.log(`✓ ${t.attrs.description}`); 35 | } else { 36 | console.log("\n\n"); 37 | console.log(`🗴 ${t.attrs.description}`); 38 | console.log(`\t ${t.result.message}`); 39 | console.log("\t", resolve(__dirname, "..", t.file)); 40 | console.log("\n\n"); 41 | } 42 | }); 43 | if (failedTests.length) { 44 | console.log(`Tests: ${failedTests.length} failed, ${json.length - failedTests.length} passed, ${json.length} total`); 45 | process.exit(1); 46 | } 47 | console.log(`Tests: ${json.length - failedTests.length} passed, ${json.length} total`); 48 | -------------------------------------------------------------------------------- /src/relative-time-format/matcher/best-fit-matcher/best-fit-matcher.ts: -------------------------------------------------------------------------------- 1 | import {MatcherOptions} from "../matcher-options"; 2 | import {MatcherResult} from "../matcher-result"; 3 | import {lookupMatcher} from "../lookup-matcher/lookup-matcher"; 4 | 5 | /** 6 | * The BestFitMatcher abstract operation compares requestedLocales, 7 | * which must be a List as returned by CanonicalizeLocaleList, 8 | * against the locales in availableLocales and determines the best available language to meet the request. 9 | * The algorithm is implementation dependent, but should produce results that a typical user of the requested 10 | * locales would perceive as at least as good as those produced by the LookupMatcher abstract operation. 11 | * RelativeTimeFormatOptions specified through Unicode locale extension sequences must be ignored by the algorithm. 12 | * Information about such subsequences is returned separately. The abstract operation returns a record 13 | * with a [[locale]] field, whose value is the language tag of the selected locale, 14 | * which must be an element of availableLocales. 15 | * If the language tag of the request locale that led to the selected locale contained a Unicode locale extension sequence, 16 | * then the returned record also contains an [[extension]] field whose value is the first Unicode locale extension sequence 17 | * within the request locale language tag. 18 | * 19 | * https://tc39.github.io/ecma402/#sec-bestfitmatcher 20 | * @param {MatcherOptions} options 21 | * @return {MatcherResult} 22 | */ 23 | export function bestFitMatcher(options: MatcherOptions): MatcherResult { 24 | return lookupMatcher(options); 25 | } 26 | -------------------------------------------------------------------------------- /src/relative-time-format/resolve-plural/resolve-plural.ts: -------------------------------------------------------------------------------- 1 | import {RelativeTimeFormat} from "../relative-time-format/relative-time-format"; 2 | import {getInternalSlot, hasInternalSlot} from "../internal-slot/internal-slot"; 3 | 4 | /** 5 | * When the ResolvePlural abstract operation is called with arguments pluralRules (which must be an object initialized as a PluralRules) and n (which must be a Number value), it returns a String value representing the plural form of n according to the effective locale and the options of pluralRules. 6 | * 7 | * https://tc39.github.io/ecma402/#sec-resolveplural 8 | * @param {RelativeTimeFormat} relativeTimeFormat - needed to get internal slots 9 | * @param {number} n 10 | */ 11 | export function resolvePlural(relativeTimeFormat: RelativeTimeFormat, n: number): string { 12 | // Assert: Type(pluralRules) is Object. 13 | // Assert: pluralRules has an [[InitializedPluralRules]] internal slot. 14 | if (!hasInternalSlot(relativeTimeFormat, "pluralRules")) { 15 | throw new TypeError(`Given instance of of Intl.RelativeTimeFormat must have an [[InitializedPluralRules]] internal slot`); 16 | } 17 | 18 | // Assert: Type(n) is Number. 19 | if (typeof n !== "number") { 20 | throw new TypeError(`Argument 'n' must be a number`); 21 | } 22 | 23 | // If n is not a finite Number, then 24 | if (!isFinite(n)) { 25 | // Return "other". 26 | return "other"; 27 | } 28 | 29 | // Let locale be pluralRules.[[Locale]]. 30 | // Let type be pluralRules.[[Type]]. 31 | const pluralRules = getInternalSlot(relativeTimeFormat, "pluralRules"); 32 | 33 | // Return ? PluralRuleSelect(locale, type, n, operands). 34 | return pluralRules.select(n); 35 | } 36 | -------------------------------------------------------------------------------- /src/relative-time-format/supported-locales/lookup-supported-locales.ts: -------------------------------------------------------------------------------- 1 | import {Locales} from "../../locale/locales"; 2 | import {removeUnicodeExtensionSequences} from "../unicode-extension/unicode-extension"; 3 | import {bestAvailableLocale} from "../matcher/best-available-locale/best-available-locale"; 4 | 5 | /** 6 | * The LookupSupportedLocales abstract operation returns the subset of the provided BCP 47 language priority list 7 | * requestedLocales for which availableLocales has a matching locale when using the BCP 47 Lookup algorithm. 8 | * Locales appear in the same order in the returned list as in requestedLocales. 9 | * 10 | * https://tc39.github.io/ecma402/#sec-bestfitsupportedlocales 11 | * @param {Locales} availableLocales 12 | * @param {Locales} requestedLocales 13 | * @return {Locales} 14 | */ 15 | export function lookupSupportedLocales(availableLocales: Locales, requestedLocales: Locales): Locales { 16 | // Let subset be a new empty List. 17 | const subset: Locales = []; 18 | // For each element locale of requestedLocales in List order, do 19 | for (const locale of requestedLocales) { 20 | // Let noExtensionsLocale be the String value that is locale with all Unicode locale extension sequences removed. 21 | const noExtensionsLocale = removeUnicodeExtensionSequences(locale); 22 | 23 | // Let availableLocale be BestAvailableLocale(availableLocales, noExtensionsLocale). 24 | const availableLocale = bestAvailableLocale(availableLocales, noExtensionsLocale); 25 | 26 | // If availableLocale is not undefined, append locale to the end of subset. 27 | if (availableLocale !== undefined) { 28 | subset.push(locale); 29 | } 30 | } 31 | return subset; 32 | } 33 | -------------------------------------------------------------------------------- /src/relative-time-format/matcher/best-available-locale/best-available-locale.ts: -------------------------------------------------------------------------------- 1 | import {Locales} from "../../../locale/locales"; 2 | import {Locale} from "../../../locale/locale"; 3 | 4 | /** 5 | * The BestAvailableLocale abstract operation compares the provided argument locale, 6 | * which must be a String value with a structurally valid and canonicalized BCP 47 language tag, 7 | * against the locales in availableLocales and returns either the longest non-empty prefix of locale 8 | * that is an element of availableLocales, or undefined if there is no such element. It uses the fallback 9 | * mechanism of RFC 4647, section 3.4. 10 | * 11 | * https://tc39.github.io/ecma402/#sec-bestavailablelocale 12 | * @param {Locales} availableLocales 13 | * @param {Locale} locale 14 | * @return {string} 15 | */ 16 | export function bestAvailableLocale(availableLocales: Locales, locale: Locale): string | undefined { 17 | // Let candidate be locale. 18 | let candidate = locale; 19 | // Repeat 20 | while (true) { 21 | // If availableLocales contains an element equal to candidate, return candidate. 22 | if (availableLocales.includes(candidate)) { 23 | return candidate; 24 | } 25 | 26 | // Let pos be the character index of the last occurrence of "-" (U+002D) within candidate. 27 | let pos = candidate.lastIndexOf("-"); 28 | // If that character does not occur, return undefined. 29 | if (pos === -1) return undefined; 30 | 31 | // If pos ≥ 2 and the character "-" occurs at index pos-2 of candidate, decrease pos by 2. 32 | if (pos >= 2 && candidate.charAt(pos - 2) === "-") { 33 | pos -= 2; 34 | } 35 | 36 | // Let candidate be the substring of candidate from position 0, inclusive, to position pos, exclusive. 37 | candidate = candidate.slice(0, pos); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.0.7](https://github.com/wessberg/intl-relative-time-format/compare/v1.0.6...v1.0.7) (2019-07-04) 2 | 3 | ## [1.0.6](https://github.com/wessberg/intl-relative-time-format/compare/v1.0.5...v1.0.6) (2019-02-09) 4 | 5 | ## [1.0.5](https://github.com/wessberg/intl-relative-time-format/compare/v1.0.4...v1.0.5) (2019-02-07) 6 | 7 | ### Bug Fixes 8 | 9 | - **chore:** Removes a few [@ts-ignore](https://github.com/ts-ignore) comments ([ed1dd85](https://github.com/wessberg/intl-relative-time-format/commit/ed1dd85)) 10 | - **chore:** Removes a few [@ts-ignore](https://github.com/ts-ignore) comments ([673e964](https://github.com/wessberg/intl-relative-time-format/commit/673e964)) 11 | 12 | ### Features 13 | 14 | - **chore:** better error messages ([20bcd1e](https://github.com/wessberg/intl-relative-time-format/commit/20bcd1e)) 15 | - **chore:** better error messages ([9284d56](https://github.com/wessberg/intl-relative-time-format/commit/9284d56)) 16 | 17 | ## [1.0.4](https://github.com/wessberg/intl-relative-time-format/compare/v1.0.3...v1.0.4) (2019-01-14) 18 | 19 | ### Features 20 | 21 | - **chore:** removes own implementation of CanonicalizeLanguageTag and BCP47-related utility functions in favor of Intl.getCanonicalLocales ([4b8d537](https://github.com/wessberg/intl-relative-time-format/commit/4b8d537)) 22 | 23 | ## [1.0.3](https://github.com/wessberg/intl-relative-time-format/compare/v1.0.2...v1.0.3) (2019-01-14) 24 | 25 | ### Features 26 | 27 | - **chore:** removes own implementation of CanonicalizeLocaleList in favor of using Intl.getCanonicalLocales directly ([d368826](https://github.com/wessberg/intl-relative-time-format/commit/d368826)) 28 | 29 | ## [1.0.2](https://github.com/wessberg/intl-relative-time-format/compare/v1.0.1...v1.0.2) (2019-01-14) 30 | 31 | ## 1.0.1 (2019-01-14) 32 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace Intl { 2 | function getCanonicalLocales(locales: string | string[] | undefined): string[]; 3 | 4 | type Locale = string; 5 | type Locales = Locale[]; 6 | 7 | type LocaleMatcher = "lookup" | "best fit"; 8 | type Style = "long" | "short" | "narrow"; 9 | type Numeric = "always" | "auto"; 10 | type NumberingSystem = string; 11 | 12 | type SingularRelativeTimeUnit = "second" | "minute" | "hour" | "day" | "week" | "month" | "quarter" | "year"; 13 | 14 | interface ResolvedRelativeTimeFormatOptions { 15 | numberingSystem: NumberingSystem; 16 | locale: Locale; 17 | style: Style; 18 | numeric: Numeric; 19 | } 20 | 21 | interface SupportedLocalesOptions { 22 | localeMatcher: LocaleMatcher; 23 | } 24 | 25 | interface RelativeTimeFormatNonLiteralPart extends Intl.NumberFormatPart { 26 | type: "currency" | "decimal" | "fraction" | "group" | "infinity" | "integer" | "minusSign" | "nan" | "plusSign" | "percentSign"; 27 | unit: SingularRelativeTimeUnit; 28 | } 29 | 30 | interface RelativeTimeFormatLiteralPart extends Intl.NumberFormatPart { 31 | type: "literal"; 32 | } 33 | 34 | type RelativeTimeFormatPart = RelativeTimeFormatNonLiteralPart | RelativeTimeFormatLiteralPart; 35 | 36 | type RelativeTimeUnit = SingularRelativeTimeUnit | "seconds" | "minutes" | "hours" | "days" | "weeks" | "months" | "quarters" | "years"; 37 | 38 | interface RelativeTimeFormatOptions { 39 | localeMatcher: LocaleMatcher; 40 | style: Style; 41 | numeric: Numeric; 42 | } 43 | 44 | class RelativeTimeFormat { 45 | constructor(locales?: Locale | Locales | undefined, options?: Partial); 46 | 47 | public static supportedLocalesOf(locales: Locale | Locales, options?: SupportedLocalesOptions | undefined): Locales; 48 | 49 | public format(value: number, unit: RelativeTimeUnit): string; 50 | 51 | public formatToParts(value: number, unit: RelativeTimeUnit): RelativeTimeFormatPart[]; 52 | 53 | public resolvedOptions(): ResolvedRelativeTimeFormatOptions; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/util/get-option.ts: -------------------------------------------------------------------------------- 1 | import {get} from "./get"; 2 | import {toBoolean} from "./to-boolean"; 3 | import {toString} from "./to-string"; 4 | import {ElementOf} from "./element-of"; 5 | 6 | /** 7 | * https://tc39.es/ecma402/#sec-getoption 8 | * @param {Options} options 9 | * @param {Property} property 10 | * @param {Type} type 11 | * @param {Values} values 12 | * @param {Fallback} fallback 13 | * @returns {Return} 14 | */ 15 | export function getOption< 16 | Options extends object, 17 | Property extends keyof Options, 18 | Type extends Options[Property] extends (string | (string | undefined)) ? "string" : "boolean", 19 | Values extends Options[Property] extends (string | (string | undefined)) ? readonly string[] : readonly boolean[], 20 | Fallback extends ElementOf, 21 | Return extends ElementOf 22 | >(options: Options, property: Property, type: Type, values: Values, fallback: Fallback): Return { 23 | // Let value be ? Get(options, property). 24 | let value = get(options, property); 25 | // If value is not undefined, then 26 | if (value !== undefined) { 27 | // Assert: type is "boolean" or "string". 28 | if (type !== "boolean" && type !== "string") { 29 | throw new TypeError(`Expected type ${type} to be 'boolean' or 'string`); 30 | } 31 | 32 | // If type is "boolean", then 33 | if (type === "boolean") { 34 | // Let value be ToBoolean(value). 35 | value = (toBoolean(value) as unknown) as Options[Property]; 36 | } 37 | 38 | // If type is "string", then 39 | if (type === "string") { 40 | // Let value be ? ToString(value). 41 | value = (toString(value) as unknown) as Options[Property]; 42 | } 43 | 44 | // If values is not undefined, then 45 | if (values !== undefined) { 46 | // If values does not contain an element equal to value, throw a RangeError exception. 47 | // tslint:disable-next-line:no-collapsible-if 48 | if (!values.includes(value as never)) { 49 | throw new RangeError(`Value ${value} out of range for options property ${property}`); 50 | } 51 | } 52 | 53 | // Return value. 54 | return (value as unknown) as Return; 55 | } 56 | 57 | // Else, return fallback. 58 | else { 59 | return (fallback as unknown) as Return; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/relative-time-format/supported-locales/supported-locales.ts: -------------------------------------------------------------------------------- 1 | import {Locales} from "../../locale/locales"; 2 | import {toObject} from "../../util/to-object"; 3 | import {bestFitSupportedLocales} from "./best-fit-supported-locales"; 4 | import {lookupSupportedLocales} from "./lookup-supported-locales"; 5 | import {SupportedLocalesOptions} from "./supported-locales-options"; 6 | import {LOCALE_MATCHER, LocaleMatcher} from "../../locale-matcher/locale-matcher"; 7 | import {getOption} from "../../util/get-option"; 8 | 9 | /** 10 | * The SupportedLocales abstract operation returns the subset of the provided BCP 47 language priority list 11 | * requestedLocales for which availableLocales has a matching locale. Two algorithms are available to match 12 | * the locales: the Lookup algorithm described in RFC 4647 section 3.4, and an implementation dependent 13 | * best-fit algorithm. Locales appear in the same order in the returned list as in requestedLocales. 14 | * 15 | * https://tc39.github.io/ecma402/#sec-supportedlocales 16 | * @param {Locales} availableLocales 17 | * @param {Locales} requestedLocales 18 | * @param {SupportedLocalesOptions} [options] 19 | * @return {Locales} 20 | */ 21 | export function supportedLocales(availableLocales: Locales, requestedLocales: Locales, options?: SupportedLocalesOptions): Locales { 22 | let matcher: LocaleMatcher; 23 | 24 | // If options is not undefined, then 25 | if (options !== undefined) { 26 | // Let options be ? ToObject(options). 27 | options = toObject(options); 28 | 29 | // Let matcher be ? GetOption(options, "localeMatcher", "string", « "lookup", "best fit" », "best fit"). 30 | matcher = getOption(options, "localeMatcher", "string", LOCALE_MATCHER, "best fit"); 31 | } 32 | 33 | // Else, let matcher be "best fit". 34 | else { 35 | matcher = "best fit"; 36 | } 37 | 38 | // If matcher is "best fit", then let supportedLocales be BestFitSupportedLocales(availableLocales, requestedLocales). 39 | // Else let supportedLocales be LookupSupportedLocales(availableLocales, requestedLocales). 40 | // Return CreateArrayFromList(supportedLocales). 41 | return matcher === "best fit" 42 | ? bestFitSupportedLocales(availableLocales, requestedLocales) 43 | : lookupSupportedLocales(availableLocales, requestedLocales); 44 | } 45 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import {browsersWithSupportForEcmaVersion} from "@wessberg/browserslist-generator"; 2 | import ts from "@wessberg/rollup-plugin-ts"; 3 | import resolve from "rollup-plugin-node-resolve"; 4 | import multiEntry from "rollup-plugin-multi-entry"; 5 | import packageJson from "./package.json"; 6 | 7 | const SHARED_OPTIONS = { 8 | context: "window", 9 | treeshake: true 10 | }; 11 | 12 | export default [ 13 | { 14 | ...SHARED_OPTIONS, 15 | input: "src/index.ts", 16 | output: [ 17 | { 18 | file: packageJson.module, 19 | format: "esm", 20 | sourcemap: true 21 | } 22 | ], 23 | plugins: [ 24 | ts({ 25 | transpiler: "babel", 26 | browserslist: browsersWithSupportForEcmaVersion("es2020") 27 | }), 28 | resolve() 29 | ] 30 | }, 31 | { 32 | ...SHARED_OPTIONS, 33 | input: "src/index.ts", 34 | output: [ 35 | { 36 | file: packageJson.main, 37 | format: "iife", 38 | sourcemap: true 39 | } 40 | ], 41 | plugins: [ 42 | ts({ 43 | transpiler: "babel", 44 | browserslist: browsersWithSupportForEcmaVersion("es5") 45 | }), 46 | resolve() 47 | ] 48 | }, 49 | { 50 | ...SHARED_OPTIONS, 51 | input: "src/index.ts", 52 | output: [ 53 | { 54 | file: packageJson.minified, 55 | format: "iife", 56 | sourcemap: true 57 | } 58 | ], 59 | plugins: [ 60 | ts({ 61 | transpiler: "babel", 62 | browserslist: browsersWithSupportForEcmaVersion("es5"), 63 | babelConfig: { 64 | comments: false, 65 | minified: true, 66 | compact: true, 67 | presets: [["minify", {builtIns: false}]] 68 | } 69 | }), 70 | resolve() 71 | ] 72 | }, 73 | { 74 | ...SHARED_OPTIONS, 75 | input: [ 76 | "src/test262.ts", 77 | "locale-data/en.js", 78 | "locale-data/en-GB.js", 79 | "locale-data/en-US.js", 80 | "locale-data/pl.js", 81 | "locale-data/pl-PL.js", 82 | "locale-data/de.js" 83 | ], 84 | output: [ 85 | { 86 | file: packageJson.test262, 87 | format: "iife", 88 | sourcemap: true 89 | } 90 | ], 91 | plugins: [ 92 | multiEntry(), 93 | ts({ 94 | tsconfig: resolvedOptions => ({...resolvedOptions, declaration: false}) 95 | }), 96 | resolve() 97 | ] 98 | } 99 | ]; 100 | -------------------------------------------------------------------------------- /src/relative-time-format/matcher/lookup-matcher/lookup-matcher.ts: -------------------------------------------------------------------------------- 1 | import {MatcherOptions} from "../matcher-options"; 2 | import {removeUnicodeExtensionSequences, UNICODE_EXTENSION_SEQUENCE_REGEXP} from "../../unicode-extension/unicode-extension"; 3 | import {bestAvailableLocale} from "../best-available-locale/best-available-locale"; 4 | import {MatcherResult} from "../matcher-result"; 5 | import {ensureDefaultLocale} from "../../default-locale/get-default-locale"; 6 | 7 | /** 8 | * The LookupMatcher abstract operation compares requestedLocales, which must be a List as returned by CanonicalizeLocaleList, 9 | * against the locales in availableLocales and determines the best available language to meet the request. 10 | * 11 | * https://tc39.github.io/ecma402/#sec-lookupmatcher 12 | * @param {MatcherOptions} options 13 | * @return {MatcherResult} 14 | */ 15 | export function lookupMatcher({availableLocales, requestedLocales}: MatcherOptions): MatcherResult { 16 | // Let result be a new Record. 17 | const result = {} as MatcherResult; 18 | // For each element locale of requestedLocales in List order, do 19 | for (const locale of requestedLocales) { 20 | // Let noExtensionsLocale be the String value that is locale with all Unicode locale extension sequences removed. 21 | const noExtensionsLocale = removeUnicodeExtensionSequences(locale); 22 | 23 | // Let availableLocale be BestAvailableLocale(availableLocales, noExtensionsLocale). 24 | const availableLocale = bestAvailableLocale(availableLocales, noExtensionsLocale); 25 | 26 | // If availableLocale is not undefined, then 27 | if (availableLocale !== undefined) { 28 | // Set result.[[locale]] to availableLocale. 29 | result.locale = availableLocale; 30 | 31 | // If locale and noExtensionsLocale are not the same String value, then 32 | if (locale !== noExtensionsLocale) { 33 | // Let extension be the String value consisting of the first substring of local 34 | // that is a Unicode locale extension sequence. 35 | const extensionMatch = locale.match(UNICODE_EXTENSION_SEQUENCE_REGEXP); 36 | // Set result.[[extension]] to extension. 37 | result.extension = extensionMatch == null ? "" : extensionMatch[0]; 38 | } 39 | return result; 40 | } 41 | } 42 | // Let defLocale be DefaultLocale(). 43 | const defLocale = ensureDefaultLocale(); 44 | 45 | // Set result.[[locale]] to defLocale. 46 | result.locale = defLocale; 47 | 48 | // Return result. 49 | return result; 50 | } 51 | -------------------------------------------------------------------------------- /src/relative-time-format/numbering-systems/numbering-systems.ts: -------------------------------------------------------------------------------- 1 | export const NUMBERING_SYSTEMS: Record = { 2 | arab: ["\u0660", "\u0661", "\u0662", "\u0663", "\u0664", "\u0665", "\u0666", "\u0667", "\u0668", "\u0669"], 3 | arabext: ["\u06F0", "\u06F1", "\u06F2", "\u06F3", "\u06F4", "\u06F5", "\u06F6", "\u06F7", "\u06F8", "\u06F9"], 4 | bali: ["\u1B50", "\u1B51", "\u1B52", "\u1B53", "\u1B54", "\u1B55", "\u1B56", "\u1B57", "\u1B58", "\u1B59"], 5 | beng: ["\u09E6", "\u09E7", "\u09E8", "\u09E9", "\u09EA", "\u09EB", "\u09EC", "\u09ED", "\u09EE", "\u09EF"], 6 | deva: ["\u0966", "\u0967", "\u0968", "\u0969", "\u096A", "\u096B", "\u096C", "\u096D", "\u096E", "\u096F"], 7 | fullwide: ["\uFF10", "\uFF11", "\uFF12", "\uFF13", "\uFF14", "\uFF15", "\uFF16", "\uFF17", "\uFF18", "\uFF19"], 8 | gujr: ["\u0AE6", "\u0AE7", "\u0AE8", "\u0AE9", "\u0AEA", "\u0AEB", "\u0AEC", "\u0AED", "\u0AEE", "\u0AEF"], 9 | guru: ["\u0A66", "\u0A67", "\u0A68", "\u0A69", "\u0A6A", "\u0A6B", "\u0A6C", "\u0A6D", "\u0A6E", "\u0A6F"], 10 | hanidec: ["\u3007", "\u4E00", "\u4E8C", "\u4E09", "\u56DB", "\u4E94", "\u516D", "\u4E03", "\u516B", "\u4E5D"], 11 | khmr: ["\u17E0", "\u17E1", "\u17E2", "\u17E3", "\u17E4", "\u17E5", "\u17E6", "\u17E7", "\u17E8", "\u17E9"], 12 | knda: ["\u0CE6", "\u0CE7", "\u0CE8", "\u0CE9", "\u0CEA", "\u0CEB", "\u0CEC", "\u0CED", "\u0CEE", "\u0CEF"], 13 | laoo: ["\u0ED0", "\u0ED1", "\u0ED2", "\u0ED3", "\u0ED4", "\u0ED5", "\u0ED6", "\u0ED7", "\u0ED8", "\u0ED9"], 14 | latn: ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"], 15 | limb: ["\u1946", "\u1947", "\u1948", "\u1949", "\u194A", "\u194B", "\u194C", "\u194D", "\u194E", "\u194F"], 16 | mlym: ["\u0D66", "\u0D67", "\u0D68", "\u0D69", "\u0D6A", "\u0D6B", "\u0D6C", "\u0D6D", "\u0D6E", "\u0D6F"], 17 | mong: ["\u1810", "\u1811", "\u1812", "\u1813", "\u1814", "\u1815", "\u1816", "\u1817", "\u1818", "\u1819"], 18 | mymr: ["\u1040", "\u1041", "\u1042", "\u1043", "\u1044", "\u1045", "\u1046", "\u1047", "\u1048", "\u1049"], 19 | orya: ["\u0B66", "\u0B67", "\u0B68", "\u0B69", "\u0B6A", "\u0B6B", "\u0B6C", "\u0B6D", "\u0B6E", "\u0B6F"], 20 | tamldec: ["\u0BE6", "\u0BE7", "\u0BE8", "\u0BE9", "\u0BEA", "\u0BEB", "\u0BEC", "\u0BED", "\u0BEE", "\u0BEF"], 21 | telu: ["\u0C66", "\u0C67", "\u0C68", "\u0C69", "\u0C6A", "\u0C6B", "\u0C6C", "\u0C6D", "\u0C6E", "\u0C6F"], 22 | thai: ["\u0E50", "\u0E51", "\u0E52", "\u0E53", "\u0E54", "\u0E55", "\u0E56", "\u0E57", "\u0E58", "\u0E59"], 23 | tibt: ["\u0F20", "\u0F21", "\u0F22", "\u0F23", "\u0F24", "\u0F25", "\u0F26", "\u0F27", "\u0F28", "\u0F29"] 24 | }; 25 | -------------------------------------------------------------------------------- /scripts/build-data/ts/create-program-from-sources.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CompilerOptions, 3 | createProgram, 4 | createSourceFile, 5 | getDefaultCompilerOptions, 6 | getDefaultLibFileName, 7 | Program, 8 | ScriptKind, 9 | ScriptTarget, 10 | SourceFile, 11 | sys 12 | } from "typescript"; 13 | 14 | export interface SourceFileInput { 15 | fileName: string; 16 | text: string; 17 | } 18 | 19 | /** 20 | * Generates a Program based on the given sources 21 | * @returns {Program} 22 | */ 23 | export function createProgramFromSources(sources: SourceFileInput[]): Program { 24 | return createProgram({ 25 | rootNames: sources.map(source => source.fileName), 26 | host: { 27 | readFile(fileName: string): string | undefined { 28 | const matchedFile = sources.find(file => file.fileName === fileName); 29 | return matchedFile == null ? undefined : matchedFile.text; 30 | }, 31 | 32 | fileExists(fileName: string): boolean { 33 | return this.readFile(fileName) != null; 34 | }, 35 | 36 | getSourceFile( 37 | fileName: string, 38 | languageVersion: ScriptTarget 39 | ): SourceFile | undefined { 40 | const sourceText = this.readFile(fileName); 41 | if (sourceText == null) return undefined; 42 | 43 | return createSourceFile( 44 | fileName, 45 | sourceText, 46 | languageVersion, 47 | true, 48 | ScriptKind.TS 49 | ); 50 | }, 51 | 52 | getCurrentDirectory() { 53 | return "."; 54 | }, 55 | 56 | getDirectories(directoryName: string) { 57 | return sys.getDirectories(directoryName); 58 | }, 59 | 60 | getDefaultLibFileName(options: CompilerOptions): string { 61 | return getDefaultLibFileName(options); 62 | }, 63 | 64 | getCanonicalFileName(fileName: string): string { 65 | return this.useCaseSensitiveFileNames() 66 | ? fileName 67 | : fileName.toLowerCase(); 68 | }, 69 | 70 | getNewLine(): string { 71 | return sys.newLine; 72 | }, 73 | 74 | useCaseSensitiveFileNames() { 75 | return sys.useCaseSensitiveFileNames; 76 | }, 77 | 78 | writeFile( 79 | fileName: string, 80 | data: string, 81 | writeByteOrderMark: boolean = false 82 | ) { 83 | console.log("write file:", fileName); 84 | sys.writeFile(fileName, data, writeByteOrderMark); 85 | } 86 | }, 87 | options: { 88 | ...getDefaultCompilerOptions(), 89 | declaration: true, 90 | declarationMap: true 91 | } 92 | }); 93 | } 94 | -------------------------------------------------------------------------------- /src/util/same-value.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The internal comparison abstract operation SameValueNonNumber(x, y), where neither x nor y are Number values, produces true or false. 3 | * 4 | * https://tc39.github.io/ecma262/#sec-samevaluenonnumber 5 | * @param {Exclude<*, number>} x 6 | * @param {Exclude<*, number>} y 7 | * @return {boolean} 8 | */ 9 | function sameValueNonNumber(x: Exclude, y: Exclude): boolean { 10 | // Assert: Type(x) is not Number. 11 | if (typeof x === "number") { 12 | throw new TypeError(`First argument 'x' must not be a number`); 13 | } 14 | 15 | // Assert: Type(x) is the same as Type(y). 16 | if (typeof x !== typeof y) { 17 | throw new TypeError(`The given arguments must have the same type`); 18 | } 19 | 20 | // If Type(x) is Undefined, return true. 21 | if (x === undefined) return true; 22 | 23 | // If Type(x) is Null, return true. 24 | if (x === null) return true; 25 | 26 | // If Type(x) is String, then 27 | if (typeof x === "string") { 28 | // If x and y are exactly the same sequence of code units 29 | // (same length and same code units at corresponding indices), return true; otherwise, return false. 30 | return x === y; 31 | } 32 | 33 | // If Type(x) is Boolean, then 34 | if (typeof x === "boolean") { 35 | // If x and y are both true or both false, return true; otherwise, return false. 36 | return x === y; 37 | } 38 | 39 | // If Type(x) is Symbol, then 40 | if (typeof x === "symbol") { 41 | // If x and y are both the same Symbol value, return true; otherwise, return false. 42 | return x.valueOf() === (y as symbol).valueOf(); 43 | } 44 | 45 | // If x and y are the same Object value, return true. Otherwise, return false. 46 | return x === y; 47 | } 48 | 49 | /** 50 | * The internal comparison abstract operation SameValue(x, y), where x and y are ECMAScript language values, produces true or false. 51 | * 52 | * https://tc39.github.io/ecma262/#sec-samevalue 53 | * @param {*} x 54 | * @param {*} y 55 | * @return {boolean} 56 | */ 57 | export function sameValue(x: unknown, y: unknown): boolean { 58 | // If Type(x) is different from Type(y), return false. 59 | if (typeof x !== typeof y) return false; 60 | 61 | // If Type(x) is Number, then 62 | if (typeof x === "number") { 63 | // If x is NaN and y is NaN, return true. 64 | if (isNaN(x) && isNaN(y as number)) return true; 65 | 66 | // If x is +0 and y is -0, return false. 67 | if (Object.is(x, 0) && Object.is(y, -0)) return false; 68 | 69 | // If x is the same Number value as y, return true. 70 | if (x === y) return true; 71 | 72 | // Return false. 73 | return false; 74 | } 75 | // Return SameValueNonNumber(x, y). 76 | return sameValueNonNumber(x, y); 77 | } 78 | -------------------------------------------------------------------------------- /scripts/build-data/build-data.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import {extractDefaultNumberSystemId, extractFields, localeIds} from "cldr"; 3 | import {sync} from "find-up"; 4 | import {dirname, join} from "path"; 5 | import {existsSync, mkdirSync} from "fs"; 6 | import {createProgramFromSources, SourceFileInput} from "./ts/create-program-from-sources"; 7 | import {ExtendedSingularRelativeTimeUnit, VALID_EXTENDED_SINGULAR_RELATIVE_TIME_UNIT_VALUES} from "../../src/unit/singular-relative-time-unit"; 8 | import {LocaleDataEntry, LocaleDataEntryValue} from "../../src/locale/locale-data"; 9 | import {stringify} from "javascript-stringify"; 10 | 11 | // The directory on disk to write locale files to 12 | const OUTPUT_DIRECTORY = join(dirname(sync("package.json")!), "locale-data"); 13 | 14 | // Ensure that the output directory exists 15 | if (!existsSync(OUTPUT_DIRECTORY)) { 16 | mkdirSync(OUTPUT_DIRECTORY); 17 | } 18 | // Prepare sources 19 | const sources: SourceFileInput[] = []; 20 | 21 | // Loop through all locales 22 | for (const localeId of localeIds) { 23 | // @ts-ignore 24 | const locale = Intl.getCanonicalLocales(localeId.replace(/_/g, "-"))[0]; 25 | console.log(`Building data for locale: ${locale} (localeId: ${localeId})`); 26 | 27 | // Take the default NumberSystem for the locale 28 | const nu = [extractDefaultNumberSystemId(localeId)]; 29 | 30 | // Prepare relative time formatting data for the locale 31 | const relativeTimeLocaleData = {} as {[Key in ExtendedSingularRelativeTimeUnit]: LocaleDataEntryValue | undefined}; 32 | 33 | const fields = extractFields(localeId); 34 | 35 | for (const [key, fieldEntry] of Object.entries(fields) as [ 36 | ExtendedSingularRelativeTimeUnit, 37 | {relative?: {[key: string]: string}; relativeTime?: {future: Record; past: Record}} 38 | ][]) { 39 | if (fieldEntry.relativeTime == null || !VALID_EXTENDED_SINGULAR_RELATIVE_TIME_UNIT_VALUES.includes(key)) continue; 40 | 41 | // @ts-ignore 42 | relativeTimeLocaleData[key] = { 43 | ...(fieldEntry.relative != null ? fieldEntry.relative : {}), 44 | future: fieldEntry.relativeTime.future, 45 | past: fieldEntry.relativeTime.past 46 | }; 47 | } 48 | 49 | // Prepare the final LocaleDataEntry 50 | const localeDataEntry: LocaleDataEntry = { 51 | ...relativeTimeLocaleData, 52 | nu 53 | }; 54 | 55 | // Add the source to the sources 56 | sources.push({ 57 | fileName: join(OUTPUT_DIRECTORY, `${locale}.ts`), 58 | text: `\ 59 | if ("__addLocaleData" in Intl.RelativeTimeFormat) { 60 | Intl.RelativeTimeFormat.__addLocaleData({ 61 | locale: "${locale}", 62 | data: ${stringify(localeDataEntry, undefined, " ")} 63 | }); 64 | }` 65 | }); 66 | } 67 | 68 | console.log(`Emitting locale data...`); 69 | 70 | // Create a Program from the SourceFiles 71 | const program = createProgramFromSources(sources); 72 | 73 | // Emit all of them! 74 | program.emit(); 75 | 76 | console.log(`Successfully built data!`); 77 | -------------------------------------------------------------------------------- /src/unit/singular-relative-time-unit.ts: -------------------------------------------------------------------------------- 1 | import {RelativeTimeUnit} from "./relative-time-unit"; 2 | 3 | export type SingularRelativeTimeUnit = "second" | "minute" | "hour" | "day" | "week" | "month" | "quarter" | "year"; 4 | 5 | export type ExtendedSingularRelativeTimeUnit = 6 | | SingularRelativeTimeUnit 7 | | "second-narrow" 8 | | "second-short" 9 | | "minute-narrow" 10 | | "minute-short" 11 | | "hour-narrow" 12 | | "hour-short" 13 | | "day-narrow" 14 | | "day-short" 15 | | "week-narrow" 16 | | "week-short" 17 | | "month-narrow" 18 | | "month-short" 19 | | "quarter-narrow" 20 | | "quarter-short" 21 | | "year-narrow" 22 | | "year-short"; 23 | 24 | const VALID_SINGULAR_RELATIVE_TIME_UNIT_VALUES: SingularRelativeTimeUnit[] = ["second", "minute", "hour", "day", "week", "month", "quarter", "year"]; 25 | 26 | export const VALID_EXTENDED_SINGULAR_RELATIVE_TIME_UNIT_VALUES: ExtendedSingularRelativeTimeUnit[] = [ 27 | ...VALID_SINGULAR_RELATIVE_TIME_UNIT_VALUES, 28 | "second-narrow", 29 | "second-short", 30 | "minute-narrow", 31 | "minute-short", 32 | "hour-narrow", 33 | "hour-short", 34 | "day-narrow", 35 | "day-short", 36 | "week-narrow", 37 | "week-short", 38 | "month-narrow", 39 | "month-short", 40 | "quarter-narrow", 41 | "quarter-short", 42 | "year-narrow", 43 | "year-short" 44 | ]; 45 | 46 | /** 47 | * Sanitizes a RelativeTimeUnit into a SingularRelativeTimeUnit 48 | * 49 | * http://tc39.github.io/proposal-intl-relative-time/#sec-singularrelativetimeunit 50 | * @param {RelativeTimeUnit} unit 51 | * @return {SingularRelativeTimeUnit} 52 | */ 53 | export function singularRelativeTimeUnit(unit: RelativeTimeUnit): SingularRelativeTimeUnit { 54 | // Assert: Type(unit) is String. 55 | if (typeof unit !== "string") { 56 | throw new TypeError(`unit: '${unit}' must be a string`); 57 | } 58 | 59 | // If unit is "seconds", return "second". 60 | if (unit === "seconds") return "second"; 61 | 62 | // If unit is "minutes", return "minute". 63 | if (unit === "minutes") return "minute"; 64 | 65 | // If unit is "hours", return "hour". 66 | if (unit === "hours") return "hour"; 67 | 68 | // If unit is "days", return "day". 69 | if (unit === "days") return "day"; 70 | 71 | // If unit is "weeks", return "week". 72 | if (unit === "weeks") return "week"; 73 | 74 | // If unit is "months", return "month". 75 | if (unit === "months") return "month"; 76 | 77 | // If unit is "quarters", return "quarter". 78 | if (unit === "quarters") return "quarter"; 79 | 80 | // If unit is "years", return "year". 81 | if (unit === "years") return "year"; 82 | 83 | // If unit is not one of "second", "minute", "hour", "day", "week", "month", "quarter", or "year", throw a RangeError exception. 84 | if (!VALID_SINGULAR_RELATIVE_TIME_UNIT_VALUES.some(validUnit => validUnit === unit)) { 85 | throw new RangeError(`Unit: '${unit}' must be one of: ${VALID_SINGULAR_RELATIVE_TIME_UNIT_VALUES.map(val => `"${val}"`).join(", ")}`); 86 | } 87 | 88 | // Return unit. 89 | return unit; 90 | } 91 | -------------------------------------------------------------------------------- /test/format-to-parts.test.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import test from "ava"; 3 | import "../src/test262"; 4 | import "../locale-data/en"; 5 | 6 | // tslint:disable 7 | 8 | test("Supports the 'day' unit with default options. #1", t => { 9 | const rtf = new Intl.RelativeTimeFormat("en"); 10 | const result = rtf.formatToParts(-1, "day"); 11 | t.deepEqual(result, [ 12 | { 13 | type: "integer", 14 | value: "1", 15 | unit: "day" 16 | }, 17 | { 18 | type: "literal", 19 | value: " day ago" 20 | } 21 | ]); 22 | }); 23 | 24 | test("Supports the 'day' unit with default options. #2", t => { 25 | const rtf = new Intl.RelativeTimeFormat("en"); 26 | const result = rtf.formatToParts(1, "day"); 27 | t.deepEqual(result, [ 28 | { 29 | type: "literal", 30 | value: "in " 31 | }, 32 | { 33 | type: "integer", 34 | value: "1", 35 | unit: "day" 36 | }, 37 | { 38 | type: "literal", 39 | value: " day" 40 | } 41 | ]); 42 | }); 43 | 44 | test("Supports the 'day' unit with default options. #3", t => { 45 | const rtf = new Intl.RelativeTimeFormat("en"); 46 | const result = rtf.formatToParts(0, "day"); 47 | t.deepEqual(result, [ 48 | { 49 | type: "literal", 50 | value: "in " 51 | }, 52 | { 53 | type: "integer", 54 | value: "0", 55 | unit: "day" 56 | }, 57 | { 58 | type: "literal", 59 | value: " days" 60 | } 61 | ]); 62 | }); 63 | 64 | test("Supports the 'day' unit with default options. #4", t => { 65 | const rtf = new Intl.RelativeTimeFormat("en"); 66 | const result = rtf.formatToParts(-1.1, "day"); 67 | 68 | t.deepEqual(result, [ 69 | { 70 | type: "integer", 71 | value: "1", 72 | unit: "day" 73 | }, 74 | { 75 | type: "decimal", 76 | value: ".", 77 | unit: "day" 78 | }, 79 | { 80 | type: "fraction", 81 | value: "1", 82 | unit: "day" 83 | }, 84 | { 85 | type: "literal", 86 | value: " days ago" 87 | } 88 | ]); 89 | }); 90 | 91 | test("Supports the 'quarter' unit with style 'short'. #1", t => { 92 | const rtf = new Intl.RelativeTimeFormat("en", {style: "short"}); 93 | const result = rtf.formatToParts(1, "quarter"); 94 | t.deepEqual(result, [ 95 | { 96 | type: "literal", 97 | value: "in " 98 | }, 99 | { 100 | type: "integer", 101 | value: "1", 102 | unit: "quarter" 103 | }, 104 | { 105 | type: "literal", 106 | value: " qtr." 107 | } 108 | ]); 109 | }); 110 | 111 | test("Supports the 'week' unit with style 'short' and numeric 'always'. #1", t => { 112 | const rtf = new Intl.RelativeTimeFormat("en", { 113 | style: "short", 114 | numeric: "always" 115 | }); 116 | const result = rtf.formatToParts(1, "week"); 117 | t.deepEqual(result, [ 118 | { 119 | type: "literal", 120 | value: "in " 121 | }, 122 | { 123 | type: "integer", 124 | value: "1", 125 | unit: "week" 126 | }, 127 | { 128 | type: "literal", 129 | value: " wk." 130 | } 131 | ]); 132 | }); 133 | -------------------------------------------------------------------------------- /test/format.test.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import test from "ava"; 3 | import "../src/test262"; 4 | import "../locale-data/en"; 5 | 6 | // tslint:disable 7 | 8 | test("Will use the default locale if none is given. #1", t => { 9 | const rtf = new Intl.RelativeTimeFormat(); 10 | const result = rtf.format(-1, "day"); 11 | t.deepEqual(result, "1 day ago"); 12 | }); 13 | 14 | test("Supports the 'day' unit with default options. #1", t => { 15 | const rtf = new Intl.RelativeTimeFormat("en"); 16 | const result = rtf.format(-1, "day"); 17 | t.deepEqual(result, "1 day ago"); 18 | }); 19 | 20 | test("Supports the 'day' unit with default options. #2", t => { 21 | const rtf = new Intl.RelativeTimeFormat("en"); 22 | const result = rtf.format(1, "day"); 23 | t.deepEqual(result, "in 1 day"); 24 | }); 25 | 26 | test("Supports the 'day' unit with default options. #3", t => { 27 | const rtf = new Intl.RelativeTimeFormat("en"); 28 | const result = rtf.format(0, "day"); 29 | t.deepEqual(result, "in 0 days"); 30 | }); 31 | 32 | test("Supports the 'day' unit with default options. #4", t => { 33 | const rtf = new Intl.RelativeTimeFormat("en"); 34 | const result = rtf.format(-1.1, "day"); 35 | t.deepEqual(result, "1.1 days ago"); 36 | }); 37 | 38 | test("Understands plural version of units as aliases of the singular ones. #1", t => { 39 | const rtf = new Intl.RelativeTimeFormat("en"); 40 | const result = rtf.format(-1.1, "days"); 41 | t.deepEqual(result, "1.1 days ago"); 42 | }); 43 | 44 | test("Supports the 'quarter' unit with style 'short'. #1", t => { 45 | const rtf = new Intl.RelativeTimeFormat("en", {style: "short"}); 46 | const result = rtf.format(1, "quarter"); 47 | t.deepEqual(result, "in 1 qtr."); 48 | }); 49 | 50 | test("Supports the 'week' unit with style 'short' and numeric 'always'. #1", t => { 51 | const rtf = new Intl.RelativeTimeFormat("en", { 52 | style: "short", 53 | numeric: "always" 54 | }); 55 | const result = rtf.format(1, "week"); 56 | t.deepEqual(result, "in 1 wk."); 57 | }); 58 | 59 | test("Supports the 'quarter' unit. #1", t => { 60 | const rtf = new Intl.RelativeTimeFormat("en"); 61 | const result = rtf.format(1, "quarter"); 62 | t.deepEqual(result, "in 1 quarter"); 63 | }); 64 | 65 | test("Supports the 'second' unit. #1", t => { 66 | const rtf = new Intl.RelativeTimeFormat("en"); 67 | const result = rtf.format(1, "second"); 68 | t.deepEqual(result, "in 1 second"); 69 | }); 70 | 71 | test("Supports the 'second' unit. #2", t => { 72 | const rtf = new Intl.RelativeTimeFormat("en"); 73 | const result = rtf.format(2, "second"); 74 | t.deepEqual(result, "in 2 seconds"); 75 | }); 76 | 77 | test("Differentiates between -0 and +0. #1", t => { 78 | const rtf = new Intl.RelativeTimeFormat("en", {numeric: "always"}); 79 | const result = rtf.format(-0, "second"); 80 | t.deepEqual(result, "0 seconds ago"); 81 | }); 82 | 83 | test("Differentiates between -0 and +0. #2", t => { 84 | const rtf = new Intl.RelativeTimeFormat("en", {numeric: "always"}); 85 | const result = rtf.format(0, "second"); 86 | t.deepEqual(result, "in 0 seconds"); 87 | }); 88 | -------------------------------------------------------------------------------- /src/relative-time-format/unicode-extension/unicode-extension.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A Regular Expression that matches Unicode extension sequences 3 | * @type {RegExp} 4 | */ 5 | export const UNICODE_EXTENSION_SEQUENCE_REGEXP = /-u(?:-[0-9a-z]{2,8})+/gi; 6 | 7 | /** 8 | * Removes all Unicode characters from the given string 9 | * @param {string} str 10 | * @return {string} 11 | */ 12 | export function removeUnicodeExtensionSequences(str: string): string { 13 | return str.replace(UNICODE_EXTENSION_SEQUENCE_REGEXP, ""); 14 | } 15 | 16 | /** 17 | * The abstract operation UnicodeExtensionValue is called with extension, which must be a Unicode locale extension sequence, 18 | * and String key. This operation returns the type subtags for key. 19 | * @param {string} extension 20 | * @param {string} key 21 | * @returns {string?} 22 | */ 23 | export function unicodeExtensionValue(extension: string, key: string): string | undefined { 24 | // Assert: The number of elements in key is 2. 25 | if (key.length !== 2) { 26 | throw new TypeError(`Could not get UnicodeExtensionValue: The given key: '${key}' must have a length of 2`); 27 | } 28 | 29 | // Let size be the number of elements in extension. 30 | const size = key.length; 31 | 32 | // Let searchValue be the concatenation of "-", key, and "-". 33 | let searchValue = `-${key}-`; 34 | 35 | // Let pos be Call(%StringProto_indexOf%, extension, « searchValue »). 36 | let pos = String.prototype.indexOf.call(extension, searchValue); 37 | 38 | // If pos ≠ -1, then 39 | if (pos !== -1) { 40 | // Let start be pos + 4. 41 | const start = pos + 4; 42 | // Let end be start. 43 | let end = start; 44 | // Let k be start. 45 | let k = start; 46 | // Let done be false. 47 | let done = false; 48 | 49 | // Repeat, while done is false 50 | while (!done) { 51 | // Let e be Call(%StringProto_indexOf%, extension, « "-", k »). 52 | const e = String.prototype.indexOf.call(extension, "-", k); 53 | 54 | // If e = -1, let len be size - k; else let len be e - k. 55 | const len = e === -1 ? size - k : e - k; 56 | 57 | // If len = 2, then 58 | if (len === 2) { 59 | // Let done be true. 60 | done = true; 61 | } 62 | 63 | // Else if e = -1, then 64 | else if (e === -1) { 65 | // Let end be size. 66 | end = size; 67 | 68 | // Let done be true. 69 | done = true; 70 | } 71 | 72 | // Else, 73 | else { 74 | // Let end be e. 75 | end = e; 76 | 77 | // Let k be e + 1. 78 | k = e + 1; 79 | } 80 | } 81 | // Return the String value equal to the substring of extension consisting of 82 | // the code units at indices start (inclusive) through end (exclusive). 83 | return extension.slice(start, end); 84 | } 85 | 86 | // Let searchValue be the concatenation of "-" and key. 87 | searchValue = `-${key}`; 88 | // Let pos be Call(%StringProto_indexOf%, extension, « searchValue »). 89 | pos = String.prototype.indexOf.call(extension, searchValue); 90 | // If pos ≠ -1 and pos + 3 = size, then 91 | if (pos !== -1 && pos + 3 === size) { 92 | // Return the empty String. 93 | return ""; 94 | } 95 | 96 | // Return undefined. 97 | return undefined; 98 | } 99 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | Contributor Covenant Code of Conduct 2 | 3 | Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting any of the code of conduct enforcers: [Frederik Wessberg](mailto:frederikwessberg@hotmail.com) ([@FredWessberg](https://twitter.com/FredWessberg)) ([Website](https://github.com/wessberg)). 59 | All complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | Attribution 69 | 70 | This Code of Conduct is adapted from the Contributor Covenant, version 1.4, 71 | available at http://contributor-covenant.org/version/1/4/ 72 | -------------------------------------------------------------------------------- /src/relative-time-format/internal-slot/internal-slot.ts: -------------------------------------------------------------------------------- 1 | import {RelativeTimeFormat} from "../relative-time-format/relative-time-format"; 2 | import {RelativeTimeFormatInstanceInternals} from "./relative-time-format-instance-internals"; 3 | import {RelativeTimeFormatStaticInternals} from "./relative-time-format-static-internals"; 4 | 5 | /** 6 | * A WeakMap between RelativeTimeFormat instances and their internal slot members 7 | * @type {WeakMap} 8 | */ 9 | export const RELATIVE_TIME_FORMAT_INSTANCE_INTERNAL_MAP: WeakMap = new WeakMap(); 10 | 11 | /** 12 | * Contains the internal static for RelativeTimeFormat 13 | * @type {RelativeTimeFormatStaticInternals} 14 | */ 15 | export const RELATIVE_TIME_FORMAT_STATIC_INTERNALS: RelativeTimeFormatStaticInternals = { 16 | /** 17 | * The value of the [[RelevantExtensionKeys]] internal slot is « "nu" ». 18 | * http://tc39.github.io/proposal-intl-relative-time/#sec-Intl.RelativeTimeFormat-internal-slots 19 | */ 20 | relevantExtensionKeys: ["nu"], 21 | 22 | /** 23 | * The value of the [[LocaleData]] internal slot is implementation defined within the constraints described in 9.1 24 | * http://tc39.github.io/proposal-intl-relative-time/#sec-Intl.RelativeTimeFormat-internal-slots 25 | */ 26 | localeData: {}, 27 | 28 | /** 29 | * The value of the [[AvailableLocales]] internal slot is implementation defined within the constraints described in 9.1 30 | * http://tc39.github.io/proposal-intl-relative-time/#sec-Intl.RelativeTimeFormat-internal-slots 31 | */ 32 | availableLocales: [] 33 | }; 34 | 35 | /** 36 | * Sets the value for a property in an internal slot for an instance of RelativeTimeFormat 37 | * @param {RelativeTimeFormat} instance 38 | * @param {T} property 39 | * @param {RelativeTimeFormatInstanceInternals[T]} value 40 | */ 41 | export function setInternalSlot( 42 | instance: RelativeTimeFormat, 43 | property: T, 44 | value: RelativeTimeFormatInstanceInternals[T] 45 | ): void { 46 | let record = RELATIVE_TIME_FORMAT_INSTANCE_INTERNAL_MAP.get(instance); 47 | if (record == null) { 48 | record = Object.create(null) as RelativeTimeFormatInstanceInternals; 49 | RELATIVE_TIME_FORMAT_INSTANCE_INTERNAL_MAP.set(instance, record); 50 | } 51 | 52 | // Update the property with the given value 53 | record[property] = value; 54 | } 55 | 56 | /** 57 | * Gets the value associated with the given property on the internal slots of the given instance of RelativeTimeFormat 58 | * @param {RelativeTimeFormat} instance 59 | * @param {T} property 60 | * @return {RelativeTimeFormatInstanceInternals[T]} 61 | */ 62 | export function getInternalSlot( 63 | instance: RelativeTimeFormat, 64 | property: T 65 | ): RelativeTimeFormatInstanceInternals[T] { 66 | const record = RELATIVE_TIME_FORMAT_INSTANCE_INTERNAL_MAP.get(instance); 67 | if (record == null) { 68 | throw new ReferenceError(`No internal slots has been allocated for the given instance of RelativeTimeFormat`); 69 | } 70 | 71 | return record[property]; 72 | } 73 | 74 | /** 75 | * Returns true if the given property on the internal slots of the given instance of RelativeTimeFormat exists 76 | * @param {RelativeTimeFormat} instance 77 | * @param {T} property 78 | * @return {RelativeTimeFormatInstanceInternals[T]} 79 | */ 80 | export function hasInternalSlot(instance: RelativeTimeFormat, property: T): boolean { 81 | const record = RELATIVE_TIME_FORMAT_INSTANCE_INTERNAL_MAP.get(instance); 82 | return record != null && property in record; 83 | } 84 | -------------------------------------------------------------------------------- /src/relative-time-format/make-parts-list/make-parts-list.ts: -------------------------------------------------------------------------------- 1 | import {SingularRelativeTimeUnit} from "../../unit/singular-relative-time-unit"; 2 | import {RelativeTimeFormatPart} from "../../relative-time-format-part/relative-time-format-part"; 3 | 4 | /** 5 | * The MakePartsList abstract operation is called with arguments pattern, 6 | * a pattern String, unit, a String, and parts, a List of Records representing a formatted Number. 7 | * 8 | * http://tc39.github.io/proposal-intl-relative-time/#sec-makepartslist 9 | * @param {string} pattern 10 | * @param {SingularRelativeTimeUnit} unit 11 | * @param {Intl.NumberFormatPart[]} parts 12 | * @returns {RelativeTimeFormatPart} 13 | */ 14 | export function makePartsList(pattern: string, unit: SingularRelativeTimeUnit, parts: Intl.NumberFormatPart[]): RelativeTimeFormatPart[] { 15 | // Let result be a new empty List. 16 | const result: RelativeTimeFormatPart[] = []; 17 | 18 | // Let beginIndex be ! Call(%StringProto_indexOf%, pattern, « "{", 0 »). 19 | let beginIndex = String.prototype.indexOf.call(pattern, "{", 0); 20 | 21 | // Let endIndex be 0. 22 | let endIndex = 0; 23 | 24 | // Let nextIndex be 0. 25 | let nextIndex = 0; 26 | 27 | // Let length be the number of elements in pattern. 28 | const length = pattern.length; 29 | 30 | // Repeat, while beginIndex is an integer index into pattern 31 | while (pattern[beginIndex] !== undefined) { 32 | // Set endIndex to ! Call(%StringProto_indexOf%, pattern, « "}", beginIndex »). 33 | endIndex = String.prototype.indexOf.call(pattern, "}", beginIndex); 34 | 35 | // Assert: endIndex is not -1, otherwise the pattern would be malformed. 36 | if (endIndex === -1) { 37 | throw new RangeError(`The pattern: '${pattern}' is malformed`); 38 | } 39 | 40 | // If beginIndex is greater than nextIndex, then 41 | if (beginIndex > nextIndex) { 42 | // Let literal be a substring of pattern from position nextIndex, inclusive, to position beginIndex, exclusive. 43 | const literal = pattern.slice(nextIndex, beginIndex); 44 | 45 | // Add new part Record { [[Type]]: "literal", [[Value]]: literal } as a new element of the list result. 46 | result.push({ 47 | type: "literal", 48 | value: literal 49 | }); 50 | } 51 | 52 | // Let p be the substring of pattern from position beginIndex, exclusive, to position endIndex, exclusive. 53 | const p = pattern.slice(beginIndex + 1, endIndex); 54 | 55 | // Assert: p is "0". 56 | if (p !== "0") { 57 | throw new TypeError(`Expected ${p} to be "0"`); 58 | } 59 | 60 | // For each part in parts, do 61 | for (const part of parts) { 62 | // Add new part Record { [[Type]]: part.[[Type]], [[Value]]: part.[[Value]], [[Unit]]: unit } as a new element on the List result. 63 | if (part.type === "literal") { 64 | result.push({...part, type: part.type}); 65 | } else { 66 | result.push({...part, unit}); 67 | } 68 | } 69 | 70 | // Set nextIndex to endIndex + 1. 71 | nextIndex = endIndex + 1; 72 | 73 | // Set beginIndex to Call(%StringProto_indexOf%, pattern, « "{", nextIndex »). 74 | beginIndex = String.prototype.indexOf.call(pattern, "{", nextIndex); 75 | } 76 | 77 | // If nextIndex is less than length, then 78 | if (nextIndex < length) { 79 | // Let literal be the substring of pattern from position nextIndex, exclusive, to position length, exclusive. 80 | // CORRECTION: It should actually be from nextIndex, inclusive, to correctly partition text 81 | const literal = pattern.slice(nextIndex, length); 82 | 83 | // Add new part Record { [[Type]]: "literal", [[Value]]: literal } as a new element of the list result. 84 | result.push({ 85 | type: "literal", 86 | value: literal 87 | }); 88 | } 89 | 90 | return result; 91 | } 92 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "intl-relative-time-format", 3 | "version": "1.0.7", 4 | "description": "A fully spec-compliant polyfill for 'Intl.RelativeTimeFormat'", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/wessberg/intl-relative-time-format.git" 8 | }, 9 | "bugs": { 10 | "url": "https://github.com/wessberg/intl-relative-time-format/issues" 11 | }, 12 | "scripts": { 13 | "generate:readme": "scaffold readme --yes", 14 | "generate:license": "scaffold license --yes", 15 | "generate:contributing": "scaffold contributing --yes", 16 | "generate:coc": "scaffold coc --yes", 17 | "generate:changelog": "standard-changelog --first-release", 18 | "generate:all": "npm run generate:license & npm run generate:contributing & npm run generate:coc & npm run generate:readme && npm run generate:changelog", 19 | "clean:dist": "rm -rf dist", 20 | "clean:compiled": "rm -rf compiled", 21 | "clean": "npm run clean:dist && npm run clean:compiled", 22 | "lint": "tsc --noEmit && tslint -c tslint.json --project tsconfig.json", 23 | "prettier": "prettier --write '{src,test,documentation}/**/*.{js,ts,json,html,xml,css,md}'", 24 | "test": "ava", 25 | "test262": "node test262-runner.js", 26 | "posttest": "npm run clean:compiled", 27 | "prebuild": "npm run clean:dist", 28 | "build": "npm run rollup", 29 | "watch": "npm run rollup -- --watch", 30 | "build_data": "cd scripts/build-data && tsc && node compiled/scripts/build-data/build-data.js && rm -rf compiled", 31 | "rollup": "rollup -c rollup.config.js", 32 | "preversion": "npm run lint && npm run build_data && NODE_ENV=production npm run build", 33 | "version": "npm run generate:all && git add .", 34 | "release": "np --no-cleanup --no-yarn" 35 | }, 36 | "files": [ 37 | "dist/**/*.*", 38 | "locale-data/**/*.*" 39 | ], 40 | "keywords": [ 41 | "intl", 42 | "RelativeTimeFormat", 43 | "ecma-402", 44 | "internationalization", 45 | "i18n", 46 | "time ago", 47 | "polyfill", 48 | "relative time", 49 | "ECMAScript internationalization API" 50 | ], 51 | "contributors": [ 52 | { 53 | "name": "Frederik Wessberg", 54 | "email": "frederikwessberg@hotmail.com", 55 | "url": "https://github.com/wessberg", 56 | "imageUrl": "https://avatars2.githubusercontent.com/u/20454213?s=460&v=4", 57 | "role": "Lead Developer", 58 | "twitter": "FredWessberg" 59 | } 60 | ], 61 | "license": "MIT", 62 | "devDependencies": { 63 | "@types/find-up": "^2.1.1", 64 | "@wessberg/rollup-plugin-ts": "1.1.59", 65 | "@wessberg/browserslist-generator": "1.0.23", 66 | "@wessberg/scaffold": "1.0.19", 67 | "@wessberg/ts-config": "^0.0.41", 68 | "babel-preset-minify": "0.5.0", 69 | "ava": "^2.1.0", 70 | "test262-harness": "^6.3.2", 71 | "cldr": "^5.3.0", 72 | "find-up": "^4.1.0", 73 | "rollup": "^1.16.6", 74 | "rollup-plugin-node-resolve": "^5.2.0", 75 | "standard-changelog": "^2.0.11", 76 | "javascript-stringify": "^2.0.0", 77 | "tslib": "^1.10.0", 78 | "tslint": "^5.18.0", 79 | "typescript": "^3.5.2", 80 | "prettier": "^1.18.2", 81 | "pretty-quick": "^1.11.1", 82 | "husky": "^3.0.0", 83 | "np": "^5.0.3", 84 | "ts-node": "8.3.0", 85 | "rollup-plugin-multi-entry": "2.1.0", 86 | "full-icu": "1.3.0" 87 | }, 88 | "dependencies": {}, 89 | "test262": "./test262-polyfill.js", 90 | "minified": "./dist/index.min.js", 91 | "main": "./dist/index.js", 92 | "module": "./dist/index.esm.js", 93 | "browser": "./dist/index.esm.js", 94 | "types": "./dist/index.d.ts", 95 | "typings": "./dist/index.d.ts", 96 | "es2015": "./dist/index.esm.js", 97 | "engines": { 98 | "node": ">=4.0.0" 99 | }, 100 | "husky": { 101 | "hooks": { 102 | "pre-commit": "pretty-quick --staged" 103 | } 104 | }, 105 | "ava": { 106 | "files": [ 107 | "test/*.test.ts" 108 | ], 109 | "compileEnhancements": false, 110 | "extensions": [ 111 | "ts" 112 | ], 113 | "require": [ 114 | "ts-node/register" 115 | ] 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/relative-time-format/partition-relative-time-pattern/partition-relative-time-pattern.ts: -------------------------------------------------------------------------------- 1 | import {RelativeTimeFormat} from "../relative-time-format/relative-time-format"; 2 | import {RelativeTimeUnit} from "../../unit/relative-time-unit"; 3 | import {ExtendedSingularRelativeTimeUnit, singularRelativeTimeUnit} from "../../unit/singular-relative-time-unit"; 4 | import {getInternalSlot, hasInternalSlot} from "../internal-slot/internal-slot"; 5 | import {resolvePlural} from "../resolve-plural/resolve-plural"; 6 | import {makePartsList} from "../make-parts-list/make-parts-list"; 7 | import {toString} from "../../util/to-string"; 8 | import {RelativeTimeFormatPart} from "../../relative-time-format-part/relative-time-format-part"; 9 | 10 | /** 11 | * When the FormatRelativeTime abstract operation is called with arguments relativeTimeFormat, 12 | * value, and unit it returns a String value representing value (interpreted as a time value as specified in ES2016, 20.3.1.1) 13 | * according to the effective locale and the formatting options of relativeTimeFormat. 14 | * @param {RelativeTimeFormat} relativeTimeFormat 15 | * @param {number} value 16 | * @param {RelativeTimeUnit} unit 17 | * @returns {RelativeTimeFormatPart[]} 18 | */ 19 | export function partitionRelativeTimePattern( 20 | relativeTimeFormat: RelativeTimeFormat, 21 | value: number, 22 | unit: RelativeTimeUnit 23 | ): RelativeTimeFormatPart[] { 24 | // Assert: relativeTimeFormat has an [[InitializedRelativeTimeFormat]] internal slot. 25 | if (!hasInternalSlot(relativeTimeFormat, "initializedRelativeTimeFormat")) { 26 | throw new TypeError(`Internal function called on incompatible receiver ${relativeTimeFormat.toString()}`); 27 | } 28 | 29 | // Assert: Type(value) is Number. 30 | if (typeof value !== "number") { 31 | throw new TypeError(`Argument: 'value' must be a number`); 32 | } 33 | // Assert: Type(unit) is String. 34 | if (typeof unit !== "string") { 35 | throw new TypeError(`Argument: 'unit' must be a string`); 36 | } 37 | 38 | // If value is NaN, +∞, or -∞, throw a RangeError exception. 39 | if (isNaN(value) || value === Infinity || value === -Infinity) { 40 | throw new RangeError(`Value need to be finite number`); 41 | } 42 | 43 | // Let unit be ? SingularRelativeTimeUnit(unit). 44 | unit = singularRelativeTimeUnit(unit); 45 | 46 | // Let fields be relativeTimeFormat.[[Fields]]. 47 | const fields = getInternalSlot(relativeTimeFormat, "fields"); 48 | 49 | // Let style be relativeTimeFormat.[[Style]]. 50 | const style = getInternalSlot(relativeTimeFormat, "style"); 51 | 52 | // If style is equal to "short", then let entry be the string-concatenation of unit and "-short". 53 | // Else if style is equal to "narrow", then let entry be the string-concatenation of unit and "-narrow". 54 | // Else let entry be unit. 55 | let entry = 56 | style === "short" 57 | ? (`${unit}-short` as ExtendedSingularRelativeTimeUnit) 58 | : style === "narrow" 59 | ? (`${unit}-narrow` as ExtendedSingularRelativeTimeUnit) 60 | : unit; 61 | 62 | // Let exists be ! HasProperty(fields, entry). 63 | let exists = entry in fields; 64 | 65 | // If exists is false, then 66 | if (!exists) { 67 | // Let entry be unit. 68 | entry = unit; 69 | } 70 | 71 | // Let patterns be ! Get(fields, entry). 72 | const patterns = fields[entry]; 73 | 74 | // Make sure that the patterns are defined 75 | if (patterns == null) { 76 | throw new TypeError(`Could not match entry: '${entry}' inside fields for locale: '${getInternalSlot(relativeTimeFormat, "locale")}'`); 77 | } 78 | 79 | // Let numeric be relativeTimeFormat.[[Numeric]]. 80 | const numeric = getInternalSlot(relativeTimeFormat, "numeric"); 81 | 82 | // If numeric is equal to "auto", then 83 | if (numeric === "auto") { 84 | // Let exists be ! HasProperty(patterns, ! ToString(value)). 85 | exists = toString(value) in patterns; 86 | 87 | // If exists is true, then 88 | if (exists) { 89 | // Let result be ! Get(patterns, ! ToString(value)). 90 | const result = patterns[toString(value)]; 91 | 92 | // Return a List containing the Record { [[Type]]: "literal", [[Value]]: result }. 93 | return [ 94 | { 95 | type: "literal", 96 | value: result 97 | } 98 | ]; 99 | } 100 | } 101 | 102 | // If value is -0 or if value is less than 0, then let tl be "past". 103 | // Else let tl be "future". 104 | const tl = Object.is(value, -0) || value < 0 ? "past" : "future"; 105 | 106 | // Let po be ! Get(patterns, tl). 107 | const po = patterns[tl]; 108 | 109 | // Let fv be ! PartitionNumberPattern(relativeTimeFormat.[[NumberFormat]], value). 110 | const fv = getInternalSlot(relativeTimeFormat, "numberFormat").formatToParts(Math.abs(value)); 111 | 112 | // Let pr be ! ResolvePlural(relativeTimeFormat.[[PluralRules]], value). 113 | const pr = resolvePlural(relativeTimeFormat, value); 114 | 115 | // Let pattern be ! Get(po, pr). 116 | const pattern = po[pr]; 117 | 118 | // Return ! MakePartsList(pattern, unit, fv). 119 | return makePartsList(pattern, unit, fv); 120 | } 121 | -------------------------------------------------------------------------------- /src/relative-time-format/resolve-locale/resolve-locale.ts: -------------------------------------------------------------------------------- 1 | import {ResolveLocaleOptions} from "./resolve-locale-options"; 2 | import {lookupMatcher} from "../matcher/lookup-matcher/lookup-matcher"; 3 | import {bestFitMatcher} from "../matcher/best-fit-matcher/best-fit-matcher"; 4 | import {ResolveLocaleResult} from "./resolve-locale-result"; 5 | import {isRecord} from "../../assert/is-record"; 6 | import {isList} from "../../assert/is-list"; 7 | import {unicodeExtensionValue} from "../unicode-extension/unicode-extension"; 8 | import {sameValue} from "../../util/same-value"; 9 | import {Locales} from "../../locale/locales"; 10 | import {RelevantExtensionKey} from "../../relevant-extension-key/relevant-extension-key"; 11 | import {LocaleData} from "../../locale/locale-data"; 12 | 13 | /** 14 | * The ResolveLocale abstract operation compares a BCP 47 language priority list 15 | * requestedLocales against the locales in availableLocales and determines the best available language to meet the request. 16 | * availableLocales, requestedLocales, and relevantExtensionKeys must be provided as List values, 17 | * options and localeData as Records. 18 | * 19 | * https://tc39.github.io/ecma402/#sec-resolvelocale 20 | * @param {Locales} availableLocales 21 | * @param {Locales} requestedLocales 22 | * @param {ResolveLocaleOptions} options 23 | * @param {RelevantExtensionKey[]} relevantExtensionKeys 24 | * @param {LocaleData} localeData 25 | * @returns {ResolveLocaleResult} 26 | */ 27 | export function resolveLocale( 28 | availableLocales: Locales, 29 | requestedLocales: Locales, 30 | options: ResolveLocaleOptions, 31 | relevantExtensionKeys: RelevantExtensionKey[], 32 | localeData: LocaleData 33 | ): ResolveLocaleResult { 34 | // Let matcher be options.[[localeMatcher]]. 35 | const matcher = options.localeMatcher; 36 | // If matcher is "lookup", then 37 | // (a) Let r be LookupMatcher(availableLocales, requestedLocales). 38 | // (b) Let r be BestFitMatcher(availableLocales, requestedLocales). 39 | const r = matcher === "lookup" ? lookupMatcher({availableLocales, requestedLocales}) : bestFitMatcher({availableLocales, requestedLocales}); 40 | 41 | // Let foundLocale be r.[[locale]]. 42 | let foundLocale = r.locale; 43 | 44 | // Let result be a new Record. 45 | const result = {} as ResolveLocaleResult; 46 | 47 | // Set result.[[dataLocale]] to foundLocale. 48 | result.dataLocale = foundLocale; 49 | 50 | // Let supportedExtension be "-u" 51 | let supportedExtension = "-u"; 52 | 53 | // For each element key of relevantExtensionKeys in List order, do 54 | for (const key of relevantExtensionKeys) { 55 | // Let foundLocaleData be localeData.[[]]. 56 | const foundLocaleData = localeData[foundLocale]; 57 | 58 | // Assert: Type(foundLocaleData) is Record. 59 | if (!isRecord(foundLocaleData)) { 60 | throw new TypeError(`LocaleData for locale: '${foundLocale}' must be an object`); 61 | } 62 | 63 | // Let keyLocaleData be foundLocaleData.[[]]. 64 | const keyLocaleData = foundLocaleData[key]; 65 | 66 | // Assert: Type(keyLocaleData) is List. 67 | if (!isList(keyLocaleData)) { 68 | throw new TypeError(`key: '${key}' in LocaleData for locale: '${foundLocale}' must be indexable`); 69 | } 70 | 71 | // Let value be keyLocaleData[0]. 72 | let value = keyLocaleData[0]; 73 | 74 | // Assert: Type(value) is either String or Null. 75 | if (typeof value !== "string" && value !== null) { 76 | throw new TypeError(`value: '${value}' for key: '${key}' in LocaleData for locale: '${foundLocale}' must be a string or null`); 77 | } 78 | 79 | // Let supportedExtensionAddition be "". 80 | let supportedExtensionAddition = ""; 81 | 82 | // If r has an [[extension]] field, then 83 | if ("extension" in r) { 84 | // Let requestedValue be UnicodeExtensionValue(r.[[extension]], key). 85 | const requestedValue = unicodeExtensionValue(r.extension!, key); 86 | 87 | // If requestedValue is not undefined, then 88 | if (requestedValue !== undefined) { 89 | // If requestedValue is not the empty String, then 90 | if (requestedValue !== "") { 91 | // If keyLocaleData contains requestedValue, then 92 | if (keyLocaleData.includes(requestedValue)) { 93 | // Let value be requestedValue. 94 | value = requestedValue; 95 | 96 | // Let supportedExtensionAddition be the concatenation of "-", key, "-", and value. 97 | supportedExtensionAddition = `-${key}-${value}`; 98 | } 99 | } 100 | 101 | // Else if keyLocaleData contains "true", then 102 | else if (keyLocaleData.includes("true")) { 103 | // Let value be "true". 104 | value = "true"; 105 | } 106 | } 107 | } 108 | 109 | // If options has a field [[]], then 110 | if ("key" in options) { 111 | // Let optionsValue be options.[[]]. 112 | const optionsValue = options.key; 113 | 114 | // Assert: Type(optionsValue) is either String, Undefined, or Null. 115 | if (typeof optionsValue !== "string" && optionsValue != null) { 116 | throw new TypeError(`options value: '${optionsValue}' must be a string, undefined, or null`); 117 | } 118 | 119 | // If keyLocaleData contains optionsValue, then 120 | if (optionsValue !== undefined && keyLocaleData.includes(optionsValue)) { 121 | // If SameValue(optionsValue, value) is false, then 122 | // tslint:disable-next-line:no-collapsible-if 123 | if (!sameValue(optionsValue, value)) { 124 | // Let value be optionsValue. 125 | value = optionsValue; 126 | // Let supportedExtensionAddition be "". 127 | supportedExtensionAddition = ""; 128 | } 129 | } 130 | } 131 | 132 | // Set result.[[]] to value. 133 | result[key] = value; 134 | 135 | // Append supportedExtensionAddition to supportedExtension. 136 | supportedExtension += supportedExtensionAddition; 137 | } 138 | 139 | // If the number of elements in supportedExtension is greater than 2, then 140 | if (supportedExtension.length > 2) { 141 | // Let privateIndex be Call(%StringProto_indexOf%, foundLocale, « "-x-" »). 142 | const privateIndex = String.prototype.indexOf.call(foundLocale, "-x-"); 143 | 144 | // If privateIndex = -1, then 145 | if (privateIndex === -1) { 146 | // Let foundLocale be the concatenation of foundLocale and supportedExtension. 147 | foundLocale = `${foundLocale}${supportedExtension}`; 148 | } 149 | 150 | // Else, 151 | else { 152 | // Let preExtension be the substring of foundLocale from position 0, inclusive, to position privateIndex, exclusive. 153 | const preExtension = foundLocale.slice(0, privateIndex); 154 | 155 | // Let postExtension be the substring of foundLocale from position privateIndex to the end of the string. 156 | const postExtension = foundLocale.slice(privateIndex); 157 | 158 | // Let foundLocale be the concatenation of preExtension, supportedExtension, and postExtension. 159 | foundLocale = `${preExtension}${supportedExtension}${postExtension}`; 160 | } 161 | 162 | // Assert: IsStructurallyValidLanguageTag(foundLocale) is true. 163 | // Let foundLocale be CanonicalizeLanguageTag(foundLocale). 164 | // Intl.getCanonicalLocales will throw a TypeError if the locale isn't structurally valid 165 | foundLocale = Intl.getCanonicalLocales(foundLocale)[0]; 166 | } 167 | 168 | // Set result.[[locale]] to foundLocale. 169 | result.locale = foundLocale; 170 | 171 | // Return result. 172 | return result; 173 | } 174 | -------------------------------------------------------------------------------- /documentation/asset/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |
Logo
4 | 5 | 6 | 7 | 8 | 9 | > A fully spec-compliant polyfill for 'Intl.RelativeTimeFormat' 10 | 11 | 12 | 13 | 14 | 15 | Downloads per month 16 | NPM version 17 | Dependencies 18 | Contributors 19 | code style: prettier 20 | License: MIT 21 | Support on Patreon 22 | 23 | 24 | 25 | 26 | 27 | ## Description 28 | 29 | 30 | 31 | This is a 1:1 implementation of the [`Intl.RelativeTimeFormat`](https://github.com/tc39/proposal-intl-relative-time) draft spec proposal ECMA-402, or the ECMAScript® Internationalization API Specification. 32 | `Intl.RelativeTimeFormat` is a really useful low-level primitive to build on top of which avoids the need to parse lots of CLDR raw data at the expense of your users and their internet connections. 33 | 34 | It builds upon other members of the `Intl` family such as `Intl.PluralRules`, `Intl.NumberFormat`, and `Intl.getCanonicalLocales`, so these must be polyfilled. [See this section for an overview](#dependencies--browser-support). 35 | 36 | This implementation passes all 150 [Test262 Conformance tests](https://github.com/tc39/test262) from the Official ECMAScript Conformance Test Suite. 37 | 38 | 39 | 40 | ### Features 41 | 42 | 43 | 44 | Some highlights of this polyfill include: 45 | 46 | - A very precise implementation of the spec, with cross-references inlined in the source code 47 | - Conditional loading of Locale data for all CLDR locales 48 | - Well-tested and well-documented. 49 | - Passes all Official ECMAScript Conformance Tests 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | ## Table of Contents 58 | 59 | - [Description](#description) 60 | - [Features](#features) 61 | - [Table of Contents](#table-of-contents) 62 | - [Install](#install) 63 | - [NPM](#npm) 64 | - [Yarn](#yarn) 65 | - [Applying the polyfill](#applying-the-polyfill) 66 | - [Loading locale data](#loading-locale-data) 67 | - [Usage](#usage) 68 | - [Intl.RelativeTimeFormat.prototype.format](#intlrelativetimeformatprototypeformat) 69 | - [Intl.RelativeTimeFormat.prototype.formatToParts](#intlrelativetimeformatprototypeformattoparts) 70 | - [Intl.RelativeTimeFormat.prototype.resolvedOptions](#intlrelativetimeformatprototyperesolvedoptions) 71 | - [Intl.RelativeTimeFormat.supportedLocalesOf](#intlrelativetimeformatsupportedlocalesof) 72 | - [Dependencies & Browser support](#dependencies--browser-support) 73 | - [Contributing](#contributing) 74 | - [Maintainers](#maintainers) 75 | - [Backers](#backers) 76 | - [Patreon](#patreon) 77 | - [FAQ](#faq) 78 | - [What is the default locale?](#what-is-the-default-locale) 79 | - [Are there any known quirks?](#are-there-any-known-quirks) 80 | - [License](#license) 81 | 82 | 83 | 84 | 85 | 86 | ## Install 87 | 88 | ### NPM 89 | 90 | ``` 91 | $ npm install intl-relative-time-format 92 | ``` 93 | 94 | ### Yarn 95 | 96 | ``` 97 | $ yarn add intl-relative-time-format 98 | ``` 99 | 100 | 101 | 102 | ## Applying the polyfill 103 | 104 | The polyfill will check for the existence of `Intl.RelativeTimeFormat` and will _only_ be applied if the runtime doesn't already support it. 105 | 106 | To include it, add this somewhere: 107 | 108 | ```typescript 109 | import "intl-relative-time-format"; 110 | 111 | // Or with commonjs: 112 | require("intl-relative-time-format"); 113 | ``` 114 | 115 | However, it is strongly suggested that you only include the polyfill for runtimes that don't already support `Intl.RelativeTimeFormat`. 116 | One way to do so is with an async import: 117 | 118 | ```typescript 119 | if (!("RelativeTimeFormat" in Intl)) { 120 | await import("intl-relative-time-format"); 121 | 122 | // or with commonjs: 123 | require("intl-relative-time-format"); 124 | } 125 | ``` 126 | 127 | Alternatively, you can use [Polyfill.app](https://github.com/wessberg/Polyfiller) which uses this polyfill and takes care of only loading the polyfill if needed as well as adding the language features that the polyfill depends on (See [dependencies](#dependencies--browser-support)). 128 | 129 | ## Loading locale data 130 | 131 | By default, no CLDR locale data is loaded. Instead, _you_ decide what data you want. 132 | To load data, you can import it via the `/locale-data` subfolder that comes with the NPM package: 133 | 134 | With ES modules: 135 | 136 | ```typescript 137 | // Load the polyfill 138 | import "intl-relative-time-format"; 139 | 140 | // Load data for the 'en' locale 141 | import "intl-relative-time-format/locale-data/en"; 142 | ``` 143 | 144 | And naturally, it also works with commonjs: 145 | 146 | ```typescript 147 | // Load the polyfill 148 | require("intl-relative-time-format"); 149 | 150 | // Load data for the 'en' locale 151 | require("intl-relative-time-format/locale-data/en"); 152 | ``` 153 | 154 | Remember, if you're also depending on a polyfilled version of `Intl.NumberFormat`, `Intl.getCanonicalLocales`, and/or `Intl.PluralRules`, you will need to import those polyfills beforehand. 155 | 156 | 157 | 158 | ## Usage 159 | 160 | 161 | 162 | The following examples are taken [directly from the original proposal](https://github.com/tc39/proposal-intl-relative-time) 163 | 164 | ### Intl.RelativeTimeFormat.prototype.format 165 | 166 | ```typescript 167 | // Create a relative time formatter in your locale 168 | // with default values explicitly passed in. 169 | const rtf = new Intl.RelativeTimeFormat("en", { 170 | localeMatcher: "best fit", // other values: "lookup" 171 | numeric: "always", // other values: "auto" 172 | style: "long" // other values: "short" or "narrow" 173 | }); 174 | 175 | // Format relative time using negative value (-1). 176 | rtf.format(-1, "day"); 177 | // > "1 day ago" 178 | 179 | // Format relative time using positive value (1). 180 | rtf.format(1, "day"); 181 | // > "in 1 day" 182 | ``` 183 | 184 | ```typescript 185 | // Create a relative time formatter in your locale 186 | // with numeric: "auto" option value passed in. 187 | const rtf = new Intl.RelativeTimeFormat("en", {numeric: "auto"}); 188 | 189 | // Format relative time using negative value (-1). 190 | rtf.format(-1, "day"); 191 | // > "yesterday" 192 | 193 | // Format relative time using positive day unit (1). 194 | rtf.format(1, "day"); 195 | // > "tomorrow" 196 | ``` 197 | 198 | ### Intl.RelativeTimeFormat.prototype.formatToParts 199 | 200 | ```typescript 201 | const rtf = new Intl.RelativeTimeFormat("en", {numeric: "auto"}); 202 | 203 | // Format relative time using the day unit. 204 | rtf.formatToParts(-1, "day"); 205 | // > [{ type: "literal", value: "yesterday"}] 206 | 207 | rtf.formatToParts(100, "day"); 208 | // > [{ type: "literal", value: "in " }, { type: "integer", value: "100", unit: "day" }, { type: "literal", value: " days" }] 209 | ``` 210 | 211 | ### Intl.RelativeTimeFormat.prototype.resolvedOptions 212 | 213 | ```typescript 214 | const rtf = new Intl.RelativeTimeFormat("en", { 215 | numeric: "always", 216 | style: "narrow" 217 | }); 218 | 219 | rtf.resolvedOptions(); 220 | // > [{ locale: "en", numberingSystem: "latn", numeric: "always", style: "narrow"}] 221 | ``` 222 | 223 | ### Intl.RelativeTimeFormat.supportedLocalesOf 224 | 225 | ```typescript 226 | Intl.RelativeTimeFormat.supportedLocalesOf(["foo", "bar", "en-US"]); 227 | // > ["en-US"] 228 | ``` 229 | 230 | ## Dependencies & Browser support 231 | 232 | This polyfill is distributed in ES3-compatible syntax, but is using some additional APIs and language features which must be available: 233 | 234 | - `Array.prototype.includes` 235 | - `Object.create` 236 | - `Object.is` 237 | - `Number.prototype.toLocaleString` 238 | - `String.prototype.includes` 239 | - `String.prototype.replace` 240 | - `Symbol.toStringTag`, 241 | - `WeakMap` 242 | - `Intl.NumberFormat` 243 | - `Intl.PluralRules` 244 | - `Intl.getCanonicalLocales` 245 | 246 | For by far the most browsers, these features will already be natively available. 247 | Generally, I would highly recommend using something like [Polyfill.app](https://github.com/wessberg/Polyfiller) which takes care of this stuff automatically. 248 | 249 | 250 | 251 | ## Contributing 252 | 253 | Do you want to contribute? Awesome! Please follow [these recommendations](./CONTRIBUTING.md). 254 | 255 | 256 | 257 | 258 | 259 | ## Maintainers 260 | 261 | | Frederik Wessberg | 262 | | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | 263 | | [Frederik Wessberg](mailto:frederikwessberg@hotmail.com)
Twitter: [@FredWessberg](https://twitter.com/FredWessberg)
_Lead Developer_ | 264 | 265 | 266 | 267 | 268 | 269 | ## Backers 270 | 271 | ### Patreon 272 | 273 | [Become a backer](https://www.patreon.com/bePatron?u=11315442) and get your name, avatar, and Twitter handle listed here. 274 | 275 | Backers on Patreon 276 | 277 | 278 | 279 | 280 | 281 | ## FAQ 282 | 283 | 284 | 285 | ### What is the default locale? 286 | 287 | The default locale will be equal to the locale file you load first. 288 | 289 | ### Are there any known quirks? 290 | 291 | Nope! 292 | 293 | 294 | 295 | ## License 296 | 297 | MIT © [Frederik Wessberg](mailto:frederikwessberg@hotmail.com) ([@FredWessberg](https://twitter.com/FredWessberg)) ([Website](https://github.com/wessberg)) 298 | 299 | 300 | -------------------------------------------------------------------------------- /src/relative-time-format/relative-time-format/relative-time-format.ts: -------------------------------------------------------------------------------- 1 | import {Locale} from "../../locale/locale"; 2 | import {Locales} from "../../locale/locales"; 3 | import {RelativeTimeFormatOptions} from "./relative-time-format-options"; 4 | import {toObject} from "../../util/to-object"; 5 | import {toString} from "../../util/to-string"; 6 | import {InputLocaleDataEntry} from "../../locale/locale-data"; 7 | import {resolveLocale} from "../resolve-locale/resolve-locale"; 8 | import {supportedLocales} from "../supported-locales/supported-locales"; 9 | import {SupportedLocalesOptions} from "../supported-locales/supported-locales-options"; 10 | import {RelativeTimeUnit} from "../../unit/relative-time-unit"; 11 | import {formatRelativeTime} from "../format-relative-time/format-relative-time"; 12 | import {getInternalSlot, hasInternalSlot, RELATIVE_TIME_FORMAT_STATIC_INTERNALS, setInternalSlot} from "../internal-slot/internal-slot"; 13 | import {IntlPluralRulesConstructor} from "../../intl-object/intl-object"; 14 | import {formatRelativeTimeToParts} from "../format-relative-time-to-parts/format-relative-time-to-parts"; 15 | import {ResolvedRelativeTimeFormatOptions} from "./resolved-relative-time-format-options"; 16 | import {getDefaultLocale, setDefaultLocale} from "../default-locale/get-default-locale"; 17 | import {getOption} from "../../util/get-option"; 18 | import {LOCALE_MATCHER} from "../../locale-matcher/locale-matcher"; 19 | import {STYLE} from "../../style/style"; 20 | import {NUMERIC} from "../../numeric/numeric"; 21 | import {toNumber} from "../../util/to-number"; 22 | import {RelativeTimeFormatPart} from "../../relative-time-format-part/relative-time-format-part"; 23 | 24 | /** 25 | * The RelativeTimeFormat constructor is the %RelativeTimeFormat% intrinsic object and a standard built-in property of the Intl object. 26 | * Behaviour common to all service constructor properties of the Intl object is specified in 9.1. 27 | * 28 | * http://tc39.github.io/proposal-intl-relative-time/#sec-intl-relativetimeformat-constructor 29 | */ 30 | export class RelativeTimeFormat { 31 | // The spec states that the constructor must have a length of 0 and therefore be parameter-less 32 | constructor() { 33 | const locales = arguments[0] as Locale | Locales | undefined; 34 | let options = arguments[1] as Partial; 35 | 36 | // If NewTarget is undefined, throw a TypeError exception. 37 | if (new.target === undefined) { 38 | throw new TypeError(`Constructor Intl.RelativeTimeFormat requires 'new'`); 39 | } 40 | 41 | // The following operations comes from the 'InitializeRelativeFormat' abstract operation (http://tc39.github.io/proposal-intl-relative-time/#sec-InitializeRelativeTimeFormat) 42 | // Let requestedLocales be ? CanonicalizeLocaleList(locales). 43 | const requestedLocales = Intl.getCanonicalLocales(locales); 44 | 45 | // If options is undefined, then (a) Let options be ObjectCreate(null). 46 | // Else (b) Let options be ? ToObject(options). 47 | options = options === undefined ? (Object.create(null) as Partial) : toObject(options); 48 | 49 | // Let opt be a new Record (that doesn't derive from Object.prototype). 50 | const opt = Object.create(null) as RelativeTimeFormatOptions; 51 | 52 | // Let matcher be ? GetOption(options, "localeMatcher", "string", «"lookup", "best fit"», "best fit"). 53 | const matcher = getOption(options, "localeMatcher", "string", LOCALE_MATCHER, "best fit"); 54 | 55 | // Set opt.[[LocaleMatcher]] to matcher. 56 | opt.localeMatcher = matcher; 57 | 58 | // Let localeData be %RelativeTimeFormat%.[[LocaleData]]. 59 | const localeData = RELATIVE_TIME_FORMAT_STATIC_INTERNALS.localeData; 60 | 61 | // Let r be ResolveLocale(%RelativeTimeFormat%.[[AvailableLocales]], requestedLocales, opt, %RelativeTimeFormat%.[[RelevantExtensionKeys]], localeData). 62 | const r = resolveLocale( 63 | RELATIVE_TIME_FORMAT_STATIC_INTERNALS.availableLocales, 64 | requestedLocales, 65 | opt, 66 | RELATIVE_TIME_FORMAT_STATIC_INTERNALS.relevantExtensionKeys, 67 | localeData 68 | ); 69 | 70 | // Let locale be r.[[Locale]]. 71 | const locale = r.locale; 72 | 73 | // Set relativeTimeFormat.[[Locale]] to locale. 74 | setInternalSlot(this, "locale", locale); 75 | 76 | // Set relativeTimeFormat.[[NumberingSystem]] to r_.[[nu]]. 77 | setInternalSlot(this, "numberingSystem", r.nu); 78 | 79 | // Let dataLocale be r.[[DataLocale]]. 80 | const dataLocale = r.dataLocale; 81 | 82 | // Let s be ? GetOption(options, "style", "string", «"long", "short", "narrow"», "long"). 83 | const s = getOption(options, "style", "string", STYLE, "long"); 84 | 85 | // Set relativeTimeFormat.[[Style]] to s. 86 | setInternalSlot(this, "style", s); 87 | 88 | // Let numeric be ? GetOption(options, "numeric", "string", «"always", "auto"», "always"). 89 | const numeric = getOption(options, "numeric", "string", NUMERIC, "always"); 90 | 91 | // Set relativeTimeFormat.[[Numeric]] to numeric. 92 | setInternalSlot(this, "numeric", numeric); 93 | 94 | // Let fields be ! Get(localeData, dataLocale). 95 | const fields = localeData[dataLocale]; 96 | 97 | // Assert: fields is an object (see 1.3.3). 98 | if (!(fields instanceof Object)) { 99 | throw new TypeError(`Expected the LocaleDataEntry for locale: '${dataLocale}' to be an Object`); 100 | } 101 | 102 | // Set relativeTimeFormat.[[Fields]] to fields. 103 | setInternalSlot(this, "fields", fields); 104 | 105 | // Let relativeTimeFormat.[[NumberFormat]] be ! Construct(%NumberFormat%, « locale »). 106 | setInternalSlot(this, "numberFormat", new Intl.NumberFormat(locale)); 107 | 108 | // Let relativeTimeFormat.[[PluralRules]] be ! Construct(%PluralRules%, « locale »). 109 | // tslint:disable-next-line:no-any 110 | setInternalSlot( 111 | this, 112 | "pluralRules", 113 | new ((Intl as unknown) as { 114 | PluralRules: IntlPluralRulesConstructor; 115 | }).PluralRules(locale) 116 | ); 117 | 118 | // Intl.RelativeTimeFormat instances have an [[InitializedRelativeTimeFormat]] internal slot. 119 | setInternalSlot(this, "initializedRelativeTimeFormat", this); 120 | } 121 | 122 | /** 123 | * Returns an array containing those of the provided locales that are supported without having to fall back to the runtime's default locale. 124 | * @param {Locale | Locales} locales 125 | * @return {Locales} 126 | */ 127 | public static supportedLocalesOf(locales: Locale | Locales): Locales { 128 | // The spec states that the 'length' value of supportedLocalesOf must be equal to 1, 129 | // so we have to pull the options argument out of the method signature 130 | const options = arguments[1] as SupportedLocalesOptions | undefined; 131 | 132 | // Let availableLocales be %RelativeTimeFormat%.[[AvailableLocales]]. 133 | const availableLocales = RELATIVE_TIME_FORMAT_STATIC_INTERNALS.availableLocales; 134 | 135 | // Let requestedLocales be ? CanonicalizeLocaleList(locales). 136 | const requestedLocales = Intl.getCanonicalLocales(locales); 137 | return supportedLocales(availableLocales, requestedLocales, options); 138 | } 139 | 140 | /** 141 | * Adds locale data to the internal slot. 142 | * This API exactly mimics that of the Intl polyfill (https://github.com/andyearnshaw/Intl.js) 143 | * @private 144 | * @internal 145 | * @param {InputLocaleDataEntry} data 146 | * @param {Locale} locale 147 | */ 148 | protected static __addLocaleData({data, locale}: InputLocaleDataEntry): void { 149 | // Use the locale as the default one if none is configured 150 | const defaultLocale = getDefaultLocale(); 151 | if (defaultLocale == null) { 152 | setDefaultLocale(locale); 153 | } 154 | 155 | RELATIVE_TIME_FORMAT_STATIC_INTERNALS.localeData[locale] = data; 156 | if (!RELATIVE_TIME_FORMAT_STATIC_INTERNALS.availableLocales.includes(locale)) { 157 | RELATIVE_TIME_FORMAT_STATIC_INTERNALS.availableLocales.push(locale); 158 | } 159 | } 160 | 161 | /** 162 | * Method that formats a value and unit according to the locale and formatting options of this Intl.RelativeTimeFormat object. 163 | * @param {number} value 164 | * @param {RelativeTimeUnit} unit 165 | * @return {string} 166 | */ 167 | public format(value: number, unit: RelativeTimeUnit): string { 168 | // Let relativeTimeFormat be the this value. 169 | const relativeTimeFormat = this; 170 | 171 | // If Type(relativeTimeFormat) is not Object, throw a TypeError exception. 172 | if (!(relativeTimeFormat instanceof Object)) { 173 | throw new TypeError(`Method Intl.RelativeTimeFormat.prototype.format called on incompatible receiver ${this.toString()}`); 174 | } 175 | 176 | // If relativeTimeFormat does not have an [[InitializedRelativeTimeFormat]] internal slot, throw a TypeError exception. 177 | if (!hasInternalSlot(relativeTimeFormat, "initializedRelativeTimeFormat")) { 178 | throw new TypeError(`Method Intl.RelativeTimeFormat.prototype.format called on incompatible receiver ${this.toString()}`); 179 | } 180 | 181 | // Let value be ? ToNumber(value). 182 | value = toNumber(value); 183 | // Let unit be ? ToString(unit). 184 | unit = toString(unit) as RelativeTimeUnit; 185 | 186 | // Return ? FormatRelativeTime(relativeTimeFormat, value, unit). 187 | return formatRelativeTime(relativeTimeFormat, value, unit); 188 | } 189 | 190 | /** 191 | * A version of the 'format' method that returns an array of objects which represent "parts" of the object, 192 | * separating the formatted number into its constituent parts and separating it from other surrounding text 193 | * @param {number} value 194 | * @param {RelativeTimeUnit} unit 195 | * @return {RelativeTimeFormatPart[]} 196 | */ 197 | public formatToParts(value: number, unit: RelativeTimeUnit): RelativeTimeFormatPart[] { 198 | // Let relativeTimeFormat be the this value. 199 | const relativeTimeFormat = this; 200 | 201 | // If Type(relativeTimeFormat) is not Object, throw a TypeError exception. 202 | if (!(relativeTimeFormat instanceof Object)) { 203 | throw new TypeError(`Method Intl.RelativeTimeFormat.prototype.formatToParts called on incompatible receiver ${this.toString()}`); 204 | } 205 | 206 | // If relativeTimeFormat does not have an [[InitializedRelativeTimeFormat]] internal slot, throw a TypeError exception. 207 | if (!hasInternalSlot(relativeTimeFormat, "initializedRelativeTimeFormat")) { 208 | throw new TypeError(`Method Intl.RelativeTimeFormat.prototype.formatToParts called on incompatible receiver ${this.toString()}`); 209 | } 210 | 211 | // Let value be ? ToNumber(value). 212 | value = toNumber(value); 213 | // Let unit be ? ToString(unit). 214 | unit = toString(unit) as RelativeTimeUnit; 215 | 216 | // Return ? FormatRelativeTimeToParts(relativeTimeFormat, value, unit). 217 | return formatRelativeTimeToParts(relativeTimeFormat, value, unit); 218 | } 219 | 220 | /** 221 | * This method provides access to the locale and options computed during initialization of the object. 222 | * @returns {ResolvedRelativeTimeFormatOptions} 223 | */ 224 | public resolvedOptions(): ResolvedRelativeTimeFormatOptions { 225 | // Let relativeTimeFormat be the this value. 226 | const relativeTimeFormat = this; 227 | 228 | // If Type(relativeTimeFormat) is not Object, throw a TypeError exception. 229 | if (!(relativeTimeFormat instanceof Object)) { 230 | throw new TypeError(`Method Intl.RelativeTimeFormat.prototype.resolvedOptions called on incompatible receiver ${this.toString()}`); 231 | } 232 | 233 | // If relativeTimeFormat does not have an [[InitializedRelativeTimeFormat]] internal slot, throw a TypeError exception. 234 | if (!hasInternalSlot(relativeTimeFormat, "initializedRelativeTimeFormat")) { 235 | throw new TypeError(`Method Intl.RelativeTimeFormat.prototype.resolvedOptions called on incompatible receiver ${this.toString()}`); 236 | } 237 | 238 | const locale = getInternalSlot(this, "locale"); 239 | const style = getInternalSlot(this, "style"); 240 | const numeric = getInternalSlot(this, "numeric"); 241 | const numberingSystem = getInternalSlot(this, "numberingSystem"); 242 | 243 | return { 244 | locale, 245 | style, 246 | numeric, 247 | numberingSystem 248 | }; 249 | } 250 | } 251 | 252 | /** 253 | * The initial value of the @@toStringTag property is the string value "Intl.RelativeTimeFormat". 254 | * This property has the attributes { [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]: true }. 255 | * @type {string} 256 | */ 257 | Object.defineProperty(RelativeTimeFormat.prototype, Symbol.toStringTag, { 258 | writable: false, 259 | enumerable: false, 260 | value: "Intl.RelativeTimeFormat", 261 | configurable: true 262 | }); 263 | --------------------------------------------------------------------------------