diff --git a/packages/xl-docx-exporter/src/docx/docxExporter.test.ts b/packages/xl-docx-exporter/src/docx/docxExporter.test.ts index d9d3adeb2b..a7bf9edbb4 100644 --- a/packages/xl-docx-exporter/src/docx/docxExporter.test.ts +++ b/packages/xl-docx-exporter/src/docx/docxExporter.test.ts @@ -28,7 +28,11 @@ describe("exporter", () => { }), docxDefaultSchemaMappings, ); - const doc = await exporter.toDocxJsDocument(testDocument); + const doc = await exporter.toDocxJsDocument(testDocument, { + sectionOptions: {}, + documentOptions: {}, + locale: "en-US", + }); const blob = await Packer.toBlob(doc); const zip = new ZipReader(new BlobReader(blob)); @@ -56,6 +60,7 @@ describe("exporter", () => { ); const doc = await exporter.toDocxJsDocument(testDocument, { + locale: "en-US", documentOptions: { creator: "John Doe", }, @@ -181,6 +186,7 @@ describe("exporter", () => { ], }, ]), + { sectionOptions: {}, documentOptions: {}, locale: "en-US" }, ); const blob = await Packer.toBlob(doc); diff --git a/packages/xl-docx-exporter/src/docx/docxExporter.ts b/packages/xl-docx-exporter/src/docx/docxExporter.ts index 9e2ddfd6b6..6a79d5c53d 100644 --- a/packages/xl-docx-exporter/src/docx/docxExporter.ts +++ b/packages/xl-docx-exporter/src/docx/docxExporter.ts @@ -187,10 +187,21 @@ export class DOCXExporter< ]; } - protected async createDefaultDocumentOptions(): Promise { - const externalStyles = (await import("./template/word/styles.xml?raw")) + protected async createDefaultDocumentOptions( + locale?: string, + ): Promise { + let externalStyles = (await import("./template/word/styles.xml?raw")) .default; + // Replace the default language in styles.xml with the provided locale. + // If not provided, default to en-US. + const resolvedLocale = (locale && locale.trim()) || "en-US"; + + externalStyles = externalStyles.replace( + /(]*\bw:val=")([^"]+)("[^>]*\/>)/g, + `$1${resolvedLocale}$3`, + ); + const bullets = ["•"]; //, "◦", "▪"]; (these don't look great, just use solid bullet for now) return { numbering: { @@ -247,6 +258,11 @@ export class DOCXExporter< options: { sectionOptions: Omit; documentOptions: DocumentOptions; + /** + * The document locale in OOXML format (e.g. en-US, fr-FR, de-DE). + * If omitted, defaults to en-US. + */ + locale?: string; } = { sectionOptions: {}, documentOptions: {}, @@ -276,13 +292,18 @@ export class DOCXExporter< options: { sectionOptions: Omit; documentOptions: DocumentOptions; + /** + * The document locale in OOXML format (e.g. en-US, fr-FR, de-DE). + * If omitted, defaults to en-US. + */ + locale?: string; } = { sectionOptions: {}, documentOptions: {}, }, ) { const doc = new Document({ - ...(await this.createDefaultDocumentOptions()), + ...(await this.createDefaultDocumentOptions(options.locale)), ...options.documentOptions, sections: [ { diff --git a/shared/util/fileUtil.ts b/shared/util/fileUtil.ts index 0a8c3d8ed4..6f77bf3a7a 100644 --- a/shared/util/fileUtil.ts +++ b/shared/util/fileUtil.ts @@ -36,6 +36,12 @@ export async function loadFileBuffer(requireUrl: { if (url.startsWith("/@fs/")) { url = url.substring("/@fs".length); } + // On Windows, vite/vitest may yield paths like "/C:/..." after removing /@fs + // Node on Windows treats paths starting with "/" as relative to current drive, + // which would produce "C:\C:\...". Strip leading slash when followed by a drive letter. + if (/^\/[A-Za-z]:/.test(url)) { + url = url.slice(1); + } const buffer = fs.readFileSync(url); return buffer; } else {