From 97c2ce9da02dfc3ee3a563342e48540c07c18a6f Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Tue, 30 Jan 2024 18:32:35 +0100 Subject: [PATCH] Ensure that `GenericL10n` works if the locale files cannot be loaded - Ensure that localization works in the GENERIC viewer, even if the necessary locale files cannot be loaded. This was the behaviour prior to the introduction of Fluent, and it seems worthwhile to keep that (especially since we already bundle the en-US strings anyway). - Let the `GenericL10n`-implementation use the *bundled* en-US strings directly when no language is provided. - Remove the `NullL10n`-implementation, and simply fallback to `GenericL10n`, to reduce the maintenance burden of viewer-components localization. - Indirectly, given the previous point, stop exporting `NullL10n` in the viewer-components since it's now removed. Note that it was never really intended to be used directly and only existed as a fallback. *Please note:* This doesn't affect the Firefox PDF Viewer, thanks to the use of import maps. --- examples/mobile-viewer/viewer.mjs | 2 +- gulpfile.mjs | 9 +-- test/unit/pdf_viewer.component_spec.js | 13 ---- test/unit/unit_test.html | 2 +- tsconfig.json | 2 +- web/annotation_editor_layer_builder.js | 7 ++- web/genericl10n.js | 65 ++++++++++++++----- web/l10n.js | 4 +- web/l10n_utils.js | 87 -------------------------- web/pdf_page_view.js | 9 ++- web/pdf_viewer.component.js | 2 - web/pdf_viewer.js | 9 ++- web/stubs.js | 18 ------ web/viewer-geckoview.html | 2 +- web/viewer.html | 2 +- 15 files changed, 78 insertions(+), 155 deletions(-) delete mode 100644 web/l10n_utils.js delete mode 100644 web/stubs.js diff --git a/examples/mobile-viewer/viewer.mjs b/examples/mobile-viewer/viewer.mjs index 32915ebe9..fedd5d8e2 100644 --- a/examples/mobile-viewer/viewer.mjs +++ b/examples/mobile-viewer/viewer.mjs @@ -272,7 +272,7 @@ const PDFViewerApplication = { }); this.pdfLinkService = linkService; - this.l10n = pdfjsViewer.NullL10n; + this.l10n = new pdfjsViewer.GenericL10n(); const container = document.getElementById("viewerContainer"); const pdfViewer = new pdfjsViewer.PDFViewer({ diff --git a/gulpfile.mjs b/gulpfile.mjs index 697a81a46..8112e8b15 100644 --- a/gulpfile.mjs +++ b/gulpfile.mjs @@ -272,7 +272,7 @@ function createWebpackConfig( "web-com": "", "web-download_manager": "", "web-external_services": "", - "web-l10n_utils": "web/stubs.js", + "web-null_l10n": "", "web-pdf_attachment_viewer": "web/pdf_attachment_viewer.js", "web-pdf_cursor_tools": "web/pdf_cursor_tools.js", "web-pdf_document_properties": "web/pdf_document_properties.js", @@ -294,6 +294,7 @@ function createWebpackConfig( viewerAlias["web-com"] = "web/chromecom.js"; viewerAlias["web-download_manager"] = "web/download_manager.js"; viewerAlias["web-external_services"] = "web/chromecom.js"; + viewerAlias["web-null_l10n"] = "web/l10n.js"; viewerAlias["web-preferences"] = "web/chromecom.js"; viewerAlias["web-print_service"] = "web/pdf_print_service.js"; } else if (bundleDefines.GENERIC) { @@ -308,13 +309,12 @@ function createWebpackConfig( viewerAlias["web-com"] = "web/genericcom.js"; viewerAlias["web-download_manager"] = "web/download_manager.js"; viewerAlias["web-external_services"] = "web/genericcom.js"; - viewerAlias["web-l10n_utils"] = "web/l10n_utils.js"; + viewerAlias["web-null_l10n"] = "web/genericl10n.js"; viewerAlias["web-preferences"] = "web/genericcom.js"; viewerAlias["web-print_service"] = "web/pdf_print_service.js"; } else if (bundleDefines.MOZCENTRAL) { if (bundleDefines.GECKOVIEW) { const gvAlias = { - "web-l10n_utils": "web/stubs.js", "web-toolbar": "web/toolbar-geckoview.js", }; for (const key in viewerAlias) { @@ -324,6 +324,7 @@ function createWebpackConfig( viewerAlias["web-com"] = "web/firefoxcom.js"; viewerAlias["web-download_manager"] = "web/firefoxcom.js"; viewerAlias["web-external_services"] = "web/firefoxcom.js"; + viewerAlias["web-null_l10n"] = "web/l10n.js"; viewerAlias["web-preferences"] = "web/firefoxcom.js"; viewerAlias["web-print_service"] = "web/firefox_print_service.js"; } @@ -1616,7 +1617,7 @@ function buildLibHelper(bundleDefines, inputStream, outputDir) { "display-node_utils": "./node_utils.js", "fluent-bundle": "../../../node_modules/@fluent/bundle/esm/index.js", "fluent-dom": "../../../node_modules/@fluent/dom/esm/index.js", - "web-l10n_utils": "../web/l10n_utils.js", + "web-null_l10n": "../web/genericl10n.js", }, }; const licenseHeaderLibre = fs diff --git a/test/unit/pdf_viewer.component_spec.js b/test/unit/pdf_viewer.component_spec.js index cace29e03..1785a4959 100644 --- a/test/unit/pdf_viewer.component_spec.js +++ b/test/unit/pdf_viewer.component_spec.js @@ -30,8 +30,6 @@ import { AnnotationLayerBuilder } from "../../web/annotation_layer_builder.js"; import { DownloadManager } from "../../web/download_manager.js"; import { EventBus } from "../../web/event_utils.js"; import { GenericL10n } from "../../web/genericl10n.js"; -import { L10n } from "../../web/l10n.js"; -import { NullL10n } from "../../web/l10n_utils.js"; import { PDFHistory } from "../../web/pdf_history.js"; import { PDFPageView } from "../../web/pdf_page_view.js"; import { PDFScriptingManager } from "../../web/pdf_scripting_manager.component.js"; @@ -54,7 +52,6 @@ describe("pdfviewer_api", function () { FindState, GenericL10n, LinkTarget, - NullL10n, parseQueryString, PDFFindController, PDFHistory, @@ -73,14 +70,4 @@ describe("pdfviewer_api", function () { XfaLayerBuilder, }); }); - - it("checks that `NullL10n` implements all methods", function () { - const methods = Object.getOwnPropertyNames(NullL10n).sort(); - - const baseMethods = Object.getOwnPropertyNames(L10n.prototype) - .filter(m => m !== "constructor" && !m.startsWith("_")) - .sort(); - - expect(methods).toEqual(baseMethods); - }); }); diff --git a/test/unit/unit_test.html b/test/unit/unit_test.html index add51eb7c..d88bd5bfc 100644 --- a/test/unit/unit_test.html +++ b/test/unit/unit_test.html @@ -30,7 +30,7 @@ "web-com": "../../web/genericcom.js", "web-download_manager": "../../web/download_manager.js", "web-external_services": "../../web/genericcom.js", - "web-l10n_utils": "../../web/l10n_utils.js", + "web-null_l10n": "../../web/genericl10n.js", "web-pdf_attachment_viewer": "../../web/pdf_attachment_viewer.js", "web-pdf_cursor_tools": "../../web/pdf_cursor_tools.js", "web-pdf_document_properties": "../../web/pdf_document_properties.js", diff --git a/tsconfig.json b/tsconfig.json index 402f13d23..4a48983e6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,7 +16,7 @@ "display-node_utils": ["./src/display/node_utils"], "fluent-bundle": ["./node_modules/@fluent/bundle/esm/index.js"], "fluent-dom": ["./node_modules/@fluent/dom/esm/index.js"], - "web-l10n_utils": ["./web/l10n_utils"] + "web-null_l10n": ["../web/genericl10n.js"] } }, "files": ["src/pdf.js", "web/pdf_viewer.component.js"] diff --git a/web/annotation_editor_layer_builder.js b/web/annotation_editor_layer_builder.js index d4a2aa9fd..c841de6ac 100644 --- a/web/annotation_editor_layer_builder.js +++ b/web/annotation_editor_layer_builder.js @@ -25,7 +25,7 @@ /** @typedef {import("../src/display/annotation_layer.js").AnnotationLayer} AnnotationLayer */ import { AnnotationEditorLayer } from "pdfjs-lib"; -import { NullL10n } from "web-l10n_utils"; +import { GenericL10n } from "web-null_l10n"; /** * @typedef {Object} AnnotationEditorLayerBuilderOptions @@ -55,7 +55,10 @@ class AnnotationEditorLayerBuilder { this.pageDiv = options.pageDiv; this.pdfPage = options.pdfPage; this.accessibilityManager = options.accessibilityManager; - this.l10n = options.l10n || NullL10n; + this.l10n = options.l10n; + if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { + this.l10n ||= new GenericL10n(); + } this.annotationEditorLayer = null; this.div = null; this._cancelled = false; diff --git a/web/genericl10n.js b/web/genericl10n.js index 7a350b50f..431b1043f 100644 --- a/web/genericl10n.js +++ b/web/genericl10n.js @@ -20,22 +20,34 @@ import { DOMLocalization } from "fluent-dom"; import { fetchData } from "pdfjs-lib"; import { L10n } from "./l10n.js"; +function createBundle(lang, text) { + const resource = new FluentResource(text); + const bundle = new FluentBundle(lang); + const errors = bundle.addResource(resource); + if (errors.length) { + console.error("L10n errors", errors); + } + return bundle; +} + /** * @implements {IL10n} */ class GenericL10n extends L10n { constructor(lang) { super({ lang }); - this._setL10n( - new DOMLocalization( - [], - GenericL10n.#generateBundles.bind( + + const generateBundles = !lang + ? GenericL10n.#generateBundlesFallback.bind( + GenericL10n, + this.getLanguage() + ) + : GenericL10n.#generateBundles.bind( GenericL10n, "en-us", this.getLanguage() - ) - ) - ); + ); + this._setL10n(new DOMLocalization([], generateBundles)); } /** @@ -63,6 +75,9 @@ class GenericL10n extends L10n { if (bundle) { yield bundle; } + if (lang === "en-us") { + yield this.#createBundleFallback(lang); + } } } @@ -74,20 +89,36 @@ class GenericL10n extends L10n { const url = new URL(path, baseURL); const text = await fetchData(url, /* type = */ "text"); - const resource = new FluentResource(text); - const bundle = new FluentBundle(lang); - const errors = bundle.addResource(resource); - if (errors.length) { - console.error("L10n errors", errors); - } - return bundle; + return createBundle(lang, text); } static async #getPaths() { - const { href } = document.querySelector(`link[type="application/l10n"]`); - const paths = await fetchData(href, /* type = */ "json"); + try { + const { href } = document.querySelector(`link[type="application/l10n"]`); + const paths = await fetchData(href, /* type = */ "json"); - return { baseURL: href.replace(/[^/]*$/, "") || "./", paths }; + return { baseURL: href.replace(/[^/]*$/, "") || "./", paths }; + } catch {} + return { baseURL: "./", paths: Object.create(null) }; + } + + static async *#generateBundlesFallback(lang) { + yield this.#createBundleFallback(lang); + } + + static async #createBundleFallback(lang) { + if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) { + throw new Error("Not implemented: #createBundleFallback"); + } + const text = + typeof PDFJSDev === "undefined" + ? await fetchData( + new URL(`./locale/${lang}/viewer.ftl`, window.location.href), + /* type = */ "text" + ) + : PDFJSDev.eval("DEFAULT_FTL"); + + return createBundle(lang, text); } } diff --git a/web/l10n.js b/web/l10n.js index 14c09c389..7909e43ca 100644 --- a/web/l10n.js +++ b/web/l10n.js @@ -117,4 +117,6 @@ class L10n { } } -export { L10n }; +const GenericL10n = null; + +export { GenericL10n, L10n }; diff --git a/web/l10n_utils.js b/web/l10n_utils.js deleted file mode 100644 index 2e493651e..000000000 --- a/web/l10n_utils.js +++ /dev/null @@ -1,87 +0,0 @@ -/* Copyright 2021 Mozilla Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** @typedef {import("./interfaces").IL10n} IL10n */ - -import { fetchData, shadow } from "pdfjs-lib"; -import { FluentBundle, FluentResource } from "fluent-bundle"; -import { DOMLocalization } from "fluent-dom"; -import { L10n } from "./l10n.js"; - -/** - * @implements {IL10n} - */ -class ConstL10n extends L10n { - constructor(lang) { - super({ lang }); - this._setL10n( - new DOMLocalization([], ConstL10n.#generateBundles.bind(ConstL10n, lang)) - ); - } - - static async *#generateBundles(lang) { - const text = - typeof PDFJSDev === "undefined" - ? await fetchData( - new URL(`./locale/${lang}/viewer.ftl`, window.location.href), - /* type = */ "text" - ) - : PDFJSDev.eval("DEFAULT_FTL"); - - const resource = new FluentResource(text); - const bundle = new FluentBundle(lang); - const errors = bundle.addResource(resource); - if (errors.length) { - console.error("L10n errors", errors); - } - yield bundle; - } - - static get instance() { - return shadow(this, "instance", new ConstL10n("en-us")); - } -} - -/** - * No-op implementation of the localization service. - * @implements {IL10n} - */ -const NullL10n = { - getLanguage() { - return ConstL10n.instance.getLanguage(); - }, - - getDirection() { - return ConstL10n.instance.getDirection(); - }, - - async get(ids, args = null, fallback) { - return ConstL10n.instance.get(ids, args, fallback); - }, - - async translate(element) { - return ConstL10n.instance.translate(element); - }, - - pause() { - return ConstL10n.instance.pause(); - }, - - resume() { - return ConstL10n.instance.resume(); - }, -}; - -export { NullL10n }; diff --git a/web/pdf_page_view.js b/web/pdf_page_view.js index 1df2f360e..4e97897b5 100644 --- a/web/pdf_page_view.js +++ b/web/pdf_page_view.js @@ -43,7 +43,7 @@ import { AnnotationEditorLayerBuilder } from "./annotation_editor_layer_builder. import { AnnotationLayerBuilder } from "./annotation_layer_builder.js"; import { compatibilityParams } from "./app_options.js"; import { DrawLayerBuilder } from "./draw_layer_builder.js"; -import { NullL10n } from "web-l10n_utils"; +import { GenericL10n } from "web-null_l10n"; import { SimpleLinkService } from "./pdf_link_service.js"; import { StructTreeLayerBuilder } from "./struct_tree_layer_builder.js"; import { TextAccessibilityManager } from "./text_accessibility.js"; @@ -157,7 +157,10 @@ class PDFPageView { this.eventBus = options.eventBus; this.renderingQueue = options.renderingQueue; - this.l10n = options.l10n || NullL10n; + this.l10n = options.l10n; + if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { + this.l10n ||= new GenericL10n(); + } this.renderTask = null; this.resume = null; @@ -214,7 +217,7 @@ class PDFPageView { } // Ensure that Fluent is connected in e.g. the COMPONENTS build. - if (this.l10n === NullL10n) { + if (!options.l10n) { this.l10n.translate(this.div); } } diff --git a/web/pdf_viewer.component.js b/web/pdf_viewer.component.js index c10d4aeb6..e3f9e8db8 100644 --- a/web/pdf_viewer.component.js +++ b/web/pdf_viewer.component.js @@ -30,7 +30,6 @@ import { AnnotationLayerBuilder } from "./annotation_layer_builder.js"; import { DownloadManager } from "./download_manager.js"; import { EventBus } from "./event_utils.js"; import { GenericL10n } from "./genericl10n.js"; -import { NullL10n } from "./l10n_utils.js"; import { PDFHistory } from "./pdf_history.js"; import { PDFPageView } from "./pdf_page_view.js"; import { PDFScriptingManager } from "./pdf_scripting_manager.component.js"; @@ -54,7 +53,6 @@ export { FindState, GenericL10n, LinkTarget, - NullL10n, parseQueryString, PDFFindController, PDFHistory, diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index 54cf66140..b1943dd9c 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -63,7 +63,7 @@ import { VERTICAL_PADDING, watchScroll, } from "./ui_utils.js"; -import { NullL10n } from "web-l10n_utils"; +import { GenericL10n } from "web-null_l10n"; import { PDFPageView } from "./pdf_page_view.js"; import { PDFRenderingQueue } from "./pdf_rendering_queue.js"; import { SimpleLinkService } from "./pdf_link_service.js"; @@ -286,7 +286,10 @@ class PDFViewer { this.removePageBorders = options.removePageBorders || false; } this.maxCanvasPixels = options.maxCanvasPixels; - this.l10n = options.l10n || NullL10n; + this.l10n = options.l10n; + if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { + this.l10n ||= new GenericL10n(); + } this.#enablePermissions = options.enablePermissions || false; this.pageColors = options.pageColors || null; @@ -327,7 +330,7 @@ class PDFViewer { if ( (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) && - this.l10n === NullL10n + !options.l10n ) { // Ensure that Fluent is connected in e.g. the COMPONENTS build. this.l10n.translate(this.container); diff --git a/web/stubs.js b/web/stubs.js deleted file mode 100644 index 333ab1a5e..000000000 --- a/web/stubs.js +++ /dev/null @@ -1,18 +0,0 @@ -/* Copyright 2023 Mozilla Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -const NullL10n = null; - -export { NullL10n }; diff --git a/web/viewer-geckoview.html b/web/viewer-geckoview.html index 7076a436b..57e28e172 100644 --- a/web/viewer-geckoview.html +++ b/web/viewer-geckoview.html @@ -63,7 +63,7 @@ See https://github.com/adobe-type-tools/cmap-resources "web-com": "./genericcom.js", "web-download_manager": "./download_manager.js", "web-external_services": "./genericcom.js", - "web-l10n_utils": "./l10n_utils.js", + "web-null_l10n": "./genericl10n.js", "web-pdf_attachment_viewer": "./stubs-geckoview.js", "web-pdf_cursor_tools": "./stubs-geckoview.js", "web-pdf_document_properties": "./stubs-geckoview.js", diff --git a/web/viewer.html b/web/viewer.html index ffe19ed97..d65e56e63 100644 --- a/web/viewer.html +++ b/web/viewer.html @@ -72,7 +72,7 @@ See https://github.com/adobe-type-tools/cmap-resources "web-com": "./genericcom.js", "web-download_manager": "./download_manager.js", "web-external_services": "./genericcom.js", - "web-l10n_utils": "./l10n_utils.js", + "web-null_l10n": "./genericl10n.js", "web-pdf_attachment_viewer": "./pdf_attachment_viewer.js", "web-pdf_cursor_tools": "./pdf_cursor_tools.js", "web-pdf_document_properties": "./pdf_document_properties.js",