Skip to content

Commit a872ede

Browse files
authored
Merge branch 'master' into mockup-lazy-recurrence
2 parents cf0e420 + 33b91d0 commit a872ede

11 files changed

Lines changed: 213 additions & 55 deletions

File tree

jest.config.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ const config = require("@patternslib/dev/jest.config.js");
44
// config.setupFilesAfterEnv.push("./node_modules/@testing-library/jest-dom/extend-expect");
55
config.setupFilesAfterEnv.push(path.resolve(__dirname, "./src/setup-tests.js"));
66
config.transformIgnorePatterns = [
7-
"/node_modules/(?!.pnpm/)(?!@patternslib/)(?!@plone/)(?!preact/)(?!screenfull/)(?!sinon/)(?!bootstrap/)(?!datatable/)(?!svelte/)(?!esm-env/).+\\.[t|j]sx?$",
8-
"/node_modules/.pnpm/(?!@patternslib)(?!@plone)(?!preact)(?!screenfull)(?!sinon)(?!bootstrap)(?!datatable)(?!svelte)(?!esm-env)",
7+
"/node_modules/(?!.pnpm/)(?!@patternslib/)(?!@plone/)(?!@formatjs/)(?!preact/)(?!screenfull/)(?!sinon/)(?!bootstrap/)(?!datatable/)(?!svelte/)(?!esm-env/).+\\.[t|j]sx?$",
8+
"/node_modules/.pnpm/(?!@patternslib)(?!@plone)(?!@formatjs)(?!preact)(?!screenfull)(?!sinon)(?!bootstrap)(?!datatable)(?!svelte)(?!esm-env)",
99
];
1010

1111
// Transforms. Order matters: Jest uses the first matching pattern, so the

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"packageManager": "pnpm@11.5.2+sha512.71c631e382066efc25625d5cf029075de07b61b37f6e27350fbd84b1bda5864c8c1967adc280776b45c30a715c0359a3be08fef42d5bb09e2b99029979692916",
1313
"dependencies": {
1414
"@11ty/eleventy-upgrade-help": "3.0.2",
15+
"@formatjs/intl-datetimeformat": "^7.4.9",
1516
"@patternslib/pat-code-editor": "4.0.1",
1617
"@patternslib/patternslib": "9.10.6",
1718
"@plone/registry": "^2.7.2",

pnpm-lock.yaml

Lines changed: 39 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/core/intl-loader.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* Global Intl polyfill loader for Plone Mockup.
3+
* Detects if the current browser supports the site's language and lazily
4+
* loads the required polyfills and locale data if not.
5+
*/
6+
7+
export async function ensureIntlSupport(lang) {
8+
if (!lang) return;
9+
const normalizedLang = lang.replace("_", "-");
10+
const baseLang = normalizedLang.split("-")[0];
11+
12+
// Check if natively supported
13+
try {
14+
if (
15+
typeof Intl !== "undefined" &&
16+
Intl.DateTimeFormat &&
17+
Intl.DateTimeFormat.supportedLocalesOf(normalizedLang).length > 0
18+
) {
19+
return;
20+
}
21+
} catch {
22+
// Fall through to loading polyfill if supportedLocalesOf fails
23+
}
24+
25+
console.info(`Locale "${normalizedLang}" not supported. Loading polyfill...`);
26+
27+
// Load polyfill core if native support is missing for this locale.
28+
try {
29+
// Use polyfill-force to ensure we get a version that supports adding locale data,
30+
// as native versions might not have the hooks for the locale-data files.
31+
await import("@formatjs/intl-datetimeformat/polyfill-force.js");
32+
} catch (e) {
33+
console.error("Failed to load Intl polyfill core", e);
34+
}
35+
36+
// Load specific locale data via Webpack dynamic chunk.
37+
try {
38+
// Use the package name with explicit .js extension.
39+
// We have an alias in webpack.config.js to help resolve this path correctly
40+
// without triggering package export warnings in Webpack 5.
41+
await import(`@formatjs/intl-datetimeformat/locale-data/${baseLang}.js`);
42+
43+
if (Intl.DateTimeFormat.supportedLocalesOf(normalizedLang).length > 0) {
44+
console.info(`Locale "${normalizedLang}" is now supported.`);
45+
}
46+
} catch (e) {
47+
console.warn(`Could not load Intl data for ${baseLang}`, e);
48+
}
49+
}

src/core/intl-loader.test.js

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { ensureIntlSupport } from "./intl-loader";
2+
3+
describe("intl-loader", () => {
4+
let originalDateTimeFormat;
5+
6+
beforeAll(() => {
7+
originalDateTimeFormat = Intl.DateTimeFormat;
8+
jest.spyOn(console, "info").mockImplementation(() => {});
9+
jest.spyOn(console, "warn").mockImplementation(() => {});
10+
jest.spyOn(console, "error").mockImplementation(() => {});
11+
});
12+
13+
afterAll(() => {
14+
Intl.DateTimeFormat = originalDateTimeFormat;
15+
console.info.mockRestore();
16+
console.warn.mockRestore();
17+
console.error.mockRestore();
18+
});
19+
20+
beforeEach(() => {
21+
jest.clearAllMocks();
22+
// Reset to original before each test to have a clean state
23+
// Note: some polyfill effects might persist if they touch other global objects
24+
Intl.DateTimeFormat = originalDateTimeFormat;
25+
});
26+
27+
it("should detect supported locales correctly", async () => {
28+
// 'en' should be supported
29+
await ensureIntlSupport("en");
30+
expect(console.info).not.toHaveBeenCalledWith(expect.stringContaining("not supported"));
31+
});
32+
33+
it("should handle locale normalization", async () => {
34+
await ensureIntlSupport("pt_BR");
35+
// pt-BR is likely supported, but we just check it doesn't crash
36+
});
37+
38+
it("should load polyfill and provide Basque (eu) formatting", async () => {
39+
// We force a mock that says 'eu' is NOT supported
40+
const mockSupportedLocalesOf = jest.fn().mockImplementation((locales) => {
41+
const l = Array.isArray(locales) ? locales[0] : locales;
42+
if (l.startsWith('eu')) return [];
43+
return originalDateTimeFormat.supportedLocalesOf(locales);
44+
});
45+
46+
// We need to mock the property because it might be a getter
47+
Object.defineProperty(Intl, 'DateTimeFormat', {
48+
value: class extends originalDateTimeFormat {
49+
static supportedLocalesOf = mockSupportedLocalesOf;
50+
},
51+
configurable: true
52+
});
53+
54+
await ensureIntlSupport("eu");
55+
56+
expect(mockSupportedLocalesOf).toHaveBeenCalled();
57+
expect(console.info).toHaveBeenCalledWith(expect.stringContaining("not supported"));
58+
59+
// After ensureIntlSupport, Intl.DateTimeFormat should have been replaced by the polyfill
60+
// since we used polyfill-force.js (or at least it was called).
61+
62+
// Verify formatting using UTC to avoid timezone shifts
63+
const date = new Date(Date.UTC(2020, 5, 1)); // June 1st UTC
64+
const dtf = new Intl.DateTimeFormat("eu", { month: "short", timeZone: "UTC" });
65+
const formatted = dtf.format(date);
66+
67+
// Basque short month for June is 'eka.'
68+
expect(formatted.toLowerCase()).toContain("eka");
69+
});
70+
});

src/pat/contentbrowser/src/ContentBrowser.svelte

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
<script>
22
import utils from "@patternslib/patternslib/src/core/utils";
3-
import { getContext } from "svelte";
3+
import { getContext, onMount } from "svelte";
44
import * as animateScroll from "svelte-scrollto";
55
import { fly } from "svelte/transition";
66
import _t from "../../../core/i18n-wrapper";
7+
import { ensureIntlSupport } from "../../../core/intl-loader";
78
import Upload from "../../upload/upload";
89
import contentStore from "./ContentStore";
910
import {
@@ -472,6 +473,11 @@
472473
};
473474
}
474475
476+
onMount(async () => {
477+
const lang = document.documentElement.lang || "en";
478+
await ensureIntlSupport(lang);
479+
});
480+
475481
$effect(() => {
476482
if ($showContentBrowser) {
477483
contentItems.get({ path: $currentPath });

src/pat/filemanager/src/App.svelte

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
<script>
22
import { onMount, setContext } from "svelte";
33
import logger from "@patternslib/patternslib/src/core/logging";
4+
import { ensureIntlSupport } from "../../../core/intl-loader";
5+
import { getLang } from "./utils/format.ts";
46
import { ConfigStore } from "./stores/ConfigStore.svelte.ts";
57
import { ContentsStore } from "./stores/ContentsStore.svelte.ts";
68
import { ColumnsStore } from "./stores/ColumnsStore.svelte.ts";
@@ -118,6 +120,14 @@
118120
119121
let isRestoringHistory = false;
120122
123+
// Lazily load Intl polyfills for the site language. Kept in its own
124+
// async onMount so the listener-owning onMount below can stay synchronous
125+
// and return its cleanup function (an async onMount returns a Promise,
126+
// which Svelte ignores, leaking the listener).
127+
onMount(async () => {
128+
await ensureIntlSupport(getLang());
129+
});
130+
121131
onMount(() => {
122132
contents.load();
123133

src/pat/filemanager/src/utils/format.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,27 @@ function parseDate(value: unknown): Date | null {
1111
}
1212

1313
/** Detect the current UI language from the <html> tag, normalized for Intl. */
14-
function getLang(): string {
14+
export function getLang(): string {
1515
if (typeof document === "undefined") return "en";
1616
return (document.documentElement.lang || "en").replace("_", "-");
1717
}
1818

1919
export function formatDate(value: unknown): string {
2020
const date = parseDate(value);
2121
if (!date) return "";
22-
return new Intl.DateTimeFormat(getLang(), {
23-
year: "numeric",
24-
month: "short",
25-
day: "numeric",
26-
hour: "2-digit",
27-
minute: "2-digit",
28-
}).format(date);
22+
const lang = getLang();
23+
try {
24+
return new Intl.DateTimeFormat(lang, {
25+
year: "numeric",
26+
month: "short",
27+
day: "numeric",
28+
hour: "2-digit",
29+
minute: "2-digit",
30+
}).format(date);
31+
} catch (e) {
32+
console.error(`Error formatting date for locale "${lang}":`, e);
33+
return date.toLocaleString(); // Fallback
34+
}
2935
}
3036

3137
/**

0 commit comments

Comments
 (0)