diff --git a/jest.config.js b/jest.config.js index 293eadd7a..d360a0ba7 100644 --- a/jest.config.js +++ b/jest.config.js @@ -4,8 +4,8 @@ const config = require("@patternslib/dev/jest.config.js"); // config.setupFilesAfterEnv.push("./node_modules/@testing-library/jest-dom/extend-expect"); config.setupFilesAfterEnv.push(path.resolve(__dirname, "./src/setup-tests.js")); config.transformIgnorePatterns = [ - "/node_modules/(?!.pnpm/)(?!@patternslib/)(?!@plone/)(?!preact/)(?!screenfull/)(?!sinon/)(?!bootstrap/)(?!datatable/)(?!svelte/)(?!esm-env/).+\\.[t|j]sx?$", - "/node_modules/.pnpm/(?!@patternslib)(?!@plone)(?!preact)(?!screenfull)(?!sinon)(?!bootstrap)(?!datatable)(?!svelte)(?!esm-env)", + "/node_modules/(?!.pnpm/)(?!@patternslib/)(?!@plone/)(?!@formatjs/)(?!preact/)(?!screenfull/)(?!sinon/)(?!bootstrap/)(?!datatable/)(?!svelte/)(?!esm-env/).+\\.[t|j]sx?$", + "/node_modules/.pnpm/(?!@patternslib)(?!@plone)(?!@formatjs)(?!preact)(?!screenfull)(?!sinon)(?!bootstrap)(?!datatable)(?!svelte)(?!esm-env)", ]; // Transforms. Order matters: Jest uses the first matching pattern, so the diff --git a/package.json b/package.json index 1d2497107..d6cb8cf7b 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "packageManager": "pnpm@11.5.2+sha512.71c631e382066efc25625d5cf029075de07b61b37f6e27350fbd84b1bda5864c8c1967adc280776b45c30a715c0359a3be08fef42d5bb09e2b99029979692916", "dependencies": { "@11ty/eleventy-upgrade-help": "3.0.2", + "@formatjs/intl-datetimeformat": "^7.4.9", "@patternslib/pat-code-editor": "4.0.1", "@patternslib/patternslib": "9.10.6", "@plone/registry": "^2.7.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6356c40f4..9aa4def51 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,6 +15,9 @@ importers: '@11ty/eleventy-upgrade-help': specifier: 3.0.2 version: 3.0.2(posthtml@0.16.7) + '@formatjs/intl-datetimeformat': + specifier: ^7.4.9 + version: 7.4.9 '@patternslib/pat-code-editor': specifier: 4.0.1 version: 4.0.1 @@ -159,7 +162,7 @@ importers: version: 3.2.4(svelte@5.56.3(@typescript-eslint/types@8.57.2)) svelte-preprocess: specifier: ^6.0.5 - version: 6.0.5(@babel/core@7.29.7)(postcss@8.5.15)(sass@1.100.0)(svelte@5.56.3(@typescript-eslint/types@8.57.2))(typescript@6.0.3) + version: 6.0.5(@babel/core@7.29.7)(postcss@8.5.15)(sass@1.101.0)(svelte@5.56.3(@typescript-eslint/types@8.57.2))(typescript@6.0.3) svelte-scrollto: specifier: ^0.2.0 version: 0.2.0 @@ -990,6 +993,18 @@ packages: resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} + '@formatjs/bigdecimal@0.2.6': + resolution: {integrity: sha512-aPzKsGQOkQRHUEbyO/ZtYfr4EqaBQnSs6U4tzTla1xBnIdEHgY2GqEqso28UMwWRkzKqqTj5+/6BmuOsRkfn2A==} + + '@formatjs/fast-memoize@3.1.6': + resolution: {integrity: sha512-H5aexk1Le7T9TPmscacZ+1pR6CTa2n1wq+HDVGXhH8TzUlQQpeXzZs91dRtmFHrbeNbjPFPfQujUqm7MHgVoXQ==} + + '@formatjs/intl-datetimeformat@7.4.9': + resolution: {integrity: sha512-dpGGhYrAq+3X0ME9V161qCgQcaY1IGhKUR6y58mep53qd3sg86ai+WkjlLbP0Hr2TFpPU9Z+i4oCxh1RUvW6mA==} + + '@formatjs/intl-localematcher@0.8.10': + resolution: {integrity: sha512-P/IC3qws3jH+1fEs+o0RIFgXKRaQlFehjS5W0FPAqdo6hgzawLl+eD0q0JjheQ3XtoOe5n8WSYfX06KQZI/QJA==} + '@fullcalendar/adaptive@5.11.5': resolution: {integrity: sha512-yGXS7u1EOKyNdNuwepDEgh2e52jaxYu9D8A4ptdKEPtBMqdStShMp/4NNN8QEJJr50a5qdJTZRA8q1HAdq2USQ==} @@ -2084,7 +2099,7 @@ packages: resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} Select2@https://codeload.github.com/ivaynberg/select2/tar.gz/95a977f674b6938af55ec5f28b7772df93786c5c: - resolution: {gitHosted: true, tarball: https://codeload.github.com/ivaynberg/select2/tar.gz/95a977f674b6938af55ec5f28b7772df93786c5c} + resolution: {gitHosted: true, integrity: sha512-42On0yUzjhE/vNTVRT/+3wUwIpKyUGEW31PA1gFRv0j+OjgXJgo4aJVWCfjEpMa+O34PYKCfEdUN0OWaAEQ/6Q==, tarball: https://codeload.github.com/ivaynberg/select2/tar.gz/95a977f674b6938af55ec5f28b7772df93786c5c} version: 3.5.4 a-sync-waterfall@1.0.1: @@ -4907,8 +4922,8 @@ packages: webpack: optional: true - sass@1.100.0: - resolution: {integrity: sha512-B5j0rYMlinhhOo9tjQebMVVn0TfyXAF+wB3b2ggZUuJ/is/Y+7+JGjirAMxHZ9Z3hIP98NPfamlAkBHa1lAaXQ==} + sass@1.101.0: + resolution: {integrity: sha512-OL3GoQyoUdDt843DpVmDO6y2k1sc5IhUDSpu8XucEI+35neq5QivZ1iuegnpraEVTJXlQGK1gl27zKcTLEPbQw==} engines: {node: '>=20.19.0'} hasBin: true @@ -6900,6 +6915,19 @@ snapshots: '@eslint/core': 1.2.1 levn: 0.4.1 + '@formatjs/bigdecimal@0.2.6': {} + + '@formatjs/fast-memoize@3.1.6': {} + + '@formatjs/intl-datetimeformat@7.4.9': + dependencies: + '@formatjs/bigdecimal': 0.2.6 + '@formatjs/intl-localematcher': 0.8.10 + + '@formatjs/intl-localematcher@0.8.10': + dependencies: + '@formatjs/fast-memoize': 3.1.6 + '@fullcalendar/adaptive@5.11.5': dependencies: '@fullcalendar/common': 5.11.5 @@ -7648,8 +7676,8 @@ snapshots: prettier: 3.8.3 regenerator-runtime: 0.14.1 release-it: 20.2.0(@types/node@25.9.2) - sass: 1.100.0 - sass-loader: 16.0.8(sass@1.100.0)(webpack@5.107.2(postcss@8.5.15)) + sass: 1.101.0 + sass-loader: 16.0.8(sass@1.101.0)(webpack@5.107.2(postcss@8.5.15)) style-loader: 4.0.0(webpack@5.107.2(postcss@8.5.15)) terser-webpack-plugin: 5.6.1(postcss@8.5.15)(webpack@5.107.2(postcss@8.5.15)) timezone-mock: 1.4.2 @@ -11235,14 +11263,14 @@ snapshots: safer-buffer@2.1.2: {} - sass-loader@16.0.8(sass@1.100.0)(webpack@5.107.2(postcss@8.5.15)): + sass-loader@16.0.8(sass@1.101.0)(webpack@5.107.2(postcss@8.5.15)): dependencies: neo-async: 2.6.2 optionalDependencies: - sass: 1.100.0 + sass: 1.101.0 webpack: 5.107.2(postcss@8.5.15)(webpack-cli@7.0.3) - sass@1.100.0: + sass@1.101.0: dependencies: chokidar: 5.0.0 immutable: 5.1.6 @@ -11621,13 +11649,13 @@ snapshots: svelte-dev-helper: 1.1.9 svelte-hmr: 0.14.12(svelte@5.56.3(@typescript-eslint/types@8.57.2)) - svelte-preprocess@6.0.5(@babel/core@7.29.7)(postcss@8.5.15)(sass@1.100.0)(svelte@5.56.3(@typescript-eslint/types@8.57.2))(typescript@6.0.3): + svelte-preprocess@6.0.5(@babel/core@7.29.7)(postcss@8.5.15)(sass@1.101.0)(svelte@5.56.3(@typescript-eslint/types@8.57.2))(typescript@6.0.3): dependencies: svelte: 5.56.3(@typescript-eslint/types@8.57.2) optionalDependencies: '@babel/core': 7.29.7 postcss: 8.5.15 - sass: 1.100.0 + sass: 1.101.0 typescript: 6.0.3 svelte-scrollto@0.2.0: diff --git a/src/core/intl-loader.js b/src/core/intl-loader.js new file mode 100644 index 000000000..12277c1df --- /dev/null +++ b/src/core/intl-loader.js @@ -0,0 +1,49 @@ +/** + * Global Intl polyfill loader for Plone Mockup. + * Detects if the current browser supports the site's language and lazily + * loads the required polyfills and locale data if not. + */ + +export async function ensureIntlSupport(lang) { + if (!lang) return; + const normalizedLang = lang.replace("_", "-"); + const baseLang = normalizedLang.split("-")[0]; + + // Check if natively supported + try { + if ( + typeof Intl !== "undefined" && + Intl.DateTimeFormat && + Intl.DateTimeFormat.supportedLocalesOf(normalizedLang).length > 0 + ) { + return; + } + } catch { + // Fall through to loading polyfill if supportedLocalesOf fails + } + + console.info(`Locale "${normalizedLang}" not supported. Loading polyfill...`); + + // Load polyfill core if native support is missing for this locale. + try { + // Use polyfill-force to ensure we get a version that supports adding locale data, + // as native versions might not have the hooks for the locale-data files. + await import("@formatjs/intl-datetimeformat/polyfill-force.js"); + } catch (e) { + console.error("Failed to load Intl polyfill core", e); + } + + // Load specific locale data via Webpack dynamic chunk. + try { + // Use the package name with explicit .js extension. + // We have an alias in webpack.config.js to help resolve this path correctly + // without triggering package export warnings in Webpack 5. + await import(`@formatjs/intl-datetimeformat/locale-data/${baseLang}.js`); + + if (Intl.DateTimeFormat.supportedLocalesOf(normalizedLang).length > 0) { + console.info(`Locale "${normalizedLang}" is now supported.`); + } + } catch (e) { + console.warn(`Could not load Intl data for ${baseLang}`, e); + } +} diff --git a/src/core/intl-loader.test.js b/src/core/intl-loader.test.js new file mode 100644 index 000000000..03571f8c3 --- /dev/null +++ b/src/core/intl-loader.test.js @@ -0,0 +1,70 @@ +import { ensureIntlSupport } from "./intl-loader"; + +describe("intl-loader", () => { + let originalDateTimeFormat; + + beforeAll(() => { + originalDateTimeFormat = Intl.DateTimeFormat; + jest.spyOn(console, "info").mockImplementation(() => {}); + jest.spyOn(console, "warn").mockImplementation(() => {}); + jest.spyOn(console, "error").mockImplementation(() => {}); + }); + + afterAll(() => { + Intl.DateTimeFormat = originalDateTimeFormat; + console.info.mockRestore(); + console.warn.mockRestore(); + console.error.mockRestore(); + }); + + beforeEach(() => { + jest.clearAllMocks(); + // Reset to original before each test to have a clean state + // Note: some polyfill effects might persist if they touch other global objects + Intl.DateTimeFormat = originalDateTimeFormat; + }); + + it("should detect supported locales correctly", async () => { + // 'en' should be supported + await ensureIntlSupport("en"); + expect(console.info).not.toHaveBeenCalledWith(expect.stringContaining("not supported")); + }); + + it("should handle locale normalization", async () => { + await ensureIntlSupport("pt_BR"); + // pt-BR is likely supported, but we just check it doesn't crash + }); + + it("should load polyfill and provide Basque (eu) formatting", async () => { + // We force a mock that says 'eu' is NOT supported + const mockSupportedLocalesOf = jest.fn().mockImplementation((locales) => { + const l = Array.isArray(locales) ? locales[0] : locales; + if (l.startsWith('eu')) return []; + return originalDateTimeFormat.supportedLocalesOf(locales); + }); + + // We need to mock the property because it might be a getter + Object.defineProperty(Intl, 'DateTimeFormat', { + value: class extends originalDateTimeFormat { + static supportedLocalesOf = mockSupportedLocalesOf; + }, + configurable: true + }); + + await ensureIntlSupport("eu"); + + expect(mockSupportedLocalesOf).toHaveBeenCalled(); + expect(console.info).toHaveBeenCalledWith(expect.stringContaining("not supported")); + + // After ensureIntlSupport, Intl.DateTimeFormat should have been replaced by the polyfill + // since we used polyfill-force.js (or at least it was called). + + // Verify formatting using UTC to avoid timezone shifts + const date = new Date(Date.UTC(2020, 5, 1)); // June 1st UTC + const dtf = new Intl.DateTimeFormat("eu", { month: "short", timeZone: "UTC" }); + const formatted = dtf.format(date); + + // Basque short month for June is 'eka.' + expect(formatted.toLowerCase()).toContain("eka"); + }); +}); diff --git a/src/pat/contentbrowser/src/ContentBrowser.svelte b/src/pat/contentbrowser/src/ContentBrowser.svelte index 2fbaa8ed1..e39048f75 100644 --- a/src/pat/contentbrowser/src/ContentBrowser.svelte +++ b/src/pat/contentbrowser/src/ContentBrowser.svelte @@ -1,9 +1,10 @@