Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
50 changes: 39 additions & 11 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

49 changes: 49 additions & 0 deletions src/core/intl-loader.js
Original file line number Diff line number Diff line change
@@ -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);
}
}
70 changes: 70 additions & 0 deletions src/core/intl-loader.test.js
Original file line number Diff line number Diff line change
@@ -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");
});
});
8 changes: 7 additions & 1 deletion src/pat/contentbrowser/src/ContentBrowser.svelte
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
<script>
import utils from "@patternslib/patternslib/src/core/utils";
import { getContext } from "svelte";
import { getContext, onMount } from "svelte";
import * as animateScroll from "svelte-scrollto";
import { fly } from "svelte/transition";
import _t from "../../../core/i18n-wrapper";
import { ensureIntlSupport } from "../../../core/intl-loader";
import Upload from "../../upload/upload";
import contentStore from "./ContentStore";
import {
Expand Down Expand Up @@ -472,6 +473,11 @@
};
}

onMount(async () => {
const lang = document.documentElement.lang || "en";
await ensureIntlSupport(lang);
});

$effect(() => {
if ($showContentBrowser) {
contentItems.get({ path: $currentPath });
Expand Down
10 changes: 10 additions & 0 deletions src/pat/filemanager/src/App.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<script>
import { onMount, setContext } from "svelte";
import logger from "@patternslib/patternslib/src/core/logging";
import { ensureIntlSupport } from "../../../core/intl-loader";
import { getLang } from "./utils/format.ts";
import { ConfigStore } from "./stores/ConfigStore.svelte.ts";
import { ContentsStore } from "./stores/ContentsStore.svelte.ts";
import { ColumnsStore } from "./stores/ColumnsStore.svelte.ts";
Expand Down Expand Up @@ -118,6 +120,14 @@

let isRestoringHistory = false;

// Lazily load Intl polyfills for the site language. Kept in its own
// async onMount so the listener-owning onMount below can stay synchronous
// and return its cleanup function (an async onMount returns a Promise,
// which Svelte ignores, leaking the listener).
onMount(async () => {
Comment thread
MrTango marked this conversation as resolved.
await ensureIntlSupport(getLang());
});

onMount(() => {
contents.load();

Expand Down
22 changes: 14 additions & 8 deletions src/pat/filemanager/src/utils/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,27 @@ function parseDate(value: unknown): Date | null {
}

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

export function formatDate(value: unknown): string {
const date = parseDate(value);
if (!date) return "";
return new Intl.DateTimeFormat(getLang(), {
year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
}).format(date);
const lang = getLang();
try {
return new Intl.DateTimeFormat(lang, {
year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
}).format(date);
} catch (e) {
console.error(`Error formatting date for locale "${lang}":`, e);
return date.toLocaleString(); // Fallback
}
}

/**
Expand Down
3 changes: 3 additions & 0 deletions src/pat/structure/structure.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Base from "@patternslib/patternslib/src/core/base";
import { ensureIntlSupport } from "../../core/intl-loader";

export default Base.extend({
name: "structure",
Expand Down Expand Up @@ -151,6 +152,8 @@ export default Base.extend({
this.options.language =
document.querySelector("html").getAttribute("lang") || "en";

await ensureIntlSupport(this.options.language);

// the ``attributes`` options key is not compatible with backbone,
// but queryHelper that will be constructed by the default
// ResultCollection will expect this to be passed into it.
Expand Down
7 changes: 7 additions & 0 deletions webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,13 @@ module.exports = () => {
extensions: [".js", ".ts", ".json", ".wasm", ".svelte"],
mainFields: ["browser", "module", "main"],
conditionNames: ["svelte", "browser", "require"],
alias: {
...config.resolve.alias,
"@formatjs/intl-datetimeformat/locale-data": path.resolve(
__dirname,
"node_modules/@formatjs/intl-datetimeformat/locale-data"
),
},
};

// NOTE: above doesn't work.
Expand Down
Loading