diff --git a/modules/translation/plural_rules.go b/modules/translation/plural_rules.go index 59665da255..587ee48850 100644 --- a/modules/translation/plural_rules.go +++ b/modules/translation/plural_rules.go @@ -2,6 +2,7 @@ // SPDX-License-Identifier: MIT // Some useful links: +// https://codeberg.org/forgejo/forgejo/src/branch/forgejo/web_src/js/webcomponents/relative-time.js // https://www.unicode.org/cldr/charts/46/supplemental/language_plural_rules.html // https://translate.codeberg.org/languages/$LANGUAGE_CODE/#information // https://github.com/WeblateOrg/language-data/blob/main/languages.csv @@ -16,7 +17,7 @@ import ( "forgejo.org/modules/translation/i18n" ) -// The constants refer to indices below in `PluralRules` and also in i18n.js, keep them in sync! +// The constants refer to indices below in `PluralRules` and also in web_src/js/webcomponents/relative-time.js, keep them in sync! const ( PluralRuleDefault = 0 PluralRuleBengali = 1 diff --git a/templates/repo/settings/lfs_file.tmpl b/templates/repo/settings/lfs_file.tmpl index 9864ed01d6..00bbae616a 100644 --- a/templates/repo/settings/lfs_file.tmpl +++ b/templates/repo/settings/lfs_file.tmpl @@ -31,10 +31,12 @@ {{ctx.Locale.Tr "repo.audio_not_supported_in_browser"}} {{else if .IsPDFFile}} -
+ + {{ctx.Locale.Tr "repo.diff.view_file"}} + {{else if .Is3DModelFile}} {{if .IsGLBFile}} - + {{else}} {{ctx.Locale.Tr "repo.file_view_raw"}}! {{end}} diff --git a/templates/repo/view_file.tmpl b/templates/repo/view_file.tmpl index 36809b769e..65be791405 100644 --- a/templates/repo/view_file.tmpl +++ b/templates/repo/view_file.tmpl @@ -127,10 +127,12 @@ {{ctx.Locale.Tr "repo.audio_not_supported_in_browser"}} {{else if .IsPDFFile}} -
+ + {{ctx.Locale.Tr "repo.diff.view_file"}} + {{else if .Is3DModelFile}} {{if .IsGLBFile}} - + {{else}} {{ctx.Locale.Tr "repo.file_view_raw"}} {{end}} diff --git a/web_src/css/modules/animations.css b/web_src/css/modules/animations.css index a86c9234aa..88b128c643 100644 --- a/web_src/css/modules/animations.css +++ b/web_src/css/modules/animations.css @@ -3,11 +3,13 @@ 100% { transform: translate(-50%, -50%) rotate(360deg); } } +lazy-webc, .is-loading { pointer-events: none !important; position: relative !important; } +lazy-webc > *, .is-loading > * { opacity: 0.3; } @@ -17,6 +19,7 @@ opacity: 0; } +lazy-webc::after, .is-loading::after { content: ""; position: absolute; @@ -52,8 +55,7 @@ form.single-button-form.is-loading .button { } .markup pre.is-loading, -.editor-loading.is-loading, -.pdf-content.is-loading { +.editor-loading.is-loading { height: var(--height-loading); } diff --git a/web_src/css/repo.css b/web_src/css/repo.css index 078ff7b4c4..a3713a4c3d 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -415,12 +415,14 @@ td .commit-summary { max-width: 600px !important; } +lazy-webc[tag="model-viewer"], model-viewer { width: 100%; height: 100vh; } -.pdf-content { +lazy-webc[tag="pdf-object"], +pdf-object { width: 100%; height: 100vh; border: none !important; @@ -429,10 +431,6 @@ model-viewer { justify-content: center; } -.pdf-content .pdf-fallback-button { - margin: 50px auto; -} - .repository.file.list .non-diff-file-content .plain-text { padding: 1em 2em; } diff --git a/web_src/js/index.js b/web_src/js/index.js index 1dab9ae292..98e57ef450 100644 --- a/web_src/js/index.js +++ b/web_src/js/index.js @@ -22,8 +22,6 @@ import {initRepoIssueContentHistory} from './features/repo-issue-content.js'; import {initStopwatch} from './features/stopwatch.js'; import {initFindFileInRepo} from './features/repo-findfile.js'; import {initCommentContent, initMarkupContent} from './markup/content.js'; -import {initPdfViewer} from './render/pdf.js'; -import {initGltfViewer} from './render/gltf.js'; import {initUserAuthOauth2, initUserAuth} from './features/user-auth.js'; import { @@ -189,8 +187,6 @@ onDomReady(() => { initUserAuthWebAuthnRegister(); initUserAuth(); initRepoDiffView(); - initPdfViewer(); - initGltfViewer(); initScopedAccessTokenCategories(); initColorPickers(); diff --git a/web_src/js/render/gltf.js b/web_src/js/render/gltf.js deleted file mode 100644 index 2d48e9f8e6..0000000000 --- a/web_src/js/render/gltf.js +++ /dev/null @@ -1,6 +0,0 @@ -export async function initGltfViewer() { - const els = document.querySelectorAll('model-viewer'); - if (!els.length) return; - - await import(/* webpackChunkName: "@google/model-viewer" */'@google/model-viewer'); -} diff --git a/web_src/js/render/pdf.js b/web_src/js/render/pdf.js deleted file mode 100644 index f31f161e6e..0000000000 --- a/web_src/js/render/pdf.js +++ /dev/null @@ -1,19 +0,0 @@ -import {htmlEscape} from 'escape-goat'; - -export async function initPdfViewer() { - const els = document.querySelectorAll('.pdf-content'); - if (!els.length) return; - - const pdfobject = await import(/* webpackChunkName: "pdfobject" */'pdfobject'); - - for (const el of els) { - const src = el.getAttribute('data-src'); - const fallbackText = el.getAttribute('data-fallback-button-text'); - pdfobject.embed(src, el, { - fallbackLink: htmlEscape` - ${fallbackText} - `, - }); - el.classList.remove('is-loading'); - } -} diff --git a/web_src/js/webcomponents/i18n.js b/web_src/js/webcomponents/i18n.js deleted file mode 100644 index c94b82b583..0000000000 --- a/web_src/js/webcomponents/i18n.js +++ /dev/null @@ -1,75 +0,0 @@ -const {pageData} = window.config; - -/** - * A list of plural rules for all languages. - * `plural_rules.go` defines the index for each of the 14 known plural rules. - * - * `pageData.PLURAL_RULE_LANG` is the index of the plural rule for the current language. - * `pageData.PLURAL_RULE_FALLBACK` is the index of the plural rule for the default language, - * to be used when a string is not translated in the current language. - * - * Each plural rule is a function that maps an amount `n` to the appropriate plural form index. - * Which index means which rule is specific for each language and also defined in `plural_rules.go`. - * The actual strings are in `pageData.PLURALSTRINGS_LANG` and `pageData.PLURALSTRINGS_FALLBACK` - * respectively, which is an array indexed by the plural form index. - * - * Links to the language plural rule and form definitions: - * https://codeberg.org/forgejo/forgejo/src/branch/forgejo/modules/translation/plural_rules.go - * https://www.unicode.org/cldr/charts/46/supplemental/language_plural_rules.html - * https://translate.codeberg.org/languages/$LANGUAGE_CODE/#information - * https://github.com/WeblateOrg/language-data/blob/main/languages.csv - */ -const PLURAL_RULES = [ - // [ 0] Common 2-form, e.g. English, German - function (n) { return n !== 1 ? 1 : 0 }, - - // [ 1] Bengali 2-form - function (n) { return n > 1 ? 1 : 0 }, - - // [ 2] Icelandic 2-form - function (n) { return n % 10 !== 1 || n % 100 === 11 ? 1 : 0 }, - - // [ 3] Filipino 2-form - function (n) { return n !== 1 && n !== 2 && n !== 3 && (n % 10 === 4 || n % 10 === 6 || n % 10 === 9) ? 1 : 0 }, - - // [ 4] One form - function (_) { return 0 }, - - // [ 5] Czech 3-form - function (n) { return (n === 1) ? 0 : (n >= 2 && n <= 4) ? 1 : 2 }, - - // [ 6] Russian 3-form - function (n) { return n % 10 === 1 && n % 100 !== 11 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2 }, - - // [ 7] Polish 3-form - function (n) { return n === 1 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2 }, - - // [ 8] Latvian 3-form - function (n) { return (n % 10 === 0 || n % 100 >= 11 && n % 100 <= 19) ? 0 : ((n % 10 === 1 && n % 100 !== 11) ? 1 : 2) }, - - // [ 9] Lithunian 3-form - function (n) { return (n % 10 === 1 && (n % 100 < 11 || n % 100 > 19)) ? 0 : ((n % 10 >= 2 && n % 10 <= 9 && (n % 100 < 11 || n % 100 > 19)) ? 1 : 2) }, - - // [10] French 3-form - function (n) { return (n === 0 || n === 1) ? 0 : ((n !== 0 && n % 1000000 === 0) ? 1 : 2) }, - - // [11] Catalan 3-form - function (n) { return (n === 1) ? 0 : ((n !== 0 && n % 1000000 === 0) ? 1 : 2) }, - - // [12] Slovenian 4-form - function (n) { return n % 100 === 1 ? 0 : n % 100 === 2 ? 1 : n % 100 === 3 || n % 100 === 4 ? 2 : 3 }, - - // [13] Arabic 6-form - function (n) { return n === 0 ? 0 : n === 1 ? 1 : n === 2 ? 2 : n % 100 >= 3 && n % 100 <= 10 ? 3 : n % 100 >= 11 ? 4 : 5 }, -]; - -/** - * Look up the correct localized plural form for amount `n` for the string with the translation key `key`. - * If the current language does not contain a translation for this key, returns the text in the default language, - * or `null` if `suppress_fallback` is set to `true`. - */ -export function GetPluralizedString(key, n, suppress_fallback) { - const result = pageData.PLURALSTRINGS_LANG[key]?.[PLURAL_RULES[pageData.PLURAL_RULE_LANG](n)]; - if (result || suppress_fallback) return result; - return pageData.PLURALSTRINGS_FALLBACK[key][PLURAL_RULES[pageData.PLURAL_RULE_FALLBACK](n)]; -} diff --git a/web_src/js/webcomponents/index.js b/web_src/js/webcomponents/index.js index f8883fa47a..1572de262f 100644 --- a/web_src/js/webcomponents/index.js +++ b/web_src/js/webcomponents/index.js @@ -1,6 +1,5 @@ -import './polyfills.js'; -import './i18n.js'; import './relative-time.js'; import './origin-url.js'; import './overflow-menu.js'; import './absolute-date.js'; +import './lazy-webc.js'; // infrequently used components should be lazy-loaded with ... diff --git a/web_src/js/webcomponents/lazy-webc.js b/web_src/js/webcomponents/lazy-webc.js new file mode 100644 index 0000000000..3570df3b5d --- /dev/null +++ b/web_src/js/webcomponents/lazy-webc.js @@ -0,0 +1,66 @@ +import {onDomReady} from '../utils/dom.js'; + +/** + * Lazy-load the promise (making it a singleton). + * @param {()=>Promise} newPromise Promise factory. + * @returns {()=>Promise} Singleton promise + */ +function lazyPromise(newPromise) { + /** @type {Promise?} */ + let p; + return () => { + p ??= newPromise(); + return p; + }; +} + +// the following web components will only be loaded if present in the page (to reduce the bundle size for infrequently used components) +const loadableComponents = { + 'model-viewer': lazyPromise(() => { + return import(/* webpackChunkName: "model-viewer" */ '@google/model-viewer'); + }), + 'pdf-object': lazyPromise(() => { + return import(/* webpackChunkName: "pdf-object" */ './pdf-object.js'); + }), +}; + +/** + * Replace elt with an element having the given tag. + * @param {HTMLElement} elt The element to replace. + * @param {string} name The tagName of the new element. + */ +function replaceTag(elt, name) { + const successor = document.createElement(name); + // Move the children to the successor + while (elt.firstChild) { + successor.append(elt.firstChild); + } + // Copy the attributes to the successor + for (let index = elt.attributes.length - 1; index >= 0; --index) { + successor.attributes.setNamedItem(elt.attributes[index].cloneNode()); + } + // Replace elt with the successor + elt.parentNode.replaceChild(successor, elt); +} + +onDomReady(() => { + // The lazy-webc component will replace itself with an element of the type given in the attribute tag. + // This seems to be the best way without having to create a global mutationObserver. + // See https://codeberg.org/forgejo/forgejo/pulls/8510 for discussion. + window.customElements.define( + 'lazy-webc', + class extends HTMLElement { + connectedCallback() { + const name = this.getAttribute('tag'); + if (loadableComponents[name]) { + loadableComponents[name]().finally(() => { + replaceTag(this, name); + }); + } else { + console.error('lazy-webc: unknown webcomponent:', name); + replaceTag(this, name); // still replace it, maybe it was eagerly defined + } + } + }, + ); +}); diff --git a/web_src/js/webcomponents/pdf-object.js b/web_src/js/webcomponents/pdf-object.js new file mode 100644 index 0000000000..15334dafe7 --- /dev/null +++ b/web_src/js/webcomponents/pdf-object.js @@ -0,0 +1,14 @@ +import pdfobject from 'pdfobject'; + +window.customElements.define( + 'pdf-object', + class extends HTMLElement { + connectedCallback() { + // since the web-component is defined after the DOM is ready, it is safe to look at the children. + const fallbackLink = this.innerHTML; // eslint-disable-line wc/no-child-traversal-in-connectedcallback + pdfobject.embed(this.getAttribute('src'), this, { + fallbackLink, + }); + } + }, +); diff --git a/web_src/js/webcomponents/polyfills.js b/web_src/js/webcomponents/polyfills.js deleted file mode 100644 index 38f50fa02f..0000000000 --- a/web_src/js/webcomponents/polyfills.js +++ /dev/null @@ -1,17 +0,0 @@ -try { - // some browsers like PaleMoon don't have full support for Intl.NumberFormat, so do the minimum polyfill to support "relative-time-element" - // https://repo.palemoon.org/MoonchildProductions/UXP/issues/2289 - new Intl.NumberFormat('en', {style: 'unit', unit: 'minute'}).format(1); -} catch { - const intlNumberFormat = Intl.NumberFormat; - Intl.NumberFormat = function(locales, options) { - if (options.style === 'unit') { - return { - format(value) { - return ` ${value} ${options.unit}`; - }, - }; - } - return intlNumberFormat(locales, options); - }; -} diff --git a/web_src/js/webcomponents/relative-time.js b/web_src/js/webcomponents/relative-time.js index d247ced3ca..2ec87450c1 100644 --- a/web_src/js/webcomponents/relative-time.js +++ b/web_src/js/webcomponents/relative-time.js @@ -1,4 +1,3 @@ -import {GetPluralizedString} from './i18n.js'; import dayjs from 'dayjs'; const {pageData} = window.config; @@ -17,8 +16,75 @@ const ABSOLUTE_DATETIME_FORMAT = new Intl.DateTimeFormat(navigator.language, { }); const FALLBACK_DATETIME_FORMAT = new Intl.RelativeTimeFormat(navigator.language, {style: 'long'}); +/** + * A list of plural rules for all languages. + * `plural_rules.go` defines the index for each of the 14 known plural rules. + * + * `pageData.PLURAL_RULE_LANG` is the index of the plural rule for the current language. + * `pageData.PLURAL_RULE_FALLBACK` is the index of the plural rule for the default language, + * to be used when a string is not translated in the current language. + * + * Each plural rule is a function that maps an amount `n` to the appropriate plural form index. + * Which index means which rule is specific for each language and also defined in `plural_rules.go`. + * The actual strings are in `pageData.PLURALSTRINGS_LANG` and `pageData.PLURALSTRINGS_FALLBACK` + * respectively, which is an array indexed by the plural form index. + * + * Links to the language plural rule and form definitions: + * https://codeberg.org/forgejo/forgejo/src/branch/forgejo/modules/translation/plural_rules.go + * https://www.unicode.org/cldr/charts/46/supplemental/language_plural_rules.html + * https://translate.codeberg.org/languages/$LANGUAGE_CODE/#information + * https://github.com/WeblateOrg/language-data/blob/main/languages.csv + */ +const PLURAL_RULES = [ + // [ 0] Common 2-form, e.g. English, German + function (n) { return n !== 1 ? 1 : 0 }, + + // [ 1] Bengali 2-form + function (n) { return n > 1 ? 1 : 0 }, + + // [ 2] Icelandic 2-form + function (n) { return n % 10 !== 1 || n % 100 === 11 ? 1 : 0 }, + + // [ 3] Filipino 2-form + function (n) { return n !== 1 && n !== 2 && n !== 3 && (n % 10 === 4 || n % 10 === 6 || n % 10 === 9) ? 1 : 0 }, + + // [ 4] One form + function (_) { return 0 }, + + // [ 5] Czech 3-form + function (n) { return (n === 1) ? 0 : (n >= 2 && n <= 4) ? 1 : 2 }, + + // [ 6] Russian 3-form + function (n) { return n % 10 === 1 && n % 100 !== 11 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2 }, + + // [ 7] Polish 3-form + function (n) { return n === 1 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2 }, + + // [ 8] Latvian 3-form + function (n) { return (n % 10 === 0 || n % 100 >= 11 && n % 100 <= 19) ? 0 : ((n % 10 === 1 && n % 100 !== 11) ? 1 : 2) }, + + // [ 9] Lithunian 3-form + function (n) { return (n % 10 === 1 && (n % 100 < 11 || n % 100 > 19)) ? 0 : ((n % 10 >= 2 && n % 10 <= 9 && (n % 100 < 11 || n % 100 > 19)) ? 1 : 2) }, + + // [10] French 3-form + function (n) { return (n === 0 || n === 1) ? 0 : ((n !== 0 && n % 1000000 === 0) ? 1 : 2) }, + + // [11] Catalan 3-form + function (n) { return (n === 1) ? 0 : ((n !== 0 && n % 1000000 === 0) ? 1 : 2) }, + + // [12] Slovenian 4-form + function (n) { return n % 100 === 1 ? 0 : n % 100 === 2 ? 1 : n % 100 === 3 || n % 100 === 4 ? 2 : 3 }, + + // [13] Arabic 6-form + function (n) { return n === 0 ? 0 : n === 1 ? 1 : n === 2 ? 2 : n % 100 >= 3 && n % 100 <= 10 ? 3 : n % 100 >= 11 ? 4 : 5 }, +]; + +/** + * Look up the correct localized plural form for amount `n` for the string with the translation key `key`. + * If the current language does not contain a translation for this key, fallback to the browser's formatting. + */ function GetPluralizedStringOrFallback(key, n, unit) { - const translation = GetPluralizedString(key, n, true); + const translation = pageData.PLURALSTRINGS_LANG[key]?.[PLURAL_RULES[pageData.PLURAL_RULE_LANG](n)]; if (translation) return translation.replace('%d', n); return FALLBACK_DATETIME_FORMAT.format(-n, unit); }