diff --git a/package.json b/package.json
index 72b4086..665f47c 100644
--- a/package.json
+++ b/package.json
@@ -13,7 +13,8 @@
"test": "vitest",
"package": "bun --run dlz",
"format": "bun --bun prettier --write . --cache",
- "upgrade-examples": "bun scripts/upgrade-examples.ts"
+ "upgrade-examples": "bun scripts/upgrade-examples.ts",
+ "generate-locales": "bun scripts/generate-locales.ts"
},
"dependencies": {
"dayjs": "^1.11.13"
diff --git a/scripts/generate-locales.ts b/scripts/generate-locales.ts
new file mode 100644
index 0000000..89c71ae
--- /dev/null
+++ b/scripts/generate-locales.ts
@@ -0,0 +1,20 @@
+import localeDayjs from "dayjs/locale.json" with { type: "json" };
+import { format } from "prettier";
+
+const locales: string[] = [];
+
+for (const { key, name } of localeDayjs) {
+ if (key === "en") {
+ locales.unshift(`'${key}'`);
+ } else {
+ locales.push(`'${key}'`);
+ }
+}
+
+const localesType = await format(
+ `/** @default "en" */\nexport type Locales = ${locales.join(" | ")};`,
+ { parser: "typescript" },
+);
+Bun.write("src/locales.d.ts", localesType);
+
+export {};
diff --git a/src/Time.svelte b/src/Time.svelte
index a0a2bd8..ce92827 100644
--- a/src/Time.svelte
+++ b/src/Time.svelte
@@ -27,6 +27,12 @@
* @type {boolean | number}
*/
live = false,
+
+ /**
+ * The locale to use for formatting
+ * @type {import("./locales").Locales}
+ */
+ locale = "en",
...rest
} = $props();
@@ -40,7 +46,7 @@
if (relative && live !== false) {
interval = setInterval(
() => {
- formatted = dayjs(timestamp).from();
+ formatted = dayjs(timestamp).locale(locale).from(dayjs());
},
Math.abs(typeof live === "number" ? live : DEFAULT_INTERVAL),
);
@@ -54,11 +60,13 @@
* @type {string}
*/
let formatted = $state(
- relative ? dayjs(timestamp).from() : dayjs(timestamp).format(format),
+ relative
+ ? dayjs(timestamp).locale(locale).from(dayjs())
+ : dayjs(timestamp).locale(locale).format(format),
);
const title = $derived(
- relative ? dayjs(timestamp).format(format) : undefined,
+ relative ? dayjs(timestamp).locale(locale).format(format) : undefined,
);
diff --git a/src/Time.svelte.d.ts b/src/Time.svelte.d.ts
index 1a419d7..49ddfc2 100644
--- a/src/Time.svelte.d.ts
+++ b/src/Time.svelte.d.ts
@@ -1,6 +1,7 @@
import type { ConfigType, OptionType } from "dayjs";
import type { Component } from "svelte";
import type { SvelteHTMLElements } from "svelte/elements";
+import type { Locales } from "./locales";
type RestProps = SvelteHTMLElements["time"];
@@ -33,6 +34,12 @@ export interface TimeProps extends RestProps {
*/
live?: boolean | number;
+ /**
+ * The locale to use for formatting
+ * @default "en"
+ */
+ locale?: Locales;
+
/**
* Formatted timestamp.
* Result of invoking `dayjs().format()`
diff --git a/src/locales.d.ts b/src/locales.d.ts
new file mode 100644
index 0000000..9db3069
--- /dev/null
+++ b/src/locales.d.ts
@@ -0,0 +1,145 @@
+/** @default "en" */
+export type Locales =
+ | "en"
+ | "af"
+ | "am"
+ | "ar-dz"
+ | "ar-iq"
+ | "ar-kw"
+ | "ar-ly"
+ | "ar-ma"
+ | "ar-sa"
+ | "ar-tn"
+ | "ar"
+ | "az"
+ | "be"
+ | "bg"
+ | "bi"
+ | "bm"
+ | "bn-bd"
+ | "bn"
+ | "bo"
+ | "br"
+ | "bs"
+ | "ca"
+ | "cs"
+ | "cv"
+ | "cy"
+ | "de-at"
+ | "da"
+ | "de-ch"
+ | "de"
+ | "dv"
+ | "el"
+ | "en-au"
+ | "en-ca"
+ | "en-gb"
+ | "en-ie"
+ | "en-il"
+ | "en-in"
+ | "en-nz"
+ | "en-sg"
+ | "en-tt"
+ | "eo"
+ | "es-do"
+ | "es-mx"
+ | "es-pr"
+ | "es-us"
+ | "et"
+ | "es"
+ | "eu"
+ | "fa"
+ | "fo"
+ | "fi"
+ | "fr-ca"
+ | "fr-ch"
+ | "fr"
+ | "fy"
+ | "ga"
+ | "gd"
+ | "gom-latn"
+ | "gl"
+ | "gu"
+ | "he"
+ | "hi"
+ | "hr"
+ | "hu"
+ | "ht"
+ | "hy-am"
+ | "id"
+ | "is"
+ | "it-ch"
+ | "it"
+ | "ja"
+ | "jv"
+ | "ka"
+ | "kk"
+ | "km"
+ | "kn"
+ | "ko"
+ | "ku"
+ | "ky"
+ | "lb"
+ | "lo"
+ | "lt"
+ | "lv"
+ | "me"
+ | "mi"
+ | "mk"
+ | "ml"
+ | "mn"
+ | "mr"
+ | "ms-my"
+ | "ms"
+ | "mt"
+ | "my"
+ | "nb"
+ | "ne"
+ | "nl-be"
+ | "nl"
+ | "pl"
+ | "pt-br"
+ | "pt"
+ | "rn"
+ | "ro"
+ | "ru"
+ | "rw"
+ | "sd"
+ | "se"
+ | "si"
+ | "sk"
+ | "sl"
+ | "sq"
+ | "sr-cyrl"
+ | "ss"
+ | "sv-fi"
+ | "sr"
+ | "sv"
+ | "sw"
+ | "ta"
+ | "te"
+ | "tet"
+ | "tg"
+ | "th"
+ | "tk"
+ | "tl-ph"
+ | "tlh"
+ | "tr"
+ | "tzl"
+ | "tzm-latn"
+ | "tzm"
+ | "ug-cn"
+ | "uk"
+ | "ur"
+ | "uz-latn"
+ | "uz"
+ | "vi"
+ | "x-pseudo"
+ | "yo"
+ | "zh-cn"
+ | "zh-hk"
+ | "zh-tw"
+ | "zh"
+ | "oc-lnc"
+ | "nn"
+ | "pa-in";
diff --git a/src/svelte-time.svelte.d.ts b/src/svelte-time.svelte.d.ts
index 52ccc1d..138ace6 100644
--- a/src/svelte-time.svelte.d.ts
+++ b/src/svelte-time.svelte.d.ts
@@ -4,7 +4,7 @@ import type { TimeProps } from "./Time.svelte";
export interface SvelteTimeOptions
extends Pick<
TimeProps,
- "timestamp" | "format" | "relative" | "live" | "title"
+ "timestamp" | "format" | "relative" | "live" | "title" | "locale"
> {}
export const svelteTime: Action<
diff --git a/src/svelte-time.svelte.js b/src/svelte-time.svelte.js
index c1ca845..d8f0b6b 100644
--- a/src/svelte-time.svelte.js
+++ b/src/svelte-time.svelte.js
@@ -17,9 +17,10 @@ export const svelteTime = (node, options = {}) => {
const format = options.format || "MMM DD, YYYY";
const relative = options.relative === true;
const live = options.live ?? false;
+ const locale = options.locale ?? "en";
- let formatted_from = dayjs(timestamp).from();
- let formatted = dayjs(timestamp).format(format);
+ let formatted_from = dayjs(timestamp).locale(locale).from();
+ let formatted = dayjs(timestamp).locale(locale).format(format);
if (relative) {
if ("title" in options) {
@@ -33,7 +34,7 @@ export const svelteTime = (node, options = {}) => {
if (live !== false) {
interval = setInterval(
() => {
- node.innerText = dayjs(timestamp).from();
+ node.innerText = dayjs(timestamp).locale(locale).from();
},
Math.abs(typeof live === "number" ? live : DEFAULT_INTERVAL),
);
diff --git a/tests/SvelteTimeLocale.test.svelte b/tests/SvelteTimeLocale.test.svelte
new file mode 100644
index 0000000..492f1ba
--- /dev/null
+++ b/tests/SvelteTimeLocale.test.svelte
@@ -0,0 +1,117 @@
+
+
+
+
+
+{germanDate}
+
+
+
+
+
+
+
+
+{spanishDate}
+
+
+
+
+
+
+{frenchDate}
+
+
+
+
+
+
+{japaneseDate}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/SvelteTimeLocale.test.ts b/tests/SvelteTimeLocale.test.ts
new file mode 100644
index 0000000..786789d
--- /dev/null
+++ b/tests/SvelteTimeLocale.test.ts
@@ -0,0 +1,160 @@
+import dayjs from "dayjs";
+import "dayjs/locale/de"; // German
+import "dayjs/locale/es"; // Spanish
+import "dayjs/locale/fr"; // French
+import "dayjs/locale/ja"; // Japanese
+import relativeTime from "dayjs/plugin/relativeTime";
+import { mount, tick, unmount } from "svelte";
+import SvelteTimeLocale from "./SvelteTimeLocale.test.svelte";
+
+// Extend dayjs with required plugins
+dayjs.extend(relativeTime);
+
+describe("svelte-time-locale", () => {
+ let instance: null | Record = null;
+ const FIXED_DATE = new Date("2024-01-01T12:00:00.000Z");
+
+ beforeEach(() => {
+ vi.useFakeTimers();
+ vi.setSystemTime(FIXED_DATE);
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ if (instance) {
+ unmount(instance);
+ }
+ instance = null;
+ document.body.innerHTML = "";
+ });
+
+ const getElement = (selector: string) => {
+ return document.querySelector(selector) as HTMLElement;
+ };
+
+ test("handles German locale formatting", async () => {
+ const target = document.body;
+ instance = mount(SvelteTimeLocale, { target });
+
+ // Full format
+ const germanFull = getElement('[data-test="german-full"]');
+ const germanFormatted = getElement('[data-test="german-formatted"]');
+ expect(germanFull.innerHTML).toEqual(germanFormatted.innerHTML);
+
+ // Short format
+ const germanShort = getElement('[data-test="german-short"]');
+ expect(germanShort.innerHTML).toEqual(
+ dayjs(FIXED_DATE).locale("de").format("dd., D. MMM YYYY"),
+ );
+
+ // Month year format
+ const germanMonthYear = getElement('[data-test="german-month-year"]');
+ expect(germanMonthYear.innerHTML).toEqual(
+ dayjs(FIXED_DATE).locale("de").format("MMMM YYYY"),
+ );
+ });
+
+ test("handles Spanish locale formatting", async () => {
+ const target = document.body;
+ instance = mount(SvelteTimeLocale, { target });
+
+ // Full format
+ const spanishFull = getElement('[data-test="spanish-full"]');
+ const spanishFormatted = getElement('[data-test="spanish-formatted"]');
+ expect(spanishFull.innerHTML).toEqual(spanishFormatted.innerHTML);
+
+ // Short format
+ const spanishShort = getElement('[data-test="spanish-short"]');
+ expect(spanishShort.innerHTML).toEqual(
+ dayjs(FIXED_DATE).locale("es").format("dd., D MMM YYYY"),
+ );
+ });
+
+ test("handles French locale formatting", async () => {
+ const target = document.body;
+ instance = mount(SvelteTimeLocale, { target });
+
+ // Full format
+ const frenchFull = getElement('[data-test="french-full"]');
+ const frenchFormatted = getElement('[data-test="french-formatted"]');
+ expect(frenchFull.innerHTML).toEqual(frenchFormatted.innerHTML);
+
+ // Short format
+ const frenchShort = getElement('[data-test="french-short"]');
+ expect(frenchShort.innerHTML).toEqual(
+ dayjs(FIXED_DATE).locale("fr").format("dd. D MMM YYYY"),
+ );
+ });
+
+ test("handles Japanese locale formatting", async () => {
+ const target = document.body;
+ instance = mount(SvelteTimeLocale, { target });
+
+ // Full format
+ const japaneseFull = getElement('[data-test="japanese-full"]');
+ const japaneseFormatted = getElement('[data-test="japanese-formatted"]');
+ expect(japaneseFull.innerHTML).toEqual(japaneseFormatted.innerHTML);
+
+ // Short format
+ const japaneseShort = getElement('[data-test="japanese-short"]');
+ expect(japaneseShort.innerHTML).toEqual(
+ dayjs(FIXED_DATE).locale("ja").format("YYYY年M月D日"),
+ );
+ });
+
+ test("handles relative time in different locales", async () => {
+ const target = document.body;
+ instance = mount(SvelteTimeLocale, { target });
+
+ // German relative time
+ const germanRelative = getElement('[data-test="german-relative"]');
+ expect(germanRelative.innerHTML).toEqual(
+ dayjs(FIXED_DATE).locale("de").fromNow(),
+ );
+
+ // Spanish relative time
+ const spanishRelative = getElement('[data-test="spanish-relative"]');
+ expect(spanishRelative.innerHTML).toEqual(
+ dayjs(FIXED_DATE).locale("es").fromNow(),
+ );
+
+ // French relative time
+ const frenchRelative = getElement('[data-test="french-relative"]');
+ expect(frenchRelative.innerHTML).toEqual(
+ dayjs(FIXED_DATE).locale("fr").fromNow(),
+ );
+
+ // Japanese relative time
+ const japaneseRelative = getElement('[data-test="japanese-relative"]');
+ expect(japaneseRelative.innerHTML).toEqual(
+ dayjs(FIXED_DATE).locale("ja").fromNow(),
+ );
+ });
+
+ test("handles action locale formatting", async () => {
+ const target = document.body;
+ instance = mount(SvelteTimeLocale, { target });
+
+ await tick();
+
+ const actionLocaleEs = getElement('[data-test="action-locale-es"]');
+ expect(actionLocaleEs.innerText).toEqual(
+ dayjs().locale("es").format("MMM DD, YYYY"),
+ );
+
+ const actionLocaleFr = getElement('[data-test="action-locale-fr"]');
+ expect(actionLocaleFr.innerText).toEqual(
+ dayjs().locale("fr").format("MMM DD, YYYY"),
+ );
+
+ const actionLocaleJa = getElement('[data-test="action-locale-ja"]');
+ expect(actionLocaleJa.innerText).toEqual(
+ dayjs().locale("ja").format("MMM DD, YYYY"),
+ );
+
+ const actionLocaleDe = getElement('[data-test="action-locale-de"]');
+ expect(actionLocaleDe.innerText).toEqual(
+ dayjs().locale("de").format("MMM DD, YYYY"),
+ );
+ });
+});