Skip to content

Commit

Permalink
feat: add duration format method
Browse files Browse the repository at this point in the history
  • Loading branch information
jesperorb committed Mar 11, 2024
1 parent b16406e commit c00f397
Show file tree
Hide file tree
Showing 27 changed files with 369 additions and 60 deletions.
48 changes: 47 additions & 1 deletion src/app.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,51 @@ type UmamiAnalytics = {
}

declare interface Window {
umami?: UmamiAnalytics
umami?: UmamiAnalytics
}

declare namespace Intl {
interface DurationFormatOptions {
localeMatcher?: "best fit" | "lookup" | undefined;
style?: "long" | "short" | "narrow" | "digital" | undefined;
years?: "long" | "short" | "narrow" | undefined;
months?: "long" | "short" | "narrow" | undefined;
weeks?: "long" | "short" | "narrow" | undefined;
days?: "long" | "short" | "narrow" | undefined;
hours?: "long" | "short" | "narrow" | undefined;
minutes?: "long" | "short" | "narrow" | undefined;
seconds?: "long" | "short" | "narrow" | undefined;
milliseconds?: "long" | "short" | "narrow" | undefined;
microseconds?: "long" | "short" | "narrow" | undefined;
nanoseconds?: "long" | "short" | "narrow" | undefined;
fractionalDigits?: number;
}

interface ResolvedDurationFormatOptions {
locale: string;
style: "long" | "short" | "narrow" | "digital";
}

interface Duration {
years?: number;
months?: number;
weeks?: number;
days?: number;
hours?: number;
minutes?: number;
seconds?: number;
milliseconds?: number;
microseconds?: number;
nanoseconds?: number;
}

interface DurationFormat {
format(input: Duration): string;
formatToParts(input: Duration): string;
resolvedOptions(): ResolvedDurationFormatOptions;
}
const DurationFormat: {
prototype: DurationFormat;
new(locales?: LocalesArgument, options?: DurationFormatOptions): DurationFormat;
};
}
32 changes: 20 additions & 12 deletions src/lib/components/Sidebar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
import { routes } from '$lib/routes';
import { onDestroy, onMount } from 'svelte';
import { browser } from '$app/environment';
import { getLocaleForSSR } from '$lib/utils/get-locale';
import { selectedLocale } from '$lib/store/selected-locale';
import { getLocaleForSSR } from '$lib/utils/get-locale';
import { selectedLocale } from '$lib/store/selected-locale';
import { testIds } from '$lib/utils/dom-utils';
import Spacing from '$lib/components/ui/Spacing.svelte';
import OpenInNewTab from '$lib/components/ui/icons/OpenInNewTab.svelte';
Expand All @@ -27,12 +27,12 @@
$: getPath($page);
const onClickMenu = (event: MouseEvent) => {
if(isDesktop) return;
if (isDesktop) return;
const eventTarget = event.target as HTMLElement;
const clickedLink = eventTarget.tagName === 'A';
if (!clickedLink) return;
const menu = document.getElementById("sidebar");
if(!menu?.contains(eventTarget)) return;
const menu = document.getElementById('sidebar');
if (!menu?.contains(eventTarget)) return;
open = false;
};
Expand Down Expand Up @@ -61,7 +61,7 @@
});
</script>

<details class="sidebar" id="sidebar" bind:open={open}>
<details class="sidebar" id="sidebar" bind:open>
<summary>
<p class="menu-button">Menu</p>
</summary>
Expand All @@ -86,6 +86,9 @@
href={`/${route.path}?locale=${locale}`}
>
{route.name}
{#if route.experimental}
<img height="16" width="16" src="/icons/experimental.svg" alt="Experimental" />
{/if}
</a>
</li>
<Spacing size={1} />
Expand Down Expand Up @@ -126,7 +129,7 @@
}
@media (min-width: 900px) {
.sidebar {
padding: var(--spacing-10) var(--spacing-6)var(--spacing-6) var(--spacing-6);
padding: var(--spacing-10) var(--spacing-6) var(--spacing-6) var(--spacing-6);
}
summary {
margin-bottom: var(--spacing-4);
Expand All @@ -136,7 +139,7 @@
}
}
summary {
list-style: none;
list-style: none;
}
summary::marker,
summary::-webkit-details-mark {
Expand All @@ -147,12 +150,17 @@
}
@keyframes fadeIn {
0% {opacity: 0; transform: translateX(-10px) }
100% {opacity: 1; transform: translateX(0) }
0% {
opacity: 0;
transform: translateX(-10px);
}
100% {
opacity: 1;
transform: translateX(0);
}
}
details[open] summary ~ * {
animation: fadeIn .3s;
animation: fadeIn 0.3s;
}
</style>
115 changes: 115 additions & 0 deletions src/lib/components/pages/DurationFormat.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<script lang="ts">
import Highlight from '$lib/components/ui/Highlight.svelte';
import OptionSection from '$lib/components/ui/OptionSection.svelte';
import Grid from '$lib/components/ui/Grid.svelte';
import ComboBoxContext from '$lib/components/ui/ComboBox/ComboBoxContext.svelte';
import ComboBox from '$lib/components/ui/ComboBox/ComboBox.svelte';
import Spacing from '$lib/components/ui/Spacing.svelte';
import Input from '$lib/components/ui/Input.svelte';
import { copyToClipboard } from '$lib/utils/copy-to-clipboard';
import { languageByLocaleAsComboBoxOptions } from '$lib/locale-data/locales';
import type { OptionValues } from '$lib/types/OptionValues.types';
import type { BrowserCompatData } from '$lib/types/BrowserSupport.types';
import { selectedLocale } from '$lib/store/selected-locale';
import { trackEvent } from '$lib/utils/analytics';
import {
durationFormatOptions,
} from '$lib/format-options/duration-format.options';
import { clampValue } from '$lib/utils/format-utils';
export let locale: string;
export let browserCompatData: BrowserCompatData | null;
let duration: Record<string, string | number> = {
years: 2,
months: 1,
weeks: '',
days: 46,
hours: '',
minutes: '',
seconds: '',
milliseconds: '',
microseconds: '',
nanoseconds: ''
}
let onClick = async (options: OptionValues) => {
await copyToClipboard(
`new Intl.DurationFormat("${locale}", ${JSON.stringify(options)}).format(${JSON.stringify(duration)})`
);
trackEvent('Copy Code', {
method: 'DurationFormat',
options: JSON.stringify(options),
locale
});
};
const tryFormat = (
options: Intl.DurationFormatOptions | undefined = undefined,
dur: Record<string, number | string>
) => {
try {
return new Intl.DurationFormat(locale, options).format(dur);
} catch (e) {
return 'Failed to use `Intl.DurationFormat`. You are probably using an unsupported browser';
}
};
const onInput = (event: Event) => {
const target = event.target as HTMLInputElement;
const newDuration = {
...duration,
[target.name]: clampValue(
{ name: target.name as any, defaultValue: "", valueType: 'number', inputType: 'text' },
target.value
) as number
};
duration = newDuration;
};
</script>

<h2>Input values</h2>
<Spacing />

<Grid>
{#each Object.keys(duration) as key}
<Input
id={key}
name={key}
label={key}
{onInput}
value={String(duration[key])}
fullWidth
/>
{/each}
</Grid>
<Spacing />

<ComboBoxContext>
<ComboBox
label="Locale"
name="locale"
bind:value={$selectedLocale}
options={languageByLocaleAsComboBoxOptions}
/>
</ComboBoxContext>
<Spacing />
<h2>Output</h2>
<Spacing />
<Grid>
{#each Object.entries(durationFormatOptions) as [option, values]}
<OptionSection header={option} {browserCompatData} stackedCompatView>
{#each values as value}
{#if value !== undefined}
<Spacing size={1} />
<Highlight
{onClick}
values={{ [option]: value }}
output={tryFormat({ [option]: value }, duration)}
/>
{/if}
{/each}
</OptionSection>
{/each}
</Grid>
3 changes: 3 additions & 0 deletions src/lib/components/ui/Header.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
<header>
<h1>
{formatHeader(header)}
{#if header === "DurationFormat" || link === "DurationFormat"}
<img height="22" width="22" src="/icons/experimental.svg" alt="Experimental" />
{/if}
</h1>
<div>
<div class="wrapper">
Expand Down
6 changes: 4 additions & 2 deletions src/lib/format-methods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ export const formatMethods = [
'PluralRules',
'Collator',
'Segmenter',
'DisplayNames'
'DisplayNames',
'DurationFormat',
] as const;

export type FormatMethodsKeys =
Expand All @@ -17,4 +18,5 @@ export type FormatMethodsKeys =
| 'PluralRules'
| 'Collator'
| 'Segmenter'
| 'DisplayNames';
| 'DisplayNames'
| 'DurationFormat';
4 changes: 2 additions & 2 deletions src/lib/format-options/collator.options.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { localeMatcher } from './locale-matcher';
import { localeMatcher } from "./common.options";

export const collatorFormatOptions = {
usage: ['sort', 'search', undefined],
numeric: [true, false, undefined],
caseFirst: ['upper', 'lower', 'false', undefined],
sensitivity: ['base', 'accent', 'case', 'variant', undefined],
ignorePunctuation: [true, false, undefined],
...localeMatcher
localeMatcher
} as const;
2 changes: 2 additions & 0 deletions src/lib/format-options/common.options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const style = [undefined, 'long', 'short', 'narrow'] as const;
export const localeMatcher = [undefined, 'best fit', 'lookup'] as const;
12 changes: 6 additions & 6 deletions src/lib/format-options/datetime-format.options.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
import { calendars, numberingSystems } from '$lib/locale-data/calendars';
import type { AllFormatOptionsKeys } from '$lib/format-options/types';
import { localeMatcher } from './locale-matcher';
import { style, localeMatcher } from './common.options';

export const datetimeFormatOptions = {
dateStyle: ['full', 'long', 'medium', 'short', undefined],
timeStyle: ['full', 'long', 'medium', 'short', undefined],
year: ['numeric', '2-digit', undefined],
month: ['numeric', '2-digit', 'long', 'short', 'narrow', undefined],
month: ['numeric', '2-digit', ...style],
day: ['numeric', '2-digit', undefined],
hour: ['numeric', '2-digit', undefined],
minute: ['numeric', '2-digit', undefined],
second: ['numeric', '2-digit', undefined],
weekday: ['long', 'short', 'narrow', undefined],
era: ['long', 'short', 'narrow', undefined],
weekday: style,
era: style,
hour12: [true, false, undefined],
hourCycle: ['h11', 'h12', 'h23', 'h24', undefined],
dayPeriod: ['narrow', 'short', 'long', undefined],
dayPeriod: style,
fractionalSecondDigits: [1, 2, 3, undefined],
calendar: [...calendars, undefined],
numberingSystem: [...numberingSystems, undefined],
timeZoneName: ['long', 'short', undefined],
formatMatcher: ['best fit', 'basic', undefined],
...localeMatcher
localeMatcher
} as const;

export const getDateTimeFormatOptions = (
Expand Down
7 changes: 4 additions & 3 deletions src/lib/format-options/display-names.options.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { localeMatcher } from './locale-matcher';

import { style, localeMatcher } from './common.options';

export const displayNamesOptions = {
style: ['long', 'short', 'narrow', undefined],
style,
type: ['language', 'region', 'script', 'calendar', 'dateTimeField', 'currency', undefined],
languageDisplay: ['dialect', 'standard', undefined],
fallback: ['code', 'none', undefined],
...localeMatcher
localeMatcher
} as const;
31 changes: 31 additions & 0 deletions src/lib/format-options/duration-format.options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { defaultNumberRange } from '$lib/utils/format-utils';
import { style, localeMatcher } from './common.options';

export const durationFormatOptions = {
style,
years: style,
months: style,
weeks: style,
days: style,
hours: style,
minutes: style,
seconds: style,
milliseconds: style,
microseconds: style,
nanoseconds: style,
fractionalDigits: [...defaultNumberRange, undefined],
localeMatcher
} as const;

export const durationValues: (keyof Intl.Duration)[] = [
"years",
"months",
"weeks",
"days",
"hours",
"minutes",
"seconds",
"milliseconds",
"microseconds",
"nanoseconds"
];
4 changes: 3 additions & 1 deletion src/lib/format-options/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { collatorFormatOptions } from './collator.options';
import { datetimeFormatOptions } from './datetime-format.options';
import { displayNamesOptions } from './display-names.options';
import { durationFormatOptions } from './duration-format.options';
import { listFormatOptions } from './list-format.options';
import {
numberFormatOptionsCommon,
Expand All @@ -23,5 +24,6 @@ export const formatOptions = {
PluralRules: pluralRulesFormatOptions,
Collator: collatorFormatOptions,
Segmenter: segmenterOptions,
DisplayNames: displayNamesOptions
DisplayNames: displayNamesOptions,
DurationFormat: durationFormatOptions
};
Loading

0 comments on commit c00f397

Please sign in to comment.