Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
28 changes: 28 additions & 0 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
5 changes: 4 additions & 1 deletion 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,7 +120,8 @@

let isRestoringHistory = false;

onMount(() => {
onMount(async () => {
Comment thread
MrTango marked this conversation as resolved.
await ensureIntlSupport(getLang());
contents.load();

function onPopState(event) {
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