From 959b03b5f4dd5628f8132e063ad79bbe4e0bde88 Mon Sep 17 00:00:00 2001 From: Hampus Date: Tue, 13 Oct 2026 22:07:00 +0200 Subject: [PATCH] fix(desktop): shell-compatible module updates, no linux walls (#3452) --- .github/workflows/build-desktop-modules.yaml | 23 +- .github/workflows/build-desktop.yaml | 43 +- fluxer_app/src/features/app/state/Updater.ts | 127 +- .../channel_header_components/UpdaterIcon.tsx | 11 - .../src/features/i18n/locales/ar/messages.po | 77 +- .../locales/auto-i18n-reviewed-unchanged.json | 2 + .../src/features/i18n/locales/bg/messages.po | 77 +- .../src/features/i18n/locales/cs/messages.po | 77 +- .../src/features/i18n/locales/da/messages.po | 77 +- .../src/features/i18n/locales/de/messages.po | 77 +- .../src/features/i18n/locales/el/messages.po | 77 +- .../features/i18n/locales/en-GB/messages.po | 77 +- .../features/i18n/locales/en-US/messages.po | 77 +- .../features/i18n/locales/es-419/messages.po | 77 +- .../features/i18n/locales/es-ES/messages.po | 77 +- .../src/features/i18n/locales/fi/messages.po | 77 +- .../src/features/i18n/locales/fr/messages.po | 77 +- .../src/features/i18n/locales/he/messages.po | 77 +- .../src/features/i18n/locales/hi/messages.po | 77 +- .../src/features/i18n/locales/hr/messages.po | 77 +- .../src/features/i18n/locales/hu/messages.po | 77 +- .../src/features/i18n/locales/id/messages.po | 77 +- .../src/features/i18n/locales/it/messages.po | 77 +- .../src/features/i18n/locales/ja/messages.po | 77 +- .../src/features/i18n/locales/ko/messages.po | 77 +- .../src/features/i18n/locales/lt/messages.po | 77 +- .../src/features/i18n/locales/nl/messages.po | 77 +- .../src/features/i18n/locales/no/messages.po | 77 +- .../src/features/i18n/locales/pl/messages.po | 77 +- .../features/i18n/locales/pt-BR/messages.po | 77 +- .../src/features/i18n/locales/ro/messages.po | 77 +- .../src/features/i18n/locales/ru/messages.po | 77 +- .../features/i18n/locales/sv-SE/messages.po | 77 +- .../src/features/i18n/locales/th/messages.po | 77 +- .../src/features/i18n/locales/tr/messages.po | 77 +- .../src/features/i18n/locales/uk/messages.po | 77 +- .../src/features/i18n/locales/vi/messages.po | 77 +- .../features/i18n/locales/zh-CN/messages.po | 77 +- .../features/i18n/locales/zh-TW/messages.po | 77 +- .../platform/utils/DesktopLocaleBridge.ts | 17 + .../commands/UpdaterModalCommands.module.css | 43 - .../updater/commands/UpdaterModalCommands.tsx | 207 +- .../updater/state/UpdaterStateMachine.ts | 10 +- .../updater/utils/LinuxManualUpdateOptions.ts | 181 -- fluxer_desktop/src/common/DesktopIdentity.ts | 6 - fluxer_desktop/src/common/Types.ts | 4 +- fluxer_desktop/src/main/Bootstrap.ts | 39 +- .../src/main/BootstrapEntry.test.mjs | 36 +- .../src/main/DesktopUpdatePrompt.test.mjs | 137 ++ .../src/main/DesktopUpdatePrompt.ts | 50 +- fluxer_desktop/src/main/MainI18n.ts | 3 + .../src/main/ModuleManifest.test.mjs | 29 +- fluxer_desktop/src/main/ModuleManifest.ts | 67 +- .../src/main/ModulePackageInstaller.test.mjs | 1 + .../src/main/ModulePackageInstaller.ts | 8 + fluxer_desktop/src/main/ModuleStore.test.mjs | 106 + fluxer_desktop/src/main/ModuleStore.ts | 245 +- .../src/main/ModuleUpdatePlanner.test.mjs | 151 +- .../src/main/ModuleUpdatePlanner.ts | 115 +- .../src/main/ModuleUpdater.test.mjs | 450 +++- fluxer_desktop/src/main/ModuleUpdater.ts | 274 +-- .../src/main/ShellDownloadFormats.ts | 32 - .../src/main/ShellUpdateCapability.test.mjs | 257 +- .../src/main/ShellUpdateCapability.ts | 9 +- fluxer_desktop/src/main/ShellUpdateSplash.ts | 151 +- fluxer_desktop/src/main/SplashWindow.test.mjs | 286 +-- fluxer_desktop/src/main/SplashWindow.ts | 100 +- fluxer_desktop/src/main/Updater.ts | 41 +- fluxer_desktop/src/main/UpdaterDownloads.ts | 65 +- fluxer_desktop/src/preload/splash.cjs | 169 +- fluxer_desktop/src/splash/index.css | 152 -- tools/ci/src/desktop.rs | 2107 +++++++++++++++-- tools/ci/src/release.rs | 81 + 73 files changed, 4142 insertions(+), 4311 deletions(-) delete mode 100644 fluxer_app/src/features/updater/commands/UpdaterModalCommands.module.css delete mode 100644 fluxer_app/src/features/updater/utils/LinuxManualUpdateOptions.ts create mode 100644 fluxer_desktop/src/main/DesktopUpdatePrompt.test.mjs diff --git a/.github/workflows/build-desktop-modules.yaml b/.github/workflows/build-desktop-modules.yaml index b0da527bf..562e4f310 100644 --- a/.github/workflows/build-desktop-modules.yaml +++ b/.github/workflows/build-desktop-modules.yaml @@ -16,18 +16,22 @@ on: required: false default: "" type: string - allow_shell_drift: - description: Publish even though shell sources changed since the live shell was built. Only when the renderer does not depend on those changes. - required: false - default: false - type: boolean + shell_contract: + description: "Did the shell contract change since the oldest shell these modules still reach? unchanged refuses if it did. compatible keeps older shells on these modules because the renderer still works on them. raise stops sending these modules to shells without the new contract: Linux and managed shells keep their current renderer, Windows and macOS update their shell first." + required: true + type: choice + options: + - unchanged + - compatible + - raise + default: unchanged permissions: contents: write id-token: write actions: read concurrency: - group: desktop-modules-${{ inputs.channel }} + group: desktop-${{ inputs.channel }} cancel-in-progress: false env: @@ -260,13 +264,14 @@ jobs: owner: fluxerapp repositories: fluxer permission-contents: read - - name: Check the live shell runs these modules + - name: Check which shells run these modules env: GH_TOKEN: ${{ steps.create-token.outputs.token }} - ALLOW_SHELL_DRIFT: ${{ inputs.allow_shell_drift }} + SHELL_CONTRACT: ${{ inputs.shell_contract }} + DESKTOP_MODULES_ONLY_RELEASE: true run: >- cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- build-desktop - --step check_shell_drift + --step check_shell_contract - name: Build the modules-only manifests run: >- diff --git a/.github/workflows/build-desktop.yaml b/.github/workflows/build-desktop.yaml index 983dafd47..64b24bbff 100644 --- a/.github/workflows/build-desktop.yaml +++ b/.github/workflows/build-desktop.yaml @@ -21,6 +21,15 @@ on: required: false default: "" type: string + shell_contract: + description: "Did the shell contract change since the oldest shell these modules still reach? unchanged refuses if it did. compatible keeps older shells on these modules because the renderer still works on them. raise stops sending these modules to shells without the new contract: Linux and managed shells keep their current renderer, Windows and macOS update their shell first." + required: true + type: choice + options: + - unchanged + - compatible + - raise + default: unchanged permissions: contents: write id-token: write @@ -28,7 +37,7 @@ permissions: concurrency: group: desktop-${{ inputs.channel }} - cancel-in-progress: true + cancel-in-progress: false env: CHANNEL: ${{ inputs.channel }} @@ -53,6 +62,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with: ref: main + fetch-depth: 0 - name: Set up Rust toolchain (CI helpers) uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de @@ -78,6 +88,18 @@ jobs: --step set_metadata --channel "${{ inputs.channel }}" + - name: Check which shells run these modules + env: + GH_TOKEN: ${{ steps.create-token.outputs.token }} + CHANNEL: ${{ steps.meta.outputs.build_channel }} + SOURCE_SHA: ${{ steps.meta.outputs.source_sha }} + VERSION: ${{ steps.meta.outputs.version }} + SHELL_CONTRACT: ${{ inputs.shell_contract }} + DESKTOP_MODULES_ONLY_RELEASE: false + run: >- + cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- build-desktop + --step check_shell_contract + matrix: name: Resolve build matrix runs-on: ubuntu-24.04-arm @@ -745,6 +767,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with: ref: ${{ needs.meta.outputs.source_sha }} + fetch-depth: 0 - name: Set up Rust toolchain (CI helpers) uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de @@ -771,6 +794,24 @@ jobs: name: desktop-modules-${{ needs.meta.outputs.build_channel }}-${{ needs.meta.outputs.version }}-${{ needs.meta.outputs.source_sha }} path: desktop-modules + - name: Create token + id: create-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 + with: + client-id: ${{ vars.FLUXER_CI_APP_ID }} + private-key: ${{ secrets.FLUXER_CI_APP_KEY }} + owner: fluxerapp + repositories: fluxer + permission-contents: read + - name: Check which shells run these modules + env: + GH_TOKEN: ${{ steps.create-token.outputs.token }} + SHELL_CONTRACT: ${{ inputs.shell_contract }} + DESKTOP_MODULES_ONLY_RELEASE: false + run: >- + cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- build-desktop + --step check_shell_contract + - name: Build desktop module manifest run: >- cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- build-desktop diff --git a/fluxer_app/src/features/app/state/Updater.ts b/fluxer_app/src/features/app/state/Updater.ts index c67d6f4f1..adb3c3d70 100644 --- a/fluxer_app/src/features/app/state/Updater.ts +++ b/fluxer_app/src/features/app/state/Updater.ts @@ -25,8 +25,7 @@ import { type UpdaterMachineEvent, type UpdaterMachineSnapshot, } from '@app/features/updater/state/UpdaterStateMachine'; -import {buildLinuxManualUpdateOptions} from '@app/features/updater/utils/LinuxManualUpdateOptions'; -import type {UpdaterEvent as NativeUpdaterEvent, UpdaterDownloadOption} from '@app/types/electron.d'; +import type {UpdaterEvent as NativeUpdaterEvent} from '@app/types/electron.d'; import {OFFICIAL_WEB_APP_HOSTS} from '@fluxer/instance_bootstrap/src/OfficialWebAppHosts'; import {makeAutoObservable, runInAction} from 'mobx'; @@ -41,7 +40,6 @@ type UpdaterEvent = context: UpdaterContext; version: string | null; downloadUrl?: string; - downloadOptions?: Array; } | { type: 'not-available'; @@ -66,7 +64,6 @@ type UpdaterEvent = const logger = new Logger('Updater'); const CHECK_INTERVAL_MS = 5 * 60 * 1000; const MIN_CHECK_INTERVAL_MS = 60 * 1000; -const MANUAL_DOWNLOAD_REFRESH_TIMEOUT_MS = 5 * 1000; const WEB_CHECK_TIMEOUT_MS = 15 * 1000; const NATIVE_CHECK_TIMEOUT_MS = 30 * 1000; const VERSION_ENDPOINT = '/version.json'; @@ -120,7 +117,6 @@ function normalizeUpdaterEvent(event: NativeUpdaterEvent): UpdaterEvent | null { context, version: event.version ?? null, downloadUrl: event.downloadUrl, - downloadOptions: event.downloadOptions, }; case 'not-available': return {type: 'not-available', context}; @@ -151,7 +147,7 @@ class Updater { private snapshot: UpdaterMachineSnapshot = createUpdaterMachineSnapshot(); currentVersion: string | null = null; channel: string | null = null; - private desktopArch: string | null = null; + private flatpakAppId: string | null = null; private isNative: boolean; private backgroundCheckStarted = false; private backgroundCheckInterval: number | null = null; @@ -164,7 +160,6 @@ class Updater { private olderShellUpdateDownloaded = false; private olderShellInstallWhenDownloaded = false; private desktopUpdateStarting = false; - private pendingManualDownloadRefreshes = 0; private checkInProgress = false; constructor() { @@ -192,10 +187,6 @@ class Updater { return this.snapshot.context.nativeManualDownloadUrl; } - get nativeManualDownloadOptions(): ReadonlyArray { - return this.snapshot.context.nativeManualDownloadOptions; - } - private get nativeCheckFailed(): boolean { return this.snapshot.context.nativeCheckFailed; } @@ -260,11 +251,16 @@ class Updater { private async bootstrapNative(): Promise { try { - const info = await getClientInfo(); + const [info, desktopInfo] = await Promise.all([ + getClientInfo(), + getElectronAPI() + ?.getDesktopInfo() + .catch(() => null), + ]); runInAction(() => { this.currentVersion = info.desktopVersion ?? null; this.channel = info.desktopChannel ?? null; - this.desktopArch = info.desktopArch ?? info.arch ?? null; + this.flatpakAppId = desktopInfo?.flatpak === true ? (desktopInfo.flatpakAppId ?? null) : null; }); } catch (error) { logger.warn('Failed to read desktop info', error); @@ -376,35 +372,22 @@ class Updater { } private handleNativeEvent(event: UpdaterEvent): void { - const isManualDownloadRefreshResult = - event.context === 'user' && - this.pendingManualDownloadRefreshes > 0 && - (event.type === 'available' || - event.type === 'not-available' || - event.type === 'error' || - event.type === 'unsupported'); - if (isManualDownloadRefreshResult) { - this.pendingManualDownloadRefreshes -= 1; - } - const isUserCheck = event.context === 'user' && !isManualDownloadRefreshResult; + const isUserCheck = event.context === 'user'; const shouldShowImmediateUserResult = isUserCheck && !this.checkInProgress; switch (event.type) { case 'checking': this.transition({type: 'check.started'}); break; - case 'available': { - const manualDownloadOptions = this.resolveManualNativeDownloadOptions(event, event.downloadOptions ?? []); + case 'available': this.transition({ type: 'native.available', version: event.version ?? null, - downloadUrl: manualDownloadOptions[0]?.url ?? event.downloadUrl ?? null, - downloadOptions: manualDownloadOptions, + downloadUrl: getElectronAPI()?.platform === 'linux' ? DESKTOP_DOWNLOAD_URL : (event.downloadUrl ?? null), }); if (shouldShowImmediateUserResult) { this.showCurrentUpdateState(); } break; - } case 'not-available': this.transition({type: 'native.notAvailable', now: Date.now()}); if (shouldShowImmediateUserResult) { @@ -442,22 +425,6 @@ class Updater { } } - private resolveManualNativeDownloadOptions( - event: Extract, - options: ReadonlyArray, - ): ReadonlyArray { - if (getElectronAPI()?.platform !== 'linux') { - return options; - } - return buildLinuxManualUpdateOptions({ - downloadUrl: event.downloadUrl ?? options[0]?.url ?? null, - channel: this.channel ?? Config.PUBLIC_RELEASE_CHANNEL, - arch: this.desktopArch, - version: event.version ?? null, - knownOptions: options, - }); - } - private startBackgroundChecks(): void { if (this.backgroundCheckStarted) return; this.backgroundCheckStarted = true; @@ -601,11 +568,7 @@ class Updater { await this.startDesktopUpdate(); return; } - if (this.isNative && this.nativeUnsupported?.reason === 'managed-package' && !this.updateInfo.web.available) { - showUpdaterModal((modals) => modals.pushUnsupportedUpdateModal('managed-package')); - return; - } - if (this.isNative && this.nativeManualUpdateAvailable && !this.nativeUnsupported) { + if (this.isNative && this.nativeManualUpdateAvailable) { this.showManualNativeUpdateModal(); return; } @@ -617,15 +580,7 @@ class Updater { } if (this.isNative && this.updateInfo.native.available) { logger.info('Native update is available but not installable in-app; opening desktop downloads...'); - const url = this.nativeManualDownloadUrl ?? this.nativeUnsupported?.downloadUrl ?? null; - if (url) { - await openExternalUrl(url); - } else if (this.nativeUnsupported) { - const {reason, downloadUrl} = this.nativeUnsupported; - showUpdaterModal((modals) => modals.pushUnsupportedUpdateModal(reason, downloadUrl)); - } else { - await openExternalUrl(DESKTOP_DOWNLOAD_URL); - } + await openExternalUrl(DESKTOP_DOWNLOAD_URL); } } @@ -663,63 +618,17 @@ class Updater { private showManualNativeUpdateModal(): void { const version = this.updateInfo.native.version; - const options = this.nativeManualDownloadOptions; - if (options.length > 0) { - const currentVersion = this.currentVersion; - showUpdaterModal((modals) => - modals.pushManualUpdateAvailableModal({ - currentVersion, - version, - options, - onDownload: (option) => this.downloadManualNativeUpdateOption(option), - }), - ); + if (getElectronAPI()?.platform === 'linux') { + const flatpakAppId = this.flatpakAppId; + showUpdaterModal((modals) => modals.pushManagedShellUpdateModal({version, flatpakAppId})); return; } - const url = this.nativeManualDownloadUrl ?? this.nativeUnsupported?.downloadUrl ?? DESKTOP_DOWNLOAD_URL; + const url = this.nativeManualDownloadUrl ?? DESKTOP_DOWNLOAD_URL; showUpdaterModal((modals) => modals.pushUpdateAvailableModal(version, () => this.downloadManualNativeUpdateOrOpen(url)), ); } - private async refreshManualNativeDownloadOption(option: UpdaterDownloadOption): Promise { - if (this.checkInProgress) { - return option; - } - this.checkInProgress = true; - this.transition({type: 'check.started'}); - let timeoutId: number | undefined; - const timedOut = new Promise((resolve) => { - timeoutId = window.setTimeout(() => resolve(false), MANUAL_DOWNLOAD_REFRESH_TIMEOUT_MS); - }); - try { - this.pendingManualDownloadRefreshes += 1; - const checked = await Promise.race([this.checkNativeUpdate('user'), timedOut]); - if (!checked) { - return option; - } - return this.nativeManualDownloadOptions.find((candidate) => candidate.format === option.format) ?? option; - } finally { - window.clearTimeout(timeoutId); - this.checkInProgress = false; - this.transition({type: 'check.finished', now: Date.now()}); - } - } - - private async downloadManualNativeUpdateOption(option: UpdaterDownloadOption): Promise { - if (this.manualNativeDownloadInFlight) { - return; - } - this.transition({type: 'manualDownload.started'}); - let currentOption = option; - try { - currentOption = await this.refreshManualNativeDownloadOption(option); - } finally { - this.transition({type: 'manualDownload.finished'}); - } - await this.downloadManualNativeUpdateOrOpen(currentOption.url, currentOption.suggestedName, currentOption.sha256); - } - private async downloadManualNativeUpdateOrOpen( url: string, suggestedName?: string, diff --git a/fluxer_app/src/features/channel/components/channel_header_components/UpdaterIcon.tsx b/fluxer_app/src/features/channel/components/channel_header_components/UpdaterIcon.tsx index 5d91eaa27..ef9477c90 100644 --- a/fluxer_app/src/features/channel/components/channel_header_components/UpdaterIcon.tsx +++ b/fluxer_app/src/features/channel/components/channel_header_components/UpdaterIcon.tsx @@ -22,15 +22,6 @@ const UPDATING_DESCRIPTOR = msg({ comment: 'Tooltip and accessible label on the channel header updater icon in the desktop app while an update the user started is downloading. The app reloads by itself when it is done.', }); -const CHOOSE_LINUX_PACKAGE_DESCRIPTOR = msg({ - message: 'Desktop update {version} available. Choose a Linux package.', - comment: 'Tooltip on the updater icon for a Linux desktop update that requires choosing a package format.', -}); -const CHOOSE_LINUX_PACKAGE_2_DESCRIPTOR = msg({ - message: 'Desktop update available. Choose a Linux package.', - comment: - 'Tooltip on the updater icon for a Linux desktop update that requires choosing a package format and no version is known.', -}); const CLICK_TO_RELOAD_AND_UPDATE_DESCRIPTOR = msg({ message: 'Reload {productName} to use web update {version}', comment: @@ -69,8 +60,6 @@ export const UpdaterIcon = observer(() => { let tooltip: string; if (affordance === 'updating') { tooltip = i18n._(UPDATING_DESCRIPTOR); - } else if (affordance === 'manual' && Updater.nativeManualDownloadOptions.length > 0) { - tooltip = version ? i18n._(CHOOSE_LINUX_PACKAGE_DESCRIPTOR, {version}) : i18n._(CHOOSE_LINUX_PACKAGE_2_DESCRIPTOR); } else if (affordance !== 'web') { tooltip = i18n._(UPDATE_AVAILABLE_DESCRIPTOR); } else { diff --git a/fluxer_app/src/features/i18n/locales/ar/messages.po b/fluxer_app/src/features/i18n/locales/ar/messages.po index d48791aba..9c86f7daa 100644 --- a/fluxer_app/src/features/i18n/locales/ar/messages.po +++ b/fluxer_app/src/features/i18n/locales/ar/messages.po @@ -1802,6 +1802,16 @@ msgstr "يحتاج {PRODUCT_NAME} إلى إعادة التشغيل لتطبيق msgid "{PRODUCT_NAME} needs to restart to disable Chromium's hardware acceleration. Use this only if you're troubleshooting graphics glitches or high GPU usage." msgstr "يحتاج {PRODUCT_NAME} إلى إعادة التشغيل لتعطيل تسريع الأجهزة في Chromium. لا تستخدم هذا الخيار إلا عند معالجة مشكلات في الرسوميات أو ارتفاع استخدام وحدة معالجة الرسوميات (GPU)." +#. Desktop updater modal body for a Flatpak install when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it from your software center." +msgstr "يتوفر {productName} الإصدار {version}. قم بتحديثه من مركز البرامج الخاص بك." + +#. Desktop updater modal body for a Linux install managed by a package manager, such as a deb, rpm or AUR package, when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it with your package manager, or download the new version." +msgstr "الإصدار {version} من {productName} متاح. قم بتحديثه باستخدام مدير الحزم الخاص بك، أو قم بتنزيل الإصدار الجديد." + #. Accessible label for the Fluxer application icon. #: src/features/ui/components/icons/FluxerIconMark.tsx msgid "{productName} application icon" @@ -2669,11 +2679,6 @@ msgstr "بوت مفيد يقوم بأشياء مذهلة!" msgid "a hostname, e.g. {exampleDomain}" msgstr "اسم مضيف، مثل {exampleDomain}" -#. Linux desktop updater modal body shown before version details and package format selector. productName is the app name. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "A Linux desktop update is available. Choose the package format for this system and {productName} will save it locally." -msgstr "يتوفر تحديث لتطبيق سطح المكتب على Linux. اختر تنسيق الحزمة لهذا النظام وسيحفظه {productName} محليًا." - #. Plutonium subscription card line offering a move to a lower price. {listPriceEffectiveDate} is a date already formatted and localized by code; never write a date into the translation. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx msgid "A lower price is available. Switching takes effect on <0>{listPriceEffectiveDate} and changes nothing else." @@ -11427,11 +11432,6 @@ msgstr "السمع معطّل" msgid "Deafened by moderators" msgstr "السمع معطّل من قبل المشرفين" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "DEB" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "DEB package" -msgstr "حزمة DEB" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label in the settings dialog mobile settings view. #: src/features/app/components/dialogs/components/MobileSettingsView.tsx @@ -12095,21 +12095,11 @@ msgstr "تم حظر إشعارات سطح المكتب. يمكنك تمكينه msgid "Desktop startup" msgstr "بدء تشغيل سطح المكتب" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update {version} available. Choose a Linux package." -msgstr "يتوفر تحديث سطح المكتب {version}. اختر حزمة Linux." - #. Modal title shown when a new desktop app update can be downloaded. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop update available" msgstr "يتوفر تحديث لتطبيق سطح المكتب" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format and no version is known. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update available. Choose a Linux package." -msgstr "يتوفر تحديث لتطبيق سطح المكتب. اختر حزمة Linux." - #. Desktop updater modal body. The version placeholder is the target app version; productName is the app name. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop version {version} is available. Download the installer, then run it to update {productName}." @@ -13049,11 +13039,6 @@ msgstr "تنزيل المثبّت" msgid "Download media" msgstr "تنزيل الوسائط" -#. Button label that downloads a selected Linux desktop update package. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Download package" -msgstr "تنزيل الحزمة" - #. Button that downloads the account recovery kit as a PDF file. #: src/features/auth/components/modals/RecoveryKitModal.tsx msgid "Download PDF" @@ -18280,11 +18265,6 @@ msgstr "ثبّته من قائمة متصفحك." msgid "Install the new app" msgstr "تثبيت التطبيق الجديد" -#. Label for the installed desktop version in the Linux update modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Installed" -msgstr "مثبّت" - #. Desktop updater modal body. The currentVersion placeholder is the installed app version. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Installed version: {currentVersion}." @@ -19794,11 +19774,6 @@ msgstr "تطبيق Linux" msgid "Linux default" msgstr "الافتراضي في Linux" -#. Select label for choosing the Linux desktop package format to download. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Linux package" -msgstr "حزمة لينكس" - #. Forum layout name shown inside an activity log row. #. Forum layout option that shows posts as rows. #: src/features/forum/utils/ForumMessageDescriptors.ts @@ -22430,11 +22405,6 @@ msgstr "ستتوقف المواضيع الجديدة عن الظهور في قا msgid "New users can't join right now. You can still create an account or sign in, then try this link again later." msgstr "لا يمكن للمستخدمين الجدد الانضمام الآن. لا يزال بإمكانك إنشاء حساب أو تسجيل الدخول، ثم إعادة المحاولة باستخدام هذا الرابط لاحقًا." -#. Fallback version label when the available desktop version is unknown. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "New version" -msgstr "إصدار جديد" - #. US state name, shown next to the country name in the mature content gate. #: src/features/user/utils/UserGeo.ts msgctxt "us-state" @@ -24142,6 +24112,8 @@ msgid "Open settings" msgstr "فتح الإعدادات" #. Button label that opens the software center via appstream protocol. +#. Native desktop dialog button on a Flatpak install that opens the software center page for the desktop app. +#: src/features/platform/utils/DesktopLocaleBridge.ts #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Open software center" msgstr "فتح مركز البرامج" @@ -28464,11 +28436,6 @@ msgstr "مربع اختيار دائري" msgid "Rows per page" msgstr "الصفوف لكل صفحة" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "RPM" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "RPM package" -msgstr "حزمة RPM" - #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts msgid "Run {productName} automatically when your computer starts" @@ -33120,11 +33087,6 @@ msgstr "اضغط على \"مشاركة\"، ثم \"إضافة إلى الشاشة msgid "Tap to add a note" msgstr "اضغط لإضافة ملاحظة" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "tar.gz" verbatim and translate only the word "archive". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "tar.gz archive" -msgstr "أرشيف tar.gz" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/SharedDescriptors.ts msgid "Taskbar badge" @@ -33513,11 +33475,6 @@ msgstr "يتم ترقية الاتصال إلى TLS فقط إذا كان الخ msgid "The desktop update could not be downloaded. Try again from the update button when your connection is stable." msgstr "تعذر تنزيل تحديث سطح المكتب. حاول مرة أخرى من زر التحديث عندما يصبح اتصالك مستقرًا." -#. Help text below the Linux package format selector in the desktop updater modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "The downloaded file is saved to your computer. Install it with your normal package manager." -msgstr "حُفظ الملف الذي تم تنزيله على جهاز الكمبيوتر. ثبّته باستخدام مدير الحزم الذي تستخدمه عادةً." - #. Activity log detail line under a deleted emoji, shown only when someone other than the person who deleted it had uploaded it. {user} is the user who uploaded the emoji, shown as a clickable user chip. #: src/features/guild/utils/guild_tabs/audit_log/AuditLogExpressionMessageMessages.ts msgid "The emoji was uploaded by {user}" @@ -36548,11 +36505,21 @@ msgstr "حتى 4K بمعدل 60 إطارًا في الثانية" msgid "Up to 720p at 30 FPS" msgstr "حتى 720p بمعدل 30 إطارًا في الثانية" +#. Native desktop dialog body under the headline announcing a new version, shown on a Flatpak install. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} from your software center." +msgstr "تحديث {appName} من مركز البرامج الخاص بك." + #. Tray menu item shown only while a desktop update is waiting. Clicking it installs the update. {appName} is the desktop app name (typically Fluxer). #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "Update {appName} now" msgstr "تحديث {appName} الآن" +#. Native desktop dialog body under the headline announcing a new version, shown on a Linux install managed by a package manager such as a deb, rpm or AUR package. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} with your package manager, or download the new version." +msgstr "حدّث {appName} باستخدام مدير الحزم الخاص بك، أو قم بتنزيل الإصدار الجديد." + #. Linux settings text when the installed desktop app is too old for system-wide shortcuts. {productName} is the app name. #: src/features/user/components/modals/tabs/components/SystemShortcutsSection.tsx msgid "Update {productName} to use system-wide shortcuts on Wayland." diff --git a/fluxer_app/src/features/i18n/locales/auto-i18n-reviewed-unchanged.json b/fluxer_app/src/features/i18n/locales/auto-i18n-reviewed-unchanged.json index 08781683e..7de950115 100644 --- a/fluxer_app/src/features/i18n/locales/auto-i18n-reviewed-unchanged.json +++ b/fluxer_app/src/features/i18n/locales/auto-i18n-reviewed-unchanged.json @@ -1608,6 +1608,7 @@ "Update passkey", "Update your passkey", "Update {appName} now", + "Update {appName} with your package manager, or download the new version.", "Update {productName} to use system-wide shortcuts on Wayland.", "Updates are on their way!", "Updates from this channel will stop arriving here. Messages already delivered stay. You can follow it again from the announcement channel.", @@ -1818,6 +1819,7 @@ "{productName} recovery kit", "{productName} registered its shortcuts with Hyprland. Bind them in your Hyprland config to use them while {productName} is not focused.", "{productName} wordmark", + "{productName} {version} is available. Update it with your package manager, or download the new version.", "{remaining, plural, one {+# more} other {+# more}}", "{renderedOnlineCount} online", "{requestedLabel} could not start. Using {effectiveLabel} instead.", diff --git a/fluxer_app/src/features/i18n/locales/bg/messages.po b/fluxer_app/src/features/i18n/locales/bg/messages.po index 0b23f7bf5..028bbc22f 100644 --- a/fluxer_app/src/features/i18n/locales/bg/messages.po +++ b/fluxer_app/src/features/i18n/locales/bg/messages.po @@ -1802,6 +1802,16 @@ msgstr "{PRODUCT_NAME} трябва да се рестартира, за да в msgid "{PRODUCT_NAME} needs to restart to disable Chromium's hardware acceleration. Use this only if you're troubleshooting graphics glitches or high GPU usage." msgstr "{PRODUCT_NAME} трябва да се рестартира, за да изключи хардуерното ускорение на Chromium. Използвай това само ако отстраняваш графични проблеми или високо натоварване на графичния процесор." +#. Desktop updater modal body for a Flatpak install when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it from your software center." +msgstr "{productName} {version} е наличен. Актуализирайте го от вашия софтуерен център." + +#. Desktop updater modal body for a Linux install managed by a package manager, such as a deb, rpm or AUR package, when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it with your package manager, or download the new version." +msgstr "{productName} {version} е наличен. Актуализирайте го с вашия пакетен мениджър или изтеглете новата версия." + #. Accessible label for the Fluxer application icon. #: src/features/ui/components/icons/FluxerIconMark.tsx msgid "{productName} application icon" @@ -2669,11 +2679,6 @@ msgstr "Полезен бот, който прави невероятни нещ msgid "a hostname, e.g. {exampleDomain}" msgstr "име на хост, напр. {exampleDomain}" -#. Linux desktop updater modal body shown before version details and package format selector. productName is the app name. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "A Linux desktop update is available. Choose the package format for this system and {productName} will save it locally." -msgstr "Налична е актуализация за настолното приложение на Linux. Избери формата на пакета за тази система и {productName} ще я запази локално." - #. Plutonium subscription card line offering a move to a lower price. {listPriceEffectiveDate} is a date already formatted and localized by code; never write a date into the translation. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx msgid "A lower price is available. Switching takes effect on <0>{listPriceEffectiveDate} and changes nothing else." @@ -11427,11 +11432,6 @@ msgstr "Звукът е изключен" msgid "Deafened by moderators" msgstr "Звукът е изключен от модератори" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "DEB" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "DEB package" -msgstr "DEB пакет" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label in the settings dialog mobile settings view. #: src/features/app/components/dialogs/components/MobileSettingsView.tsx @@ -12095,21 +12095,11 @@ msgstr "Известията на работния плот са блокира msgid "Desktop startup" msgstr "Стартиране с компютъра" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update {version} available. Choose a Linux package." -msgstr "Налична е актуализация {version} за настолното приложение. Избери пакет за Linux." - #. Modal title shown when a new desktop app update can be downloaded. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop update available" msgstr "Налична е актуализация за настолното приложение" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format and no version is known. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update available. Choose a Linux package." -msgstr "Налична е актуализация за настолното приложение. Избери пакет за Linux." - #. Desktop updater modal body. The version placeholder is the target app version; productName is the app name. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop version {version} is available. Download the installer, then run it to update {productName}." @@ -13049,11 +13039,6 @@ msgstr "Изтегляне на инсталатор" msgid "Download media" msgstr "Изтегляне на медия" -#. Button label that downloads a selected Linux desktop update package. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Download package" -msgstr "Изтегляне на пакет" - #. Button that downloads the account recovery kit as a PDF file. #: src/features/auth/components/modals/RecoveryKitModal.tsx msgid "Download PDF" @@ -18280,11 +18265,6 @@ msgstr "Инсталирай го от менюто на браузъра си." msgid "Install the new app" msgstr "Инсталирай новото приложение" -#. Label for the installed desktop version in the Linux update modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Installed" -msgstr "Инсталирана" - #. Desktop updater modal body. The currentVersion placeholder is the installed app version. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Installed version: {currentVersion}." @@ -19794,11 +19774,6 @@ msgstr "Приложение за Linux" msgid "Linux default" msgstr "По подразбиране за Linux" -#. Select label for choosing the Linux desktop package format to download. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Linux package" -msgstr "Пакет за Linux" - #. Forum layout name shown inside an activity log row. #. Forum layout option that shows posts as rows. #: src/features/forum/utils/ForumMessageDescriptors.ts @@ -22430,11 +22405,6 @@ msgstr "Новите нишки ще спрат да се показват в с msgid "New users can't join right now. You can still create an account or sign in, then try this link again later." msgstr "В момента нови потребители не могат да се присъединят. Все пак можеш да създадеш акаунт или да влезеш, а след това опитай отново с този линк по-късно." -#. Fallback version label when the available desktop version is unknown. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "New version" -msgstr "Нова версия" - #. US state name, shown next to the country name in the mature content gate. #: src/features/user/utils/UserGeo.ts msgctxt "us-state" @@ -24142,6 +24112,8 @@ msgid "Open settings" msgstr "Отваряне на настройките" #. Button label that opens the software center via appstream protocol. +#. Native desktop dialog button on a Flatpak install that opens the software center page for the desktop app. +#: src/features/platform/utils/DesktopLocaleBridge.ts #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Open software center" msgstr "Отваряне на софтуерния център" @@ -28464,11 +28436,6 @@ msgstr "Кръгло поле за отметка" msgid "Rows per page" msgstr "Редове на страница" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "RPM" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "RPM package" -msgstr "RPM пакет" - #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts msgid "Run {productName} automatically when your computer starts" @@ -33120,11 +33087,6 @@ msgstr "Докосни „Споделяне“, след което „Доба msgid "Tap to add a note" msgstr "Докосни, за да добавиш бележка" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "tar.gz" verbatim and translate only the word "archive". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "tar.gz archive" -msgstr "tar.gz архив" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/SharedDescriptors.ts msgid "Taskbar badge" @@ -33513,11 +33475,6 @@ msgstr "Връзката се надгражда до TLS само ако сър msgid "The desktop update could not be downloaded. Try again from the update button when your connection is stable." msgstr "Актуализацията за настолното приложение не можа да бъде изтеглена. Опитай отново от бутона за актуализация, когато връзката ти е стабилна." -#. Help text below the Linux package format selector in the desktop updater modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "The downloaded file is saved to your computer. Install it with your normal package manager." -msgstr "Изтегленият файл е запазен на компютъра ти. Инсталирай го с обичайния си мениджър на пакети." - #. Activity log detail line under a deleted emoji, shown only when someone other than the person who deleted it had uploaded it. {user} is the user who uploaded the emoji, shown as a clickable user chip. #: src/features/guild/utils/guild_tabs/audit_log/AuditLogExpressionMessageMessages.ts msgid "The emoji was uploaded by {user}" @@ -36548,11 +36505,21 @@ msgstr "До 4K при 60 FPS" msgid "Up to 720p at 30 FPS" msgstr "До 720p при 30 FPS" +#. Native desktop dialog body under the headline announcing a new version, shown on a Flatpak install. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} from your software center." +msgstr "Актуализирайте {appName} от вашия софтуерен център." + #. Tray menu item shown only while a desktop update is waiting. Clicking it installs the update. {appName} is the desktop app name (typically Fluxer). #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "Update {appName} now" msgstr "Актуализирай {appName} сега" +#. Native desktop dialog body under the headline announcing a new version, shown on a Linux install managed by a package manager such as a deb, rpm or AUR package. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} with your package manager, or download the new version." +msgstr "Актуализирайте {appName} с вашия пакетен мениджър или изтеглете новата версия." + #. Linux settings text when the installed desktop app is too old for system-wide shortcuts. {productName} is the app name. #: src/features/user/components/modals/tabs/components/SystemShortcutsSection.tsx msgid "Update {productName} to use system-wide shortcuts on Wayland." diff --git a/fluxer_app/src/features/i18n/locales/cs/messages.po b/fluxer_app/src/features/i18n/locales/cs/messages.po index 3b69024a6..e14169344 100644 --- a/fluxer_app/src/features/i18n/locales/cs/messages.po +++ b/fluxer_app/src/features/i18n/locales/cs/messages.po @@ -1802,6 +1802,16 @@ msgstr "Aby se změna záhlaví okna projevila, je potřeba restartovat aplikaci msgid "{PRODUCT_NAME} needs to restart to disable Chromium's hardware acceleration. Use this only if you're troubleshooting graphics glitches or high GPU usage." msgstr "Aplikace {PRODUCT_NAME} se musí restartovat, aby se vypnula hardwarová akcelerace Chromia. Tuto možnost použijte pouze při řešení problémů s grafikou nebo vysokým využitím GPU." +#. Desktop updater modal body for a Flatpak install when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it from your software center." +msgstr "{productName} verze {version} je k dispozici. Aktualizujte ji ze svého centra softwaru." + +#. Desktop updater modal body for a Linux install managed by a package manager, such as a deb, rpm or AUR package, when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it with your package manager, or download the new version." +msgstr "Je k dispozici {productName} {version}. Aktualizujte jej pomocí správce balíčků nebo si stáhněte novou verzi." + #. Accessible label for the Fluxer application icon. #: src/features/ui/components/icons/FluxerIconMark.tsx msgid "{productName} application icon" @@ -2669,11 +2679,6 @@ msgstr "Užitečný bot, který umí úžasné věci!" msgid "a hostname, e.g. {exampleDomain}" msgstr "název hostitele, např. {exampleDomain}" -#. Linux desktop updater modal body shown before version details and package format selector. productName is the app name. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "A Linux desktop update is available. Choose the package format for this system and {productName} will save it locally." -msgstr "Je k dispozici aktualizace desktopové aplikace pro Linux. Vyberte formát balíčku pro tento systém a aplikace {productName} jej uloží do počítače." - #. Plutonium subscription card line offering a move to a lower price. {listPriceEffectiveDate} is a date already formatted and localized by code; never write a date into the translation. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx msgid "A lower price is available. Switching takes effect on <0>{listPriceEffectiveDate} and changes nothing else." @@ -11427,11 +11432,6 @@ msgstr "Zvuk vypnutý" msgid "Deafened by moderators" msgstr "Zvuk vypnutý moderátory" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "DEB" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "DEB package" -msgstr "Balíček DEB" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label in the settings dialog mobile settings view. #: src/features/app/components/dialogs/components/MobileSettingsView.tsx @@ -12095,21 +12095,11 @@ msgstr "Oznámení na plochu byla zablokována. Můžete je povolit později v n msgid "Desktop startup" msgstr "Spuštění na počítači" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update {version} available. Choose a Linux package." -msgstr "Je k dispozici aktualizace desktopové aplikace {version}. Vyberte balíček pro Linux." - #. Modal title shown when a new desktop app update can be downloaded. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop update available" msgstr "Je k dispozici aktualizace desktopové aplikace" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format and no version is known. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update available. Choose a Linux package." -msgstr "Je k dispozici aktualizace desktopové aplikace. Vyberte balíček pro Linux." - #. Desktop updater modal body. The version placeholder is the target app version; productName is the app name. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop version {version} is available. Download the installer, then run it to update {productName}." @@ -13049,11 +13039,6 @@ msgstr "Stáhnout instalační program" msgid "Download media" msgstr "Stáhnout média" -#. Button label that downloads a selected Linux desktop update package. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Download package" -msgstr "Stáhnout balíček" - #. Button that downloads the account recovery kit as a PDF file. #: src/features/auth/components/modals/RecoveryKitModal.tsx msgid "Download PDF" @@ -18280,11 +18265,6 @@ msgstr "Nainstalujte si ho z nabídky prohlížeče." msgid "Install the new app" msgstr "Nainstalovat novou aplikaci" -#. Label for the installed desktop version in the Linux update modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Installed" -msgstr "Nainstalováno" - #. Desktop updater modal body. The currentVersion placeholder is the installed app version. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Installed version: {currentVersion}." @@ -19794,11 +19774,6 @@ msgstr "Aplikace pro Linux" msgid "Linux default" msgstr "Výchozí zařízení v systému Linux" -#. Select label for choosing the Linux desktop package format to download. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Linux package" -msgstr "Balíček pro Linux" - #. Forum layout name shown inside an activity log row. #. Forum layout option that shows posts as rows. #: src/features/forum/utils/ForumMessageDescriptors.ts @@ -22430,11 +22405,6 @@ msgstr "Nové vlákna se po této době nečinnosti přestanou zobrazovat v sezn msgid "New users can't join right now. You can still create an account or sign in, then try this link again later." msgstr "Noví uživatelé se teď nemůžou připojit. Můžete si ale vytvořit účet nebo se přihlásit a zkusit tento odkaz znovu později." -#. Fallback version label when the available desktop version is unknown. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "New version" -msgstr "Nová verze" - #. US state name, shown next to the country name in the mature content gate. #: src/features/user/utils/UserGeo.ts msgctxt "us-state" @@ -24142,6 +24112,8 @@ msgid "Open settings" msgstr "Otevřít nastavení" #. Button label that opens the software center via appstream protocol. +#. Native desktop dialog button on a Flatpak install that opens the software center page for the desktop app. +#: src/features/platform/utils/DesktopLocaleBridge.ts #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Open software center" msgstr "Otevřít centrum softwaru" @@ -28464,11 +28436,6 @@ msgstr "Kulaté zaškrtávací políčko" msgid "Rows per page" msgstr "Řádků na stránku" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "RPM" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "RPM package" -msgstr "Balíček RPM" - #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts msgid "Run {productName} automatically when your computer starts" @@ -33120,11 +33087,6 @@ msgstr "Klepněte na Sdílet a poté na Přidat na plochu." msgid "Tap to add a note" msgstr "Klepnutím přidáte poznámku" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "tar.gz" verbatim and translate only the word "archive". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "tar.gz archive" -msgstr "Archiv tar.gz" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/SharedDescriptors.ts msgid "Taskbar badge" @@ -33513,11 +33475,6 @@ msgstr "Připojení se upgraduje na TLS pouze v případě, že jej server nabí msgid "The desktop update could not be downloaded. Try again from the update button when your connection is stable." msgstr "Aktualizaci desktopové aplikace se nepodařilo stáhnout. Až bude připojení stabilní, zkuste to znovu pomocí tlačítka aktualizace." -#. Help text below the Linux package format selector in the desktop updater modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "The downloaded file is saved to your computer. Install it with your normal package manager." -msgstr "Stažený soubor je uložen do vašeho počítače. Nainstalujte jej pomocí svého správce balíčků." - #. Activity log detail line under a deleted emoji, shown only when someone other than the person who deleted it had uploaded it. {user} is the user who uploaded the emoji, shown as a clickable user chip. #: src/features/guild/utils/guild_tabs/audit_log/AuditLogExpressionMessageMessages.ts msgid "The emoji was uploaded by {user}" @@ -36548,11 +36505,21 @@ msgstr "Až 4K při 60 snímcích za sekundu" msgid "Up to 720p at 30 FPS" msgstr "Až 720p při 30 snímcích za sekundu" +#. Native desktop dialog body under the headline announcing a new version, shown on a Flatpak install. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} from your software center." +msgstr "Aktualizujte {appName} ze svého centra softwaru." + #. Tray menu item shown only while a desktop update is waiting. Clicking it installs the update. {appName} is the desktop app name (typically Fluxer). #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "Update {appName} now" msgstr "Aktualizovat {appName} nyní" +#. Native desktop dialog body under the headline announcing a new version, shown on a Linux install managed by a package manager such as a deb, rpm or AUR package. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} with your package manager, or download the new version." +msgstr "Aktualizujte {appName} pomocí správce balíčků nebo si stáhněte novou verzi." + #. Linux settings text when the installed desktop app is too old for system-wide shortcuts. {productName} is the app name. #: src/features/user/components/modals/tabs/components/SystemShortcutsSection.tsx msgid "Update {productName} to use system-wide shortcuts on Wayland." diff --git a/fluxer_app/src/features/i18n/locales/da/messages.po b/fluxer_app/src/features/i18n/locales/da/messages.po index 4d1252e2d..9d54ff2eb 100644 --- a/fluxer_app/src/features/i18n/locales/da/messages.po +++ b/fluxer_app/src/features/i18n/locales/da/messages.po @@ -1802,6 +1802,16 @@ msgstr "{PRODUCT_NAME} skal genstartes, for at ændringen af titellinjen kan tr msgid "{PRODUCT_NAME} needs to restart to disable Chromium's hardware acceleration. Use this only if you're troubleshooting graphics glitches or high GPU usage." msgstr "{PRODUCT_NAME} skal genstartes for at deaktivere Chromiums hardwareacceleration. Brug kun dette, hvis du fejlfinder grafiske fejl eller høj GPU-brug." +#. Desktop updater modal body for a Flatpak install when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it from your software center." +msgstr "{productName} {version} er tilgængelig. Opdater den fra dit softwarecenter." + +#. Desktop updater modal body for a Linux install managed by a package manager, such as a deb, rpm or AUR package, when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it with your package manager, or download the new version." +msgstr "{productName} {version} er tilgængelig. Opdater den med din pakkehåndtering, eller download den nye version." + #. Accessible label for the Fluxer application icon. #: src/features/ui/components/icons/FluxerIconMark.tsx msgid "{productName} application icon" @@ -2669,11 +2679,6 @@ msgstr "En hjælpsom bot, der kan fantastiske ting!" msgid "a hostname, e.g. {exampleDomain}" msgstr "et værtsnavn, f.eks. {exampleDomain}" -#. Linux desktop updater modal body shown before version details and package format selector. productName is the app name. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "A Linux desktop update is available. Choose the package format for this system and {productName} will save it locally." -msgstr "En opdatering til Linux-appen er tilgængelig. Vælg pakkeformatet for dette system, så gemmer {productName} det lokalt." - #. Plutonium subscription card line offering a move to a lower price. {listPriceEffectiveDate} is a date already formatted and localized by code; never write a date into the translation. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx msgid "A lower price is available. Switching takes effect on <0>{listPriceEffectiveDate} and changes nothing else." @@ -11427,11 +11432,6 @@ msgstr "Lyden er slået fra" msgid "Deafened by moderators" msgstr "Lyden er slået fra af moderatorer" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "DEB" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "DEB package" -msgstr "DEB-pakke" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label in the settings dialog mobile settings view. #: src/features/app/components/dialogs/components/MobileSettingsView.tsx @@ -12095,21 +12095,11 @@ msgstr "Notifikationer på computeren er blevet blokeret. Du kan aktivere dem se msgid "Desktop startup" msgstr "Opstart af computerapp" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update {version} available. Choose a Linux package." -msgstr "Version {version} af computerappen er tilgængelig. Vælg en Linux-pakke." - #. Modal title shown when a new desktop app update can be downloaded. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop update available" msgstr "Opdatering til computerappen tilgængelig" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format and no version is known. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update available. Choose a Linux package." -msgstr "Opdatering til computerappen tilgængelig. Vælg en Linux-pakke." - #. Desktop updater modal body. The version placeholder is the target app version; productName is the app name. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop version {version} is available. Download the installer, then run it to update {productName}." @@ -13049,11 +13039,6 @@ msgstr "Download installationsprogram" msgid "Download media" msgstr "Download medier" -#. Button label that downloads a selected Linux desktop update package. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Download package" -msgstr "Download pakke" - #. Button that downloads the account recovery kit as a PDF file. #: src/features/auth/components/modals/RecoveryKitModal.tsx msgid "Download PDF" @@ -18280,11 +18265,6 @@ msgstr "Installer det fra browsermenuen." msgid "Install the new app" msgstr "Installer den nye app" -#. Label for the installed desktop version in the Linux update modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Installed" -msgstr "Installeret" - #. Desktop updater modal body. The currentVersion placeholder is the installed app version. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Installed version: {currentVersion}." @@ -19794,11 +19774,6 @@ msgstr "Linux-app" msgid "Linux default" msgstr "Linux-standard" -#. Select label for choosing the Linux desktop package format to download. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Linux package" -msgstr "Linux-pakke" - #. Forum layout name shown inside an activity log row. #. Forum layout option that shows posts as rows. #: src/features/forum/utils/ForumMessageDescriptors.ts @@ -22430,11 +22405,6 @@ msgstr "Nye tråde vises ikke længere på kanallisten efter denne periode med i msgid "New users can't join right now. You can still create an account or sign in, then try this link again later." msgstr "Nye brugere kan ikke blive medlem lige nu. Du kan stadig oprette en konto eller logge ind og derefter prøve dette link igen senere." -#. Fallback version label when the available desktop version is unknown. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "New version" -msgstr "Ny version" - #. US state name, shown next to the country name in the mature content gate. #: src/features/user/utils/UserGeo.ts msgctxt "us-state" @@ -24142,6 +24112,8 @@ msgid "Open settings" msgstr "Åbn indstillinger" #. Button label that opens the software center via appstream protocol. +#. Native desktop dialog button on a Flatpak install that opens the software center page for the desktop app. +#: src/features/platform/utils/DesktopLocaleBridge.ts #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Open software center" msgstr "Åbn softwarecenter" @@ -28464,11 +28436,6 @@ msgstr "Afkrydsningsfelt i rund stil" msgid "Rows per page" msgstr "Rækker pr. side" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "RPM" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "RPM package" -msgstr "RPM-pakke" - #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts msgid "Run {productName} automatically when your computer starts" @@ -33120,11 +33087,6 @@ msgstr "Tryk på Del, og vælg derefter Føj til hjemmeskærm." msgid "Tap to add a note" msgstr "Tryk for at tilføje en note" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "tar.gz" verbatim and translate only the word "archive". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "tar.gz archive" -msgstr "tar.gz-arkiv" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/SharedDescriptors.ts msgid "Taskbar badge" @@ -33513,11 +33475,6 @@ msgstr "Forbindelsen opgraderes kun til TLS, hvis serveren tilbyder det." msgid "The desktop update could not be downloaded. Try again from the update button when your connection is stable." msgstr "Opdateringen til computerappen kunne ikke downloades. Prøv igen via opdateringsknappen, når din forbindelse er stabil." -#. Help text below the Linux package format selector in the desktop updater modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "The downloaded file is saved to your computer. Install it with your normal package manager." -msgstr "Den downloadede fil gemmes på din computer. Installer den med din normale pakkehåndtering." - #. Activity log detail line under a deleted emoji, shown only when someone other than the person who deleted it had uploaded it. {user} is the user who uploaded the emoji, shown as a clickable user chip. #: src/features/guild/utils/guild_tabs/audit_log/AuditLogExpressionMessageMessages.ts msgid "The emoji was uploaded by {user}" @@ -36548,11 +36505,21 @@ msgstr "Op til 4K ved 60 FPS" msgid "Up to 720p at 30 FPS" msgstr "Op til 720p ved 30 billeder/sek" +#. Native desktop dialog body under the headline announcing a new version, shown on a Flatpak install. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} from your software center." +msgstr "Opdater {appName} fra dit softwarecenter." + #. Tray menu item shown only while a desktop update is waiting. Clicking it installs the update. {appName} is the desktop app name (typically Fluxer). #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "Update {appName} now" msgstr "Opdater {appName} nu" +#. Native desktop dialog body under the headline announcing a new version, shown on a Linux install managed by a package manager such as a deb, rpm or AUR package. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} with your package manager, or download the new version." +msgstr "Opdater {appName} med din pakkehåndtering, eller download den nye version." + #. Linux settings text when the installed desktop app is too old for system-wide shortcuts. {productName} is the app name. #: src/features/user/components/modals/tabs/components/SystemShortcutsSection.tsx msgid "Update {productName} to use system-wide shortcuts on Wayland." diff --git a/fluxer_app/src/features/i18n/locales/de/messages.po b/fluxer_app/src/features/i18n/locales/de/messages.po index 6a58a846f..c010b5458 100644 --- a/fluxer_app/src/features/i18n/locales/de/messages.po +++ b/fluxer_app/src/features/i18n/locales/de/messages.po @@ -1802,6 +1802,16 @@ msgstr "{PRODUCT_NAME} muss neu gestartet werden, damit die Änderung der Titell msgid "{PRODUCT_NAME} needs to restart to disable Chromium's hardware acceleration. Use this only if you're troubleshooting graphics glitches or high GPU usage." msgstr "{PRODUCT_NAME} muss neu gestartet werden, um die Hardwarebeschleunigung von Chromium zu deaktivieren. Verwende dies nur, wenn du Grafikfehler oder eine hohe GPU-Auslastung beheben möchtest." +#. Desktop updater modal body for a Flatpak install when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it from your software center." +msgstr "{productName} {version} ist verfügbar. Aktualisiere es über dein Softwarecenter." + +#. Desktop updater modal body for a Linux install managed by a package manager, such as a deb, rpm or AUR package, when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it with your package manager, or download the new version." +msgstr "{productName} {version} ist verfügbar. Aktualisiere es mit deinem Paketmanager oder lade die neue Version herunter." + #. Accessible label for the Fluxer application icon. #: src/features/ui/components/icons/FluxerIconMark.tsx msgid "{productName} application icon" @@ -2669,11 +2679,6 @@ msgstr "Ein hilfreicher Bot, der tolle Dinge tut!" msgid "a hostname, e.g. {exampleDomain}" msgstr "ein Hostname, z. B. {exampleDomain}" -#. Linux desktop updater modal body shown before version details and package format selector. productName is the app name. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "A Linux desktop update is available. Choose the package format for this system and {productName} will save it locally." -msgstr "Ein Desktop-Update für Linux ist verfügbar. Wähle das Paketformat für dieses System und {productName} speichert es lokal." - #. Plutonium subscription card line offering a move to a lower price. {listPriceEffectiveDate} is a date already formatted and localized by code; never write a date into the translation. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx msgid "A lower price is available. Switching takes effect on <0>{listPriceEffectiveDate} and changes nothing else." @@ -11427,11 +11432,6 @@ msgstr "Ton ausgeschaltet" msgid "Deafened by moderators" msgstr "Ton von Moderatoren ausgeschaltet" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "DEB" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "DEB package" -msgstr "DEB-Paket" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label in the settings dialog mobile settings view. #: src/features/app/components/dialogs/components/MobileSettingsView.tsx @@ -12095,21 +12095,11 @@ msgstr "Desktop-Benachrichtigungen wurden blockiert. Du kannst sie später in de msgid "Desktop startup" msgstr "Desktop-Start" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update {version} available. Choose a Linux package." -msgstr "Desktop-Update {version} verfügbar. Wähle ein Linux-Paket." - #. Modal title shown when a new desktop app update can be downloaded. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop update available" msgstr "Desktop-Update verfügbar" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format and no version is known. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update available. Choose a Linux package." -msgstr "Desktop-Update verfügbar. Wähle ein Linux-Paket." - #. Desktop updater modal body. The version placeholder is the target app version; productName is the app name. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop version {version} is available. Download the installer, then run it to update {productName}." @@ -13049,11 +13039,6 @@ msgstr "Installer herunterladen" msgid "Download media" msgstr "Medien herunterladen" -#. Button label that downloads a selected Linux desktop update package. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Download package" -msgstr "Paket herunterladen" - #. Button that downloads the account recovery kit as a PDF file. #: src/features/auth/components/modals/RecoveryKitModal.tsx msgid "Download PDF" @@ -18280,11 +18265,6 @@ msgstr "Installiere es über das Menü deines Browsers." msgid "Install the new app" msgstr "Neue App installieren" -#. Label for the installed desktop version in the Linux update modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Installed" -msgstr "Installiert" - #. Desktop updater modal body. The currentVersion placeholder is the installed app version. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Installed version: {currentVersion}." @@ -19794,11 +19774,6 @@ msgstr "Linux-App" msgid "Linux default" msgstr "Linux-Standard" -#. Select label for choosing the Linux desktop package format to download. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Linux package" -msgstr "Linux-Paket" - #. Forum layout name shown inside an activity log row. #. Forum layout option that shows posts as rows. #: src/features/forum/utils/ForumMessageDescriptors.ts @@ -22430,11 +22405,6 @@ msgstr "Neue Threads werden nach dieser Inaktivitätsperiode nicht mehr in der K msgid "New users can't join right now. You can still create an account or sign in, then try this link again later." msgstr "Neue Benutzer können im Moment nicht beitreten. Du kannst aber trotzdem einen Account erstellen oder dich anmelden und es später noch einmal mit diesem Link versuchen." -#. Fallback version label when the available desktop version is unknown. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "New version" -msgstr "Neue Version" - #. US state name, shown next to the country name in the mature content gate. #: src/features/user/utils/UserGeo.ts msgctxt "us-state" @@ -24142,6 +24112,8 @@ msgid "Open settings" msgstr "Einstellungen öffnen" #. Button label that opens the software center via appstream protocol. +#. Native desktop dialog button on a Flatpak install that opens the software center page for the desktop app. +#: src/features/platform/utils/DesktopLocaleBridge.ts #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Open software center" msgstr "Softwarecenter öffnen" @@ -28464,11 +28436,6 @@ msgstr "Rundes Kästchen" msgid "Rows per page" msgstr "Zeilen pro Seite" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "RPM" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "RPM package" -msgstr "RPM-Paket" - #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts msgid "Run {productName} automatically when your computer starts" @@ -33120,11 +33087,6 @@ msgstr "Tippe auf „Teilen“ und dann auf „Zum Home-Bildschirm hinzufügen msgid "Tap to add a note" msgstr "Tippen, um eine Notiz hinzuzufügen" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "tar.gz" verbatim and translate only the word "archive". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "tar.gz archive" -msgstr "tar.gz-Archiv" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/SharedDescriptors.ts msgid "Taskbar badge" @@ -33513,11 +33475,6 @@ msgstr "Die Verbindung wird nur dann auf TLS aktualisiert, wenn der Server dies msgid "The desktop update could not be downloaded. Try again from the update button when your connection is stable." msgstr "Das Desktop-Update konnte nicht heruntergeladen werden. Versuche es erneut über die Schaltfläche „Update“, wenn deine Verbindung stabil ist." -#. Help text below the Linux package format selector in the desktop updater modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "The downloaded file is saved to your computer. Install it with your normal package manager." -msgstr "Die heruntergeladene Datei ist auf deinem Computer gespeichert. Installiere sie mit deinem üblichen Paketmanager." - #. Activity log detail line under a deleted emoji, shown only when someone other than the person who deleted it had uploaded it. {user} is the user who uploaded the emoji, shown as a clickable user chip. #: src/features/guild/utils/guild_tabs/audit_log/AuditLogExpressionMessageMessages.ts msgid "The emoji was uploaded by {user}" @@ -36548,11 +36505,21 @@ msgstr "Bis zu 4K bei 60 FPS" msgid "Up to 720p at 30 FPS" msgstr "Bis zu 720p bei 30 FPS" +#. Native desktop dialog body under the headline announcing a new version, shown on a Flatpak install. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} from your software center." +msgstr "Aktualisiere {appName} über dein Software-Center." + #. Tray menu item shown only while a desktop update is waiting. Clicking it installs the update. {appName} is the desktop app name (typically Fluxer). #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "Update {appName} now" msgstr "{appName} jetzt aktualisieren" +#. Native desktop dialog body under the headline announcing a new version, shown on a Linux install managed by a package manager such as a deb, rpm or AUR package. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} with your package manager, or download the new version." +msgstr "Aktualisiere {appName} mit deinem Paketmanager oder lade die neue Version herunter." + #. Linux settings text when the installed desktop app is too old for system-wide shortcuts. {productName} is the app name. #: src/features/user/components/modals/tabs/components/SystemShortcutsSection.tsx msgid "Update {productName} to use system-wide shortcuts on Wayland." diff --git a/fluxer_app/src/features/i18n/locales/el/messages.po b/fluxer_app/src/features/i18n/locales/el/messages.po index 19203c5a1..87cb93e62 100644 --- a/fluxer_app/src/features/i18n/locales/el/messages.po +++ b/fluxer_app/src/features/i18n/locales/el/messages.po @@ -1802,6 +1802,16 @@ msgstr "Το {PRODUCT_NAME} πρέπει να επανεκκινηθεί για msgid "{PRODUCT_NAME} needs to restart to disable Chromium's hardware acceleration. Use this only if you're troubleshooting graphics glitches or high GPU usage." msgstr "Το {PRODUCT_NAME} πρέπει να επανεκκινηθεί για να απενεργοποιηθεί η επιτάχυνση υλικού του Chromium. Χρησιμοποιήστε το μόνο αν αντιμετωπίζετε προβλήματα γραφικών ή υψηλή χρήση GPU." +#. Desktop updater modal body for a Flatpak install when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it from your software center." +msgstr "Το {productName} {version} είναι διαθέσιμο. Ενημερώστε το από το κέντρο λογισμικού σας." + +#. Desktop updater modal body for a Linux install managed by a package manager, such as a deb, rpm or AUR package, when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it with your package manager, or download the new version." +msgstr "Το {productName} {version} είναι διαθέσιμο. Ενημερώστε το με τον διαχειριστή πακέτων σας ή κατεβάστε τη νέα έκδοση." + #. Accessible label for the Fluxer application icon. #: src/features/ui/components/icons/FluxerIconMark.tsx msgid "{productName} application icon" @@ -2669,11 +2679,6 @@ msgstr "Ένα εξυπηρετικό bot που κάνει καταπληκτι msgid "a hostname, e.g. {exampleDomain}" msgstr "ένα όνομα κεντρικού υπολογιστή, π.χ. {exampleDomain}" -#. Linux desktop updater modal body shown before version details and package format selector. productName is the app name. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "A Linux desktop update is available. Choose the package format for this system and {productName} will save it locally." -msgstr "Υπάρχει διαθέσιμη ενημέρωση της εφαρμογής για Linux. Επιλέξτε τη μορφή πακέτου για αυτό το σύστημα και το {productName} θα την αποθηκεύσει τοπικά." - #. Plutonium subscription card line offering a move to a lower price. {listPriceEffectiveDate} is a date already formatted and localized by code; never write a date into the translation. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx msgid "A lower price is available. Switching takes effect on <0>{listPriceEffectiveDate} and changes nothing else." @@ -11427,11 +11432,6 @@ msgstr "Σε σίγαση ήχου" msgid "Deafened by moderators" msgstr "Σε σίγαση ήχου από συντονιστές" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "DEB" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "DEB package" -msgstr "Πακέτο DEB" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label in the settings dialog mobile settings view. #: src/features/app/components/dialogs/components/MobileSettingsView.tsx @@ -12095,21 +12095,11 @@ msgstr "Οι ειδοποιήσεις στην επιφάνεια εργασία msgid "Desktop startup" msgstr "Εκκίνηση στην επιφάνεια εργασίας" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update {version} available. Choose a Linux package." -msgstr "Διαθέσιμη ενημέρωση {version} για υπολογιστή. Επιλέξτε πακέτο Linux." - #. Modal title shown when a new desktop app update can be downloaded. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop update available" msgstr "Διαθέσιμη ενημέρωση για υπολογιστή" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format and no version is known. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update available. Choose a Linux package." -msgstr "Διαθέσιμη ενημέρωση για υπολογιστή. Επιλέξτε πακέτο Linux." - #. Desktop updater modal body. The version placeholder is the target app version; productName is the app name. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop version {version} is available. Download the installer, then run it to update {productName}." @@ -13049,11 +13039,6 @@ msgstr "Λήψη προγράμματος εγκατάστασης" msgid "Download media" msgstr "Λήψη πολυμέσων" -#. Button label that downloads a selected Linux desktop update package. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Download package" -msgstr "Λήψη πακέτου" - #. Button that downloads the account recovery kit as a PDF file. #: src/features/auth/components/modals/RecoveryKitModal.tsx msgid "Download PDF" @@ -18280,11 +18265,6 @@ msgstr "Εγκαταστήστε το από το μενού του προγρά msgid "Install the new app" msgstr "Εγκατάσταση της νέας εφαρμογής" -#. Label for the installed desktop version in the Linux update modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Installed" -msgstr "Εγκατεστημένο" - #. Desktop updater modal body. The currentVersion placeholder is the installed app version. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Installed version: {currentVersion}." @@ -19794,11 +19774,6 @@ msgstr "Εφαρμογή Linux" msgid "Linux default" msgstr "Προεπιλεγμένη συσκευή Linux" -#. Select label for choosing the Linux desktop package format to download. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Linux package" -msgstr "Πακέτο Linux" - #. Forum layout name shown inside an activity log row. #. Forum layout option that shows posts as rows. #: src/features/forum/utils/ForumMessageDescriptors.ts @@ -22430,11 +22405,6 @@ msgstr "Τα νέα νήματα θα σταματήσουν να εμφανίζ msgid "New users can't join right now. You can still create an account or sign in, then try this link again later." msgstr "Οι νέοι χρήστες δεν μπορούν να εγγραφούν αυτήν τη στιγμή. Μπορείτε να δημιουργήσετε έναν λογαριασμό ή να συνδεθείτε και να δοκιμάσετε ξανά αυτόν τον σύνδεσμο αργότερα." -#. Fallback version label when the available desktop version is unknown. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "New version" -msgstr "Νέα έκδοση" - #. US state name, shown next to the country name in the mature content gate. #: src/features/user/utils/UserGeo.ts msgctxt "us-state" @@ -24142,6 +24112,8 @@ msgid "Open settings" msgstr "Άνοιγμα ρυθμίσεων" #. Button label that opens the software center via appstream protocol. +#. Native desktop dialog button on a Flatpak install that opens the software center page for the desktop app. +#: src/features/platform/utils/DesktopLocaleBridge.ts #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Open software center" msgstr "Άνοιγμα κέντρου λογισμικού" @@ -28464,11 +28436,6 @@ msgstr "Στρογγυλό πλαίσιο επιλογής" msgid "Rows per page" msgstr "Σειρές ανά σελίδα" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "RPM" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "RPM package" -msgstr "Πακέτο RPM" - #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts msgid "Run {productName} automatically when your computer starts" @@ -33120,11 +33087,6 @@ msgstr "Πατήστε Κοινοποίηση και, στη συνέχεια, msgid "Tap to add a note" msgstr "Πατήστε για προσθήκη σημείωσης" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "tar.gz" verbatim and translate only the word "archive". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "tar.gz archive" -msgstr "Αρχείο tar.gz" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/SharedDescriptors.ts msgid "Taskbar badge" @@ -33513,11 +33475,6 @@ msgstr "Η σύνδεση αναβαθμίζεται σε TLS μόνο εάν ο msgid "The desktop update could not be downloaded. Try again from the update button when your connection is stable." msgstr "Δεν ήταν δυνατή η λήψη της ενημέρωσης για την εφαρμογή υπολογιστή. Δοκιμάστε ξανά από το κουμπί ενημέρωσης όταν η σύνδεσή σας είναι σταθερή." -#. Help text below the Linux package format selector in the desktop updater modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "The downloaded file is saved to your computer. Install it with your normal package manager." -msgstr "Το αρχείο που κατεβάσατε έχει αποθηκευτεί στον υπολογιστή σας. Εγκαταστήστε το με τον συνηθισμένο διαχειριστή πακέτων σας." - #. Activity log detail line under a deleted emoji, shown only when someone other than the person who deleted it had uploaded it. {user} is the user who uploaded the emoji, shown as a clickable user chip. #: src/features/guild/utils/guild_tabs/audit_log/AuditLogExpressionMessageMessages.ts msgid "The emoji was uploaded by {user}" @@ -36548,11 +36505,21 @@ msgstr "Έως 4K στα 60 FPS" msgid "Up to 720p at 30 FPS" msgstr "Έως 720p στα 30 FPS" +#. Native desktop dialog body under the headline announcing a new version, shown on a Flatpak install. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} from your software center." +msgstr "Ενημέρωση του {appName} από το κέντρο λογισμικού σας." + #. Tray menu item shown only while a desktop update is waiting. Clicking it installs the update. {appName} is the desktop app name (typically Fluxer). #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "Update {appName} now" msgstr "Ενημέρωση {appName} τώρα" +#. Native desktop dialog body under the headline announcing a new version, shown on a Linux install managed by a package manager such as a deb, rpm or AUR package. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} with your package manager, or download the new version." +msgstr "Ενημερώστε το {appName} με τον διαχειριστή πακέτων σας ή κατεβάστε τη νέα έκδοση." + #. Linux settings text when the installed desktop app is too old for system-wide shortcuts. {productName} is the app name. #: src/features/user/components/modals/tabs/components/SystemShortcutsSection.tsx msgid "Update {productName} to use system-wide shortcuts on Wayland." diff --git a/fluxer_app/src/features/i18n/locales/en-GB/messages.po b/fluxer_app/src/features/i18n/locales/en-GB/messages.po index d9d12e51b..e8b9b7808 100644 --- a/fluxer_app/src/features/i18n/locales/en-GB/messages.po +++ b/fluxer_app/src/features/i18n/locales/en-GB/messages.po @@ -1802,6 +1802,16 @@ msgstr "{PRODUCT_NAME} needs to restart for the title bar change to take effect. msgid "{PRODUCT_NAME} needs to restart to disable Chromium's hardware acceleration. Use this only if you're troubleshooting graphics glitches or high GPU usage." msgstr "{PRODUCT_NAME} needs to restart to disable Chromium's hardware acceleration. Use this only if you're troubleshooting graphics glitches or high GPU usage." +#. Desktop updater modal body for a Flatpak install when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it from your software center." +msgstr "{productName} {version} is available. Update it from your software centre." + +#. Desktop updater modal body for a Linux install managed by a package manager, such as a deb, rpm or AUR package, when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it with your package manager, or download the new version." +msgstr "{productName} {version} is available. Update it with your package manager, or download the new version." + #. Accessible label for the Fluxer application icon. #: src/features/ui/components/icons/FluxerIconMark.tsx msgid "{productName} application icon" @@ -2669,11 +2679,6 @@ msgstr "A helpful bot that does amazing things!" msgid "a hostname, e.g. {exampleDomain}" msgstr "a hostname, e.g. {exampleDomain}" -#. Linux desktop updater modal body shown before version details and package format selector. productName is the app name. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "A Linux desktop update is available. Choose the package format for this system and {productName} will save it locally." -msgstr "A Linux desktop update is available. Choose the package format for this system and {productName} will save it locally." - #. Plutonium subscription card line offering a move to a lower price. {listPriceEffectiveDate} is a date already formatted and localized by code; never write a date into the translation. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx msgid "A lower price is available. Switching takes effect on <0>{listPriceEffectiveDate} and changes nothing else." @@ -11427,11 +11432,6 @@ msgstr "Deafened" msgid "Deafened by moderators" msgstr "Deafened by moderators" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "DEB" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "DEB package" -msgstr "DEB package" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label in the settings dialog mobile settings view. #: src/features/app/components/dialogs/components/MobileSettingsView.tsx @@ -12095,21 +12095,11 @@ msgstr "Desktop notifications have been blocked. You can enable them later in yo msgid "Desktop startup" msgstr "Desktop startup" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update {version} available. Choose a Linux package." -msgstr "Desktop update {version} available. Choose a Linux package." - #. Modal title shown when a new desktop app update can be downloaded. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop update available" msgstr "Desktop update available" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format and no version is known. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update available. Choose a Linux package." -msgstr "Desktop update available. Choose a Linux package." - #. Desktop updater modal body. The version placeholder is the target app version; productName is the app name. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop version {version} is available. Download the installer, then run it to update {productName}." @@ -13049,11 +13039,6 @@ msgstr "Download installer" msgid "Download media" msgstr "Download media" -#. Button label that downloads a selected Linux desktop update package. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Download package" -msgstr "Download package" - #. Button that downloads the account recovery kit as a PDF file. #: src/features/auth/components/modals/RecoveryKitModal.tsx msgid "Download PDF" @@ -18280,11 +18265,6 @@ msgstr "Install it from your browser menu." msgid "Install the new app" msgstr "Install the new app" -#. Label for the installed desktop version in the Linux update modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Installed" -msgstr "Installed" - #. Desktop updater modal body. The currentVersion placeholder is the installed app version. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Installed version: {currentVersion}." @@ -19794,11 +19774,6 @@ msgstr "Linux app" msgid "Linux default" msgstr "Linux default" -#. Select label for choosing the Linux desktop package format to download. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Linux package" -msgstr "Linux package" - #. Forum layout name shown inside an activity log row. #. Forum layout option that shows posts as rows. #: src/features/forum/utils/ForumMessageDescriptors.ts @@ -22430,11 +22405,6 @@ msgstr "New threads will stop showing in the channel list after this period of i msgid "New users can't join right now. You can still create an account or sign in, then try this link again later." msgstr "New users can't join right now. You can still create an account or sign in, then try this link again later." -#. Fallback version label when the available desktop version is unknown. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "New version" -msgstr "New version" - #. US state name, shown next to the country name in the mature content gate. #: src/features/user/utils/UserGeo.ts msgctxt "us-state" @@ -24142,6 +24112,8 @@ msgid "Open settings" msgstr "Open settings" #. Button label that opens the software center via appstream protocol. +#. Native desktop dialog button on a Flatpak install that opens the software center page for the desktop app. +#: src/features/platform/utils/DesktopLocaleBridge.ts #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Open software center" msgstr "Open software centre" @@ -28464,11 +28436,6 @@ msgstr "Round style checkbox" msgid "Rows per page" msgstr "Rows per page" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "RPM" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "RPM package" -msgstr "RPM package" - #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts msgid "Run {productName} automatically when your computer starts" @@ -33120,11 +33087,6 @@ msgstr "Tap Share, then Add to Home Screen." msgid "Tap to add a note" msgstr "Tap to add a note" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "tar.gz" verbatim and translate only the word "archive". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "tar.gz archive" -msgstr "tar.gz archive" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/SharedDescriptors.ts msgid "Taskbar badge" @@ -33513,11 +33475,6 @@ msgstr "The connection upgrades to TLS only if the server offers it." msgid "The desktop update could not be downloaded. Try again from the update button when your connection is stable." msgstr "The desktop update could not be downloaded. Try again from the update button when your connection is stable." -#. Help text below the Linux package format selector in the desktop updater modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "The downloaded file is saved to your computer. Install it with your normal package manager." -msgstr "The downloaded file is saved to your computer. Install it with your normal package manager." - #. Activity log detail line under a deleted emoji, shown only when someone other than the person who deleted it had uploaded it. {user} is the user who uploaded the emoji, shown as a clickable user chip. #: src/features/guild/utils/guild_tabs/audit_log/AuditLogExpressionMessageMessages.ts msgid "The emoji was uploaded by {user}" @@ -36548,11 +36505,21 @@ msgstr "Up to 4K at 60 FPS" msgid "Up to 720p at 30 FPS" msgstr "Up to 720p at 30 FPS" +#. Native desktop dialog body under the headline announcing a new version, shown on a Flatpak install. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} from your software center." +msgstr "Update {appName} from your software centre." + #. Tray menu item shown only while a desktop update is waiting. Clicking it installs the update. {appName} is the desktop app name (typically Fluxer). #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "Update {appName} now" msgstr "Update {appName} now" +#. Native desktop dialog body under the headline announcing a new version, shown on a Linux install managed by a package manager such as a deb, rpm or AUR package. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} with your package manager, or download the new version." +msgstr "Update {appName} with your package manager, or download the new version." + #. Linux settings text when the installed desktop app is too old for system-wide shortcuts. {productName} is the app name. #: src/features/user/components/modals/tabs/components/SystemShortcutsSection.tsx msgid "Update {productName} to use system-wide shortcuts on Wayland." diff --git a/fluxer_app/src/features/i18n/locales/en-US/messages.po b/fluxer_app/src/features/i18n/locales/en-US/messages.po index 2da5bb6f8..6b682b6ee 100644 --- a/fluxer_app/src/features/i18n/locales/en-US/messages.po +++ b/fluxer_app/src/features/i18n/locales/en-US/messages.po @@ -1802,6 +1802,16 @@ msgstr "{PRODUCT_NAME} needs to restart for the title bar change to take effect. msgid "{PRODUCT_NAME} needs to restart to disable Chromium's hardware acceleration. Use this only if you're troubleshooting graphics glitches or high GPU usage." msgstr "{PRODUCT_NAME} needs to restart to disable Chromium's hardware acceleration. Use this only if you're troubleshooting graphics glitches or high GPU usage." +#. Desktop updater modal body for a Flatpak install when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it from your software center." +msgstr "{productName} {version} is available. Update it from your software center." + +#. Desktop updater modal body for a Linux install managed by a package manager, such as a deb, rpm or AUR package, when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it with your package manager, or download the new version." +msgstr "{productName} {version} is available. Update it with your package manager, or download the new version." + #. Accessible label for the Fluxer application icon. #: src/features/ui/components/icons/FluxerIconMark.tsx msgid "{productName} application icon" @@ -2669,11 +2679,6 @@ msgstr "A helpful bot that does amazing things!" msgid "a hostname, e.g. {exampleDomain}" msgstr "a hostname, e.g. {exampleDomain}" -#. Linux desktop updater modal body shown before version details and package format selector. productName is the app name. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "A Linux desktop update is available. Choose the package format for this system and {productName} will save it locally." -msgstr "A Linux desktop update is available. Choose the package format for this system and {productName} will save it locally." - #. Plutonium subscription card line offering a move to a lower price. {listPriceEffectiveDate} is a date already formatted and localized by code; never write a date into the translation. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx msgid "A lower price is available. Switching takes effect on <0>{listPriceEffectiveDate} and changes nothing else." @@ -11427,11 +11432,6 @@ msgstr "Deafened" msgid "Deafened by moderators" msgstr "Deafened by moderators" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "DEB" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "DEB package" -msgstr "DEB package" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label in the settings dialog mobile settings view. #: src/features/app/components/dialogs/components/MobileSettingsView.tsx @@ -12095,21 +12095,11 @@ msgstr "Desktop notifications have been blocked. You can enable them later in yo msgid "Desktop startup" msgstr "Desktop startup" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update {version} available. Choose a Linux package." -msgstr "Desktop update {version} available. Choose a Linux package." - #. Modal title shown when a new desktop app update can be downloaded. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop update available" msgstr "Desktop update available" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format and no version is known. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update available. Choose a Linux package." -msgstr "Desktop update available. Choose a Linux package." - #. Desktop updater modal body. The version placeholder is the target app version; productName is the app name. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop version {version} is available. Download the installer, then run it to update {productName}." @@ -13049,11 +13039,6 @@ msgstr "Download installer" msgid "Download media" msgstr "Download media" -#. Button label that downloads a selected Linux desktop update package. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Download package" -msgstr "Download package" - #. Button that downloads the account recovery kit as a PDF file. #: src/features/auth/components/modals/RecoveryKitModal.tsx msgid "Download PDF" @@ -18280,11 +18265,6 @@ msgstr "Install it from your browser menu." msgid "Install the new app" msgstr "Install the new app" -#. Label for the installed desktop version in the Linux update modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Installed" -msgstr "Installed" - #. Desktop updater modal body. The currentVersion placeholder is the installed app version. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Installed version: {currentVersion}." @@ -19794,11 +19774,6 @@ msgstr "Linux app" msgid "Linux default" msgstr "Linux default" -#. Select label for choosing the Linux desktop package format to download. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Linux package" -msgstr "Linux package" - #. Forum layout name shown inside an activity log row. #. Forum layout option that shows posts as rows. #: src/features/forum/utils/ForumMessageDescriptors.ts @@ -22430,11 +22405,6 @@ msgstr "New threads will stop showing in the channel list after this period of i msgid "New users can't join right now. You can still create an account or sign in, then try this link again later." msgstr "New users can't join right now. You can still create an account or sign in, then try this link again later." -#. Fallback version label when the available desktop version is unknown. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "New version" -msgstr "New version" - #. US state name, shown next to the country name in the mature content gate. #: src/features/user/utils/UserGeo.ts msgctxt "us-state" @@ -24142,6 +24112,8 @@ msgid "Open settings" msgstr "Open settings" #. Button label that opens the software center via appstream protocol. +#. Native desktop dialog button on a Flatpak install that opens the software center page for the desktop app. +#: src/features/platform/utils/DesktopLocaleBridge.ts #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Open software center" msgstr "Open software center" @@ -28464,11 +28436,6 @@ msgstr "Round style checkbox" msgid "Rows per page" msgstr "Rows per page" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "RPM" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "RPM package" -msgstr "RPM package" - #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts msgid "Run {productName} automatically when your computer starts" @@ -33120,11 +33087,6 @@ msgstr "Tap Share, then Add to Home Screen." msgid "Tap to add a note" msgstr "Tap to add a note" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "tar.gz" verbatim and translate only the word "archive". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "tar.gz archive" -msgstr "tar.gz archive" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/SharedDescriptors.ts msgid "Taskbar badge" @@ -33513,11 +33475,6 @@ msgstr "The connection upgrades to TLS only if the server offers it." msgid "The desktop update could not be downloaded. Try again from the update button when your connection is stable." msgstr "The desktop update could not be downloaded. Try again from the update button when your connection is stable." -#. Help text below the Linux package format selector in the desktop updater modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "The downloaded file is saved to your computer. Install it with your normal package manager." -msgstr "The downloaded file is saved to your computer. Install it with your normal package manager." - #. Activity log detail line under a deleted emoji, shown only when someone other than the person who deleted it had uploaded it. {user} is the user who uploaded the emoji, shown as a clickable user chip. #: src/features/guild/utils/guild_tabs/audit_log/AuditLogExpressionMessageMessages.ts msgid "The emoji was uploaded by {user}" @@ -36548,11 +36505,21 @@ msgstr "Up to 4K at 60 FPS" msgid "Up to 720p at 30 FPS" msgstr "Up to 720p at 30 FPS" +#. Native desktop dialog body under the headline announcing a new version, shown on a Flatpak install. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} from your software center." +msgstr "Update {appName} from your software center." + #. Tray menu item shown only while a desktop update is waiting. Clicking it installs the update. {appName} is the desktop app name (typically Fluxer). #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "Update {appName} now" msgstr "Update {appName} now" +#. Native desktop dialog body under the headline announcing a new version, shown on a Linux install managed by a package manager such as a deb, rpm or AUR package. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} with your package manager, or download the new version." +msgstr "Update {appName} with your package manager, or download the new version." + #. Linux settings text when the installed desktop app is too old for system-wide shortcuts. {productName} is the app name. #: src/features/user/components/modals/tabs/components/SystemShortcutsSection.tsx msgid "Update {productName} to use system-wide shortcuts on Wayland." diff --git a/fluxer_app/src/features/i18n/locales/es-419/messages.po b/fluxer_app/src/features/i18n/locales/es-419/messages.po index 84e3be9fd..363e6ac4d 100644 --- a/fluxer_app/src/features/i18n/locales/es-419/messages.po +++ b/fluxer_app/src/features/i18n/locales/es-419/messages.po @@ -1802,6 +1802,16 @@ msgstr "{PRODUCT_NAME} necesita reiniciarse para que el cambio en la barra de t msgid "{PRODUCT_NAME} needs to restart to disable Chromium's hardware acceleration. Use this only if you're troubleshooting graphics glitches or high GPU usage." msgstr "{PRODUCT_NAME} debe reiniciarse para deshabilitar la aceleración de hardware de Chromium. Usa esto solo si estás solucionando problemas gráficos o un alto uso de la GPU." +#. Desktop updater modal body for a Flatpak install when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it from your software center." +msgstr "{productName} {version} está disponible. Actualízalo desde tu centro de software." + +#. Desktop updater modal body for a Linux install managed by a package manager, such as a deb, rpm or AUR package, when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it with your package manager, or download the new version." +msgstr "{productName} {version} está disponible. Actualízalo con tu gestor de paquetes o descarga la nueva versión." + #. Accessible label for the Fluxer application icon. #: src/features/ui/components/icons/FluxerIconMark.tsx msgid "{productName} application icon" @@ -2669,11 +2679,6 @@ msgstr "¡Un bot útil que hace cosas increíbles!" msgid "a hostname, e.g. {exampleDomain}" msgstr "un nombre de host, p. ej. {exampleDomain}" -#. Linux desktop updater modal body shown before version details and package format selector. productName is the app name. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "A Linux desktop update is available. Choose the package format for this system and {productName} will save it locally." -msgstr "Hay una actualización de escritorio para Linux disponible. Elige el formato del paquete para este sistema y {productName} lo guardará localmente." - #. Plutonium subscription card line offering a move to a lower price. {listPriceEffectiveDate} is a date already formatted and localized by code; never write a date into the translation. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx msgid "A lower price is available. Switching takes effect on <0>{listPriceEffectiveDate} and changes nothing else." @@ -11427,11 +11432,6 @@ msgstr "Sonido desactivado" msgid "Deafened by moderators" msgstr "Sonido desactivado por los moderadores" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "DEB" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "DEB package" -msgstr "Paquete DEB" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label in the settings dialog mobile settings view. #: src/features/app/components/dialogs/components/MobileSettingsView.tsx @@ -12095,21 +12095,11 @@ msgstr "Las notificaciones de escritorio se bloquearon. Puedes activarlas más t msgid "Desktop startup" msgstr "Inicio de la aplicación de escritorio" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update {version} available. Choose a Linux package." -msgstr "Actualización de escritorio {version} disponible. Elige un paquete de Linux." - #. Modal title shown when a new desktop app update can be downloaded. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop update available" msgstr "Actualización de escritorio disponible" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format and no version is known. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update available. Choose a Linux package." -msgstr "Actualización de escritorio disponible. Elige un paquete de Linux." - #. Desktop updater modal body. The version placeholder is the target app version; productName is the app name. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop version {version} is available. Download the installer, then run it to update {productName}." @@ -13049,11 +13039,6 @@ msgstr "Descargar instalador" msgid "Download media" msgstr "Descargar contenido multimedia" -#. Button label that downloads a selected Linux desktop update package. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Download package" -msgstr "Descargar paquete" - #. Button that downloads the account recovery kit as a PDF file. #: src/features/auth/components/modals/RecoveryKitModal.tsx msgid "Download PDF" @@ -18280,11 +18265,6 @@ msgstr "Instálalo desde el menú de tu navegador." msgid "Install the new app" msgstr "Instalar la nueva app" -#. Label for the installed desktop version in the Linux update modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Installed" -msgstr "Instalado" - #. Desktop updater modal body. The currentVersion placeholder is the installed app version. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Installed version: {currentVersion}." @@ -19794,11 +19774,6 @@ msgstr "Aplicación para Linux" msgid "Linux default" msgstr "Predeterminado de Linux" -#. Select label for choosing the Linux desktop package format to download. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Linux package" -msgstr "Paquete para Linux" - #. Forum layout name shown inside an activity log row. #. Forum layout option that shows posts as rows. #: src/features/forum/utils/ForumMessageDescriptors.ts @@ -22430,11 +22405,6 @@ msgstr "Los hilos nuevos dejarán de mostrarse en la lista del canal después de msgid "New users can't join right now. You can still create an account or sign in, then try this link again later." msgstr "Los usuarios nuevos no pueden unirse en este momento. Puedes crear una cuenta o iniciar sesión y volver a intentar con este enlace más tarde." -#. Fallback version label when the available desktop version is unknown. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "New version" -msgstr "Nueva versión" - #. US state name, shown next to the country name in the mature content gate. #: src/features/user/utils/UserGeo.ts msgctxt "us-state" @@ -24142,6 +24112,8 @@ msgid "Open settings" msgstr "Abrir configuración" #. Button label that opens the software center via appstream protocol. +#. Native desktop dialog button on a Flatpak install that opens the software center page for the desktop app. +#: src/features/platform/utils/DesktopLocaleBridge.ts #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Open software center" msgstr "Abrir centro de software" @@ -28464,11 +28436,6 @@ msgstr "Casilla de verificación de estilo redondo" msgid "Rows per page" msgstr "Filas por página" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "RPM" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "RPM package" -msgstr "Paquete RPM" - #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts msgid "Run {productName} automatically when your computer starts" @@ -33120,11 +33087,6 @@ msgstr "Toca Compartir y luego Agregar a pantalla de inicio." msgid "Tap to add a note" msgstr "Toca para agregar una nota" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "tar.gz" verbatim and translate only the word "archive". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "tar.gz archive" -msgstr "Archivo tar.gz" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/SharedDescriptors.ts msgid "Taskbar badge" @@ -33513,11 +33475,6 @@ msgstr "La conexión se actualiza a TLS solo si el servidor lo ofrece." msgid "The desktop update could not be downloaded. Try again from the update button when your connection is stable." msgstr "No se pudo descargar la actualización de escritorio. Intenta de nuevo desde el botón de actualización cuando tu conexión sea estable." -#. Help text below the Linux package format selector in the desktop updater modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "The downloaded file is saved to your computer. Install it with your normal package manager." -msgstr "El archivo descargado se guarda en tu computadora. Instálalo con tu gestor de paquetes habitual." - #. Activity log detail line under a deleted emoji, shown only when someone other than the person who deleted it had uploaded it. {user} is the user who uploaded the emoji, shown as a clickable user chip. #: src/features/guild/utils/guild_tabs/audit_log/AuditLogExpressionMessageMessages.ts msgid "The emoji was uploaded by {user}" @@ -36548,11 +36505,21 @@ msgstr "Hasta 4K a 60 FPS" msgid "Up to 720p at 30 FPS" msgstr "Hasta 720p a 30 FPS" +#. Native desktop dialog body under the headline announcing a new version, shown on a Flatpak install. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} from your software center." +msgstr "Actualiza {appName} desde tu centro de software." + #. Tray menu item shown only while a desktop update is waiting. Clicking it installs the update. {appName} is the desktop app name (typically Fluxer). #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "Update {appName} now" msgstr "Actualizar {appName} ahora" +#. Native desktop dialog body under the headline announcing a new version, shown on a Linux install managed by a package manager such as a deb, rpm or AUR package. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} with your package manager, or download the new version." +msgstr "Actualiza {appName} con tu gestor de paquetes o descarga la nueva versión." + #. Linux settings text when the installed desktop app is too old for system-wide shortcuts. {productName} is the app name. #: src/features/user/components/modals/tabs/components/SystemShortcutsSection.tsx msgid "Update {productName} to use system-wide shortcuts on Wayland." diff --git a/fluxer_app/src/features/i18n/locales/es-ES/messages.po b/fluxer_app/src/features/i18n/locales/es-ES/messages.po index 9b4baa735..00ea80a97 100644 --- a/fluxer_app/src/features/i18n/locales/es-ES/messages.po +++ b/fluxer_app/src/features/i18n/locales/es-ES/messages.po @@ -1802,6 +1802,16 @@ msgstr "{PRODUCT_NAME} necesita reiniciarse para que el cambio en la barra de t msgid "{PRODUCT_NAME} needs to restart to disable Chromium's hardware acceleration. Use this only if you're troubleshooting graphics glitches or high GPU usage." msgstr "{PRODUCT_NAME} necesita reiniciarse para deshabilitar la aceleración de hardware de Chromium. Usa esto solo si estás solucionando problemas gráficos o un alto uso de la GPU." +#. Desktop updater modal body for a Flatpak install when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it from your software center." +msgstr "{productName} {version} está disponible. Actualízalo desde tu centro de software." + +#. Desktop updater modal body for a Linux install managed by a package manager, such as a deb, rpm or AUR package, when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it with your package manager, or download the new version." +msgstr "{productName} {version} está disponible. Actualízalo con tu gestor de paquetes o descarga la nueva versión." + #. Accessible label for the Fluxer application icon. #: src/features/ui/components/icons/FluxerIconMark.tsx msgid "{productName} application icon" @@ -2669,11 +2679,6 @@ msgstr "¡Un bot útil que hace cosas increíbles!" msgid "a hostname, e.g. {exampleDomain}" msgstr "un nombre de host, p. ej., {exampleDomain}" -#. Linux desktop updater modal body shown before version details and package format selector. productName is the app name. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "A Linux desktop update is available. Choose the package format for this system and {productName} will save it locally." -msgstr "Hay una actualización de escritorio para Linux disponible. Elige el formato del paquete para este sistema y {productName} lo guardará localmente." - #. Plutonium subscription card line offering a move to a lower price. {listPriceEffectiveDate} is a date already formatted and localized by code; never write a date into the translation. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx msgid "A lower price is available. Switching takes effect on <0>{listPriceEffectiveDate} and changes nothing else." @@ -11427,11 +11432,6 @@ msgstr "Sonido desactivado" msgid "Deafened by moderators" msgstr "Sonido desactivado por los moderadores" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "DEB" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "DEB package" -msgstr "Paquete DEB" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label in the settings dialog mobile settings view. #: src/features/app/components/dialogs/components/MobileSettingsView.tsx @@ -12095,21 +12095,11 @@ msgstr "Las notificaciones de escritorio se han bloqueado. Puedes activarlas má msgid "Desktop startup" msgstr "Inicio de la aplicación de escritorio" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update {version} available. Choose a Linux package." -msgstr "Actualización de escritorio {version} disponible. Elige un paquete de Linux." - #. Modal title shown when a new desktop app update can be downloaded. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop update available" msgstr "Actualización de escritorio disponible" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format and no version is known. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update available. Choose a Linux package." -msgstr "Actualización de escritorio disponible. Elige un paquete de Linux." - #. Desktop updater modal body. The version placeholder is the target app version; productName is the app name. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop version {version} is available. Download the installer, then run it to update {productName}." @@ -13049,11 +13039,6 @@ msgstr "Descargar instalador" msgid "Download media" msgstr "Descargar contenido multimedia" -#. Button label that downloads a selected Linux desktop update package. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Download package" -msgstr "Descargar paquete" - #. Button that downloads the account recovery kit as a PDF file. #: src/features/auth/components/modals/RecoveryKitModal.tsx msgid "Download PDF" @@ -18280,11 +18265,6 @@ msgstr "Instálalo desde el menú de tu navegador." msgid "Install the new app" msgstr "Instalar la nueva app" -#. Label for the installed desktop version in the Linux update modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Installed" -msgstr "Instalada" - #. Desktop updater modal body. The currentVersion placeholder is the installed app version. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Installed version: {currentVersion}." @@ -19794,11 +19774,6 @@ msgstr "Aplicación para Linux" msgid "Linux default" msgstr "Predeterminado de Linux" -#. Select label for choosing the Linux desktop package format to download. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Linux package" -msgstr "Paquete de Linux" - #. Forum layout name shown inside an activity log row. #. Forum layout option that shows posts as rows. #: src/features/forum/utils/ForumMessageDescriptors.ts @@ -22430,11 +22405,6 @@ msgstr "Los hilos nuevos dejarán de aparecer en la lista del canal tras este pe msgid "New users can't join right now. You can still create an account or sign in, then try this link again later." msgstr "Los usuarios nuevos no pueden unirse ahora mismo. Puedes crear una cuenta o iniciar sesión y volver a probar este enlace más tarde." -#. Fallback version label when the available desktop version is unknown. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "New version" -msgstr "Nueva versión" - #. US state name, shown next to the country name in the mature content gate. #: src/features/user/utils/UserGeo.ts msgctxt "us-state" @@ -24142,6 +24112,8 @@ msgid "Open settings" msgstr "Abrir ajustes" #. Button label that opens the software center via appstream protocol. +#. Native desktop dialog button on a Flatpak install that opens the software center page for the desktop app. +#: src/features/platform/utils/DesktopLocaleBridge.ts #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Open software center" msgstr "Abrir centro de software" @@ -28464,11 +28436,6 @@ msgstr "Casilla de verificación de estilo redondo" msgid "Rows per page" msgstr "Filas por página" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "RPM" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "RPM package" -msgstr "Paquete RPM" - #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts msgid "Run {productName} automatically when your computer starts" @@ -33120,11 +33087,6 @@ msgstr "Toca Compartir y, a continuación, Añadir a pantalla de inicio." msgid "Tap to add a note" msgstr "Toca para añadir una nota" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "tar.gz" verbatim and translate only the word "archive". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "tar.gz archive" -msgstr "Archivo tar.gz" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/SharedDescriptors.ts msgid "Taskbar badge" @@ -33513,11 +33475,6 @@ msgstr "La conexión se actualiza a TLS solo si el servidor lo ofrece." msgid "The desktop update could not be downloaded. Try again from the update button when your connection is stable." msgstr "No se pudo descargar la actualización de escritorio. Inténtalo de nuevo desde el botón de actualización cuando tu conexión sea estable." -#. Help text below the Linux package format selector in the desktop updater modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "The downloaded file is saved to your computer. Install it with your normal package manager." -msgstr "El archivo descargado se guarda en tu ordenador. Instálalo con tu gestor de paquetes habitual." - #. Activity log detail line under a deleted emoji, shown only when someone other than the person who deleted it had uploaded it. {user} is the user who uploaded the emoji, shown as a clickable user chip. #: src/features/guild/utils/guild_tabs/audit_log/AuditLogExpressionMessageMessages.ts msgid "The emoji was uploaded by {user}" @@ -36548,11 +36505,21 @@ msgstr "Hasta 4K a 60 FPS" msgid "Up to 720p at 30 FPS" msgstr "Hasta 720p a 30 FPS" +#. Native desktop dialog body under the headline announcing a new version, shown on a Flatpak install. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} from your software center." +msgstr "Actualiza {appName} desde tu centro de software." + #. Tray menu item shown only while a desktop update is waiting. Clicking it installs the update. {appName} is the desktop app name (typically Fluxer). #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "Update {appName} now" msgstr "Actualizar {appName} ahora" +#. Native desktop dialog body under the headline announcing a new version, shown on a Linux install managed by a package manager such as a deb, rpm or AUR package. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} with your package manager, or download the new version." +msgstr "Actualiza {appName} con tu gestor de paquetes o descarga la nueva versión." + #. Linux settings text when the installed desktop app is too old for system-wide shortcuts. {productName} is the app name. #: src/features/user/components/modals/tabs/components/SystemShortcutsSection.tsx msgid "Update {productName} to use system-wide shortcuts on Wayland." diff --git a/fluxer_app/src/features/i18n/locales/fi/messages.po b/fluxer_app/src/features/i18n/locales/fi/messages.po index 7fe9f3d18..7ed75d2ae 100644 --- a/fluxer_app/src/features/i18n/locales/fi/messages.po +++ b/fluxer_app/src/features/i18n/locales/fi/messages.po @@ -1802,6 +1802,16 @@ msgstr "{PRODUCT_NAME} täytyy käynnistää uudelleen, jotta otsikkopalkin muut msgid "{PRODUCT_NAME} needs to restart to disable Chromium's hardware acceleration. Use this only if you're troubleshooting graphics glitches or high GPU usage." msgstr "{PRODUCT_NAME} on käynnistettävä uudelleen, jotta Chromiumin laitteistokiihdytys voidaan poistaa käytöstä. Käytä tätä vain, jos selvität grafiikkaongelmia tai korkeaa GPU:n käyttöastetta." +#. Desktop updater modal body for a Flatpak install when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it from your software center." +msgstr "{productName} {version} on saatavilla. Päivitä se ohjelmistokeskuksestasi." + +#. Desktop updater modal body for a Linux install managed by a package manager, such as a deb, rpm or AUR package, when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it with your package manager, or download the new version." +msgstr "{productName} {version} on saatavilla. Päivitä se paketinhallinnallasi tai lataa uusi versio." + #. Accessible label for the Fluxer application icon. #: src/features/ui/components/icons/FluxerIconMark.tsx msgid "{productName} application icon" @@ -2669,11 +2679,6 @@ msgstr "Auttavainen botti, joka tekee uskomattomia asioita!" msgid "a hostname, e.g. {exampleDomain}" msgstr "isäntänimi, esim. {exampleDomain}" -#. Linux desktop updater modal body shown before version details and package format selector. productName is the app name. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "A Linux desktop update is available. Choose the package format for this system and {productName} will save it locally." -msgstr "Linux-työpöytäpäivitys on saatavilla. Valitse pakettimuoto tälle järjestelmälle, niin {productName} tallentaa sen paikallisesti." - #. Plutonium subscription card line offering a move to a lower price. {listPriceEffectiveDate} is a date already formatted and localized by code; never write a date into the translation. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx msgid "A lower price is available. Switching takes effect on <0>{listPriceEffectiveDate} and changes nothing else." @@ -11427,11 +11432,6 @@ msgstr "Äänet mykistetty" msgid "Deafened by moderators" msgstr "Moderaattorit mykistivät äänesi" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "DEB" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "DEB package" -msgstr "DEB-paketti" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label in the settings dialog mobile settings view. #: src/features/app/components/dialogs/components/MobileSettingsView.tsx @@ -12095,21 +12095,11 @@ msgstr "Työpöytäilmoitukset on estetty. Voit ottaa ne käyttöön myöhemmin msgid "Desktop startup" msgstr "Käynnistys työpöydällä" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update {version} available. Choose a Linux package." -msgstr "Työpöytäpäivitys {version} saatavilla. Valitse Linux-paketti." - #. Modal title shown when a new desktop app update can be downloaded. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop update available" msgstr "Työpöytäsovelluksen päivitys saatavilla" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format and no version is known. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update available. Choose a Linux package." -msgstr "Työpöytäpäivitys saatavilla. Valitse Linux-paketti." - #. Desktop updater modal body. The version placeholder is the target app version; productName is the app name. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop version {version} is available. Download the installer, then run it to update {productName}." @@ -13049,11 +13039,6 @@ msgstr "Lataa asennusohjelma" msgid "Download media" msgstr "Lataa media" -#. Button label that downloads a selected Linux desktop update package. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Download package" -msgstr "Lataa paketti" - #. Button that downloads the account recovery kit as a PDF file. #: src/features/auth/components/modals/RecoveryKitModal.tsx msgid "Download PDF" @@ -18280,11 +18265,6 @@ msgstr "Asenna se selaimesi valikosta." msgid "Install the new app" msgstr "Asenna uusi sovellus" -#. Label for the installed desktop version in the Linux update modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Installed" -msgstr "Asennettu" - #. Desktop updater modal body. The currentVersion placeholder is the installed app version. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Installed version: {currentVersion}." @@ -19794,11 +19774,6 @@ msgstr "Linux-sovellus" msgid "Linux default" msgstr "Linuxin oletusarvo" -#. Select label for choosing the Linux desktop package format to download. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Linux package" -msgstr "Linux-paketti" - #. Forum layout name shown inside an activity log row. #. Forum layout option that shows posts as rows. #: src/features/forum/utils/ForumMessageDescriptors.ts @@ -22430,11 +22405,6 @@ msgstr "Uudet keskustelut lakkaavat näkymästä kanavaluettelossa tämän toime msgid "New users can't join right now. You can still create an account or sign in, then try this link again later." msgstr "Uudet käyttäjät eivät voi liittyä juuri nyt. Voit silti luoda tilin tai kirjautua sisään ja yrittää tätä linkkiä uudelleen myöhemmin." -#. Fallback version label when the available desktop version is unknown. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "New version" -msgstr "Uusi versio" - #. US state name, shown next to the country name in the mature content gate. #: src/features/user/utils/UserGeo.ts msgctxt "us-state" @@ -24142,6 +24112,8 @@ msgid "Open settings" msgstr "Avaa asetukset" #. Button label that opens the software center via appstream protocol. +#. Native desktop dialog button on a Flatpak install that opens the software center page for the desktop app. +#: src/features/platform/utils/DesktopLocaleBridge.ts #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Open software center" msgstr "Avaa ohjelmistokeskus" @@ -28464,11 +28436,6 @@ msgstr "Pyöreä valintaruutu" msgid "Rows per page" msgstr "Rivejä sivulla" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "RPM" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "RPM package" -msgstr "RPM-paketti" - #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts msgid "Run {productName} automatically when your computer starts" @@ -33120,11 +33087,6 @@ msgstr "Napauta Jaa ja sitten Lisää Koti-valikkoon." msgid "Tap to add a note" msgstr "Lisää muistiinpano napauttamalla" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "tar.gz" verbatim and translate only the word "archive". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "tar.gz archive" -msgstr "tar.gz-arkisto" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/SharedDescriptors.ts msgid "Taskbar badge" @@ -33513,11 +33475,6 @@ msgstr "Yhteys päivitetään TLS-salausta käyttäväksi vain, jos palvelin tar msgid "The desktop update could not be downloaded. Try again from the update button when your connection is stable." msgstr "Työpöytäpäivitystä ei voitu ladata. Yritä uudelleen päivityspainikkeesta, kun yhteytesi on vakaa." -#. Help text below the Linux package format selector in the desktop updater modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "The downloaded file is saved to your computer. Install it with your normal package manager." -msgstr "Ladattu tiedosto tallennetaan tietokoneellesi. Asenna se tavallisella pakettienhallintaohjelmallasi." - #. Activity log detail line under a deleted emoji, shown only when someone other than the person who deleted it had uploaded it. {user} is the user who uploaded the emoji, shown as a clickable user chip. #: src/features/guild/utils/guild_tabs/audit_log/AuditLogExpressionMessageMessages.ts msgid "The emoji was uploaded by {user}" @@ -36548,11 +36505,21 @@ msgstr "Jopa 4K 60 FPS" msgid "Up to 720p at 30 FPS" msgstr "Jopa 720p 30 FPS" +#. Native desktop dialog body under the headline announcing a new version, shown on a Flatpak install. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} from your software center." +msgstr "Päivitä {appName} ohjelmistokeskuksestasi." + #. Tray menu item shown only while a desktop update is waiting. Clicking it installs the update. {appName} is the desktop app name (typically Fluxer). #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "Update {appName} now" msgstr "Päivitä {appName} nyt" +#. Native desktop dialog body under the headline announcing a new version, shown on a Linux install managed by a package manager such as a deb, rpm or AUR package. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} with your package manager, or download the new version." +msgstr "Päivitä {appName} paketinhallinnallasi tai lataa uusi versio." + #. Linux settings text when the installed desktop app is too old for system-wide shortcuts. {productName} is the app name. #: src/features/user/components/modals/tabs/components/SystemShortcutsSection.tsx msgid "Update {productName} to use system-wide shortcuts on Wayland." diff --git a/fluxer_app/src/features/i18n/locales/fr/messages.po b/fluxer_app/src/features/i18n/locales/fr/messages.po index 248af09c5..12ea839ff 100644 --- a/fluxer_app/src/features/i18n/locales/fr/messages.po +++ b/fluxer_app/src/features/i18n/locales/fr/messages.po @@ -1802,6 +1802,16 @@ msgstr "{PRODUCT_NAME} doit redémarrer pour que la modification de la barre de msgid "{PRODUCT_NAME} needs to restart to disable Chromium's hardware acceleration. Use this only if you're troubleshooting graphics glitches or high GPU usage." msgstr "{PRODUCT_NAME} doit redémarrer pour désactiver l'accélération matérielle de Chromium. N'utilisez cette option que si vous rencontrez des problèmes graphiques ou une utilisation élevée du GPU." +#. Desktop updater modal body for a Flatpak install when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it from your software center." +msgstr "{productName} {version} est disponible. Mettez-le à jour depuis votre centre logiciel." + +#. Desktop updater modal body for a Linux install managed by a package manager, such as a deb, rpm or AUR package, when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it with your package manager, or download the new version." +msgstr "Une nouvelle version de {productName} ({version}) est disponible. Mettez-la à jour avec votre gestionnaire de paquets, ou téléchargez la nouvelle version." + #. Accessible label for the Fluxer application icon. #: src/features/ui/components/icons/FluxerIconMark.tsx msgid "{productName} application icon" @@ -2669,11 +2679,6 @@ msgstr "Un bot utile qui fait des choses incroyables !" msgid "a hostname, e.g. {exampleDomain}" msgstr "un nom d'hôte, par exemple {exampleDomain}" -#. Linux desktop updater modal body shown before version details and package format selector. productName is the app name. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "A Linux desktop update is available. Choose the package format for this system and {productName} will save it locally." -msgstr "Une mise à jour de l’application pour Linux est disponible. Choisissez le format de paquet adapté à ce système et {productName} l’enregistrera sur votre ordinateur." - #. Plutonium subscription card line offering a move to a lower price. {listPriceEffectiveDate} is a date already formatted and localized by code; never write a date into the translation. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx msgid "A lower price is available. Switching takes effect on <0>{listPriceEffectiveDate} and changes nothing else." @@ -11427,11 +11432,6 @@ msgstr "Son désactivé" msgid "Deafened by moderators" msgstr "Son désactivé par les modérateurs" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "DEB" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "DEB package" -msgstr "Paquet DEB" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label in the settings dialog mobile settings view. #: src/features/app/components/dialogs/components/MobileSettingsView.tsx @@ -12095,21 +12095,11 @@ msgstr "Les notifications de bureau ont été bloquées. Vous pouvez les activer msgid "Desktop startup" msgstr "Démarrage sur ordinateur" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update {version} available. Choose a Linux package." -msgstr "Mise à jour {version} de l’application de bureau disponible. Choisissez un paquet Linux." - #. Modal title shown when a new desktop app update can be downloaded. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop update available" msgstr "Mise à jour de l’application de bureau disponible" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format and no version is known. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update available. Choose a Linux package." -msgstr "Mise à jour de l’application disponible. Choisissez un paquet Linux." - #. Desktop updater modal body. The version placeholder is the target app version; productName is the app name. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop version {version} is available. Download the installer, then run it to update {productName}." @@ -13049,11 +13039,6 @@ msgstr "Télécharger l’installateur" msgid "Download media" msgstr "Télécharger le média" -#. Button label that downloads a selected Linux desktop update package. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Download package" -msgstr "Télécharger le paquet" - #. Button that downloads the account recovery kit as a PDF file. #: src/features/auth/components/modals/RecoveryKitModal.tsx msgid "Download PDF" @@ -18280,11 +18265,6 @@ msgstr "Installez-le depuis le menu de votre navigateur." msgid "Install the new app" msgstr "Installer la nouvelle application" -#. Label for the installed desktop version in the Linux update modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Installed" -msgstr "Installé" - #. Desktop updater modal body. The currentVersion placeholder is the installed app version. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Installed version: {currentVersion}." @@ -19794,11 +19774,6 @@ msgstr "Application Linux" msgid "Linux default" msgstr "Linux (par défaut)" -#. Select label for choosing the Linux desktop package format to download. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Linux package" -msgstr "Paquet Linux" - #. Forum layout name shown inside an activity log row. #. Forum layout option that shows posts as rows. #: src/features/forum/utils/ForumMessageDescriptors.ts @@ -22430,11 +22405,6 @@ msgstr "Les nouveaux fils de discussion ne s'afficheront plus dans la liste des msgid "New users can't join right now. You can still create an account or sign in, then try this link again later." msgstr "De nouveaux membres ne peuvent pas rejoindre la communauté pour le moment. Vous pouvez tout de même créer un compte ou vous connecter, puis réessayer ce lien plus tard." -#. Fallback version label when the available desktop version is unknown. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "New version" -msgstr "Nouvelle version" - #. US state name, shown next to the country name in the mature content gate. #: src/features/user/utils/UserGeo.ts msgctxt "us-state" @@ -24142,6 +24112,8 @@ msgid "Open settings" msgstr "Ouvrir les réglages" #. Button label that opens the software center via appstream protocol. +#. Native desktop dialog button on a Flatpak install that opens the software center page for the desktop app. +#: src/features/platform/utils/DesktopLocaleBridge.ts #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Open software center" msgstr "Ouvrir le centre logiciel" @@ -28464,11 +28436,6 @@ msgstr "Case à cocher de style rond" msgid "Rows per page" msgstr "Lignes par page" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "RPM" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "RPM package" -msgstr "Paquet RPM" - #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts msgid "Run {productName} automatically when your computer starts" @@ -33120,11 +33087,6 @@ msgstr "Touchez Partager, puis Sur l’écran d’accueil." msgid "Tap to add a note" msgstr "Appuyer pour ajouter une note" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "tar.gz" verbatim and translate only the word "archive". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "tar.gz archive" -msgstr "Archive tar.gz" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/SharedDescriptors.ts msgid "Taskbar badge" @@ -33513,11 +33475,6 @@ msgstr "La connexion passe à TLS uniquement si le serveur le propose." msgid "The desktop update could not be downloaded. Try again from the update button when your connection is stable." msgstr "Impossible de télécharger la mise à jour de l’application de bureau. Réessayez avec le bouton de mise à jour lorsque votre connexion sera stable." -#. Help text below the Linux package format selector in the desktop updater modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "The downloaded file is saved to your computer. Install it with your normal package manager." -msgstr "Le fichier téléchargé est enregistré sur votre ordinateur. Installez-le avec votre gestionnaire de paquets habituel." - #. Activity log detail line under a deleted emoji, shown only when someone other than the person who deleted it had uploaded it. {user} is the user who uploaded the emoji, shown as a clickable user chip. #: src/features/guild/utils/guild_tabs/audit_log/AuditLogExpressionMessageMessages.ts msgid "The emoji was uploaded by {user}" @@ -36548,11 +36505,21 @@ msgstr "Jusqu'à 4K à 60 FPS" msgid "Up to 720p at 30 FPS" msgstr "Jusqu'à 720p à 30 IPS" +#. Native desktop dialog body under the headline announcing a new version, shown on a Flatpak install. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} from your software center." +msgstr "Mettez à jour {appName} depuis votre centre logiciel." + #. Tray menu item shown only while a desktop update is waiting. Clicking it installs the update. {appName} is the desktop app name (typically Fluxer). #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "Update {appName} now" msgstr "Mettre à jour {appName} maintenant" +#. Native desktop dialog body under the headline announcing a new version, shown on a Linux install managed by a package manager such as a deb, rpm or AUR package. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} with your package manager, or download the new version." +msgstr "Mettez à jour {appName} avec votre gestionnaire de paquets, ou téléchargez la nouvelle version." + #. Linux settings text when the installed desktop app is too old for system-wide shortcuts. {productName} is the app name. #: src/features/user/components/modals/tabs/components/SystemShortcutsSection.tsx msgid "Update {productName} to use system-wide shortcuts on Wayland." diff --git a/fluxer_app/src/features/i18n/locales/he/messages.po b/fluxer_app/src/features/i18n/locales/he/messages.po index 5af80e088..755c38e07 100644 --- a/fluxer_app/src/features/i18n/locales/he/messages.po +++ b/fluxer_app/src/features/i18n/locales/he/messages.po @@ -1802,6 +1802,16 @@ msgstr "כדי שהשינוי בשורת הכותרת ייכנס לתוקף, י msgid "{PRODUCT_NAME} needs to restart to disable Chromium's hardware acceleration. Use this only if you're troubleshooting graphics glitches or high GPU usage." msgstr "יש להפעיל מחדש את {PRODUCT_NAME} כדי להשבית את האצת החומרה של Chromium. השתמש באפשרות זו רק אם אתה מנסה לפתור תקלות גרפיקה או שימוש גבוה במעבד הגרפי." +#. Desktop updater modal body for a Flatpak install when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it from your software center." +msgstr "{productName} גרסה {version} זמינה. עדכן אותה ממרכז התוכנה שלך." + +#. Desktop updater modal body for a Linux install managed by a package manager, such as a deb, rpm or AUR package, when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it with your package manager, or download the new version." +msgstr "{productName} גרסה {version} זמינה. עדכן אותה באמצעות מנהל החבילות שלך, או הורד את הגרסה החדשה." + #. Accessible label for the Fluxer application icon. #: src/features/ui/components/icons/FluxerIconMark.tsx msgid "{productName} application icon" @@ -2669,11 +2679,6 @@ msgstr "בוט מועיל שעושה דברים מדהימים!" msgid "a hostname, e.g. {exampleDomain}" msgstr "שם מארח, לדוגמה {exampleDomain}" -#. Linux desktop updater modal body shown before version details and package format selector. productName is the app name. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "A Linux desktop update is available. Choose the package format for this system and {productName} will save it locally." -msgstr "זמין עדכון לאפליקציית שולחן העבודה ללינוקס. בחר את פורמט החבילה עבור המערכת הזו, ו-{productName} ישמור אותו באופן מקומי." - #. Plutonium subscription card line offering a move to a lower price. {listPriceEffectiveDate} is a date already formatted and localized by code; never write a date into the translation. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx msgid "A lower price is available. Switching takes effect on <0>{listPriceEffectiveDate} and changes nothing else." @@ -11427,11 +11432,6 @@ msgstr "השמע מושתק" msgid "Deafened by moderators" msgstr "השמע הושתק על ידי מנהלים" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "DEB" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "DEB package" -msgstr "חבילת DEB" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label in the settings dialog mobile settings view. #: src/features/app/components/dialogs/components/MobileSettingsView.tsx @@ -12095,21 +12095,11 @@ msgstr "התראות שולחן עבודה נחסמו. ניתן להפעיל א msgid "Desktop startup" msgstr "הפעלה במחשב" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update {version} available. Choose a Linux package." -msgstr "עדכון {version} זמין לאפליקציית המחשב. בחר חבילת Linux." - #. Modal title shown when a new desktop app update can be downloaded. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop update available" msgstr "עדכון זמין לאפליקציית המחשב" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format and no version is known. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update available. Choose a Linux package." -msgstr "עדכון זמין לאפליקציית המחשב. בחר חבילת Linux." - #. Desktop updater modal body. The version placeholder is the target app version; productName is the app name. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop version {version} is available. Download the installer, then run it to update {productName}." @@ -13049,11 +13039,6 @@ msgstr "הורדת קובץ ההתקנה" msgid "Download media" msgstr "הורדת מדיה" -#. Button label that downloads a selected Linux desktop update package. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Download package" -msgstr "הורדת חבילה" - #. Button that downloads the account recovery kit as a PDF file. #: src/features/auth/components/modals/RecoveryKitModal.tsx msgid "Download PDF" @@ -18280,11 +18265,6 @@ msgstr "התקן אותו מתפריט הדפדפן שלך." msgid "Install the new app" msgstr "התקן את האפליקציה החדשה" -#. Label for the installed desktop version in the Linux update modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Installed" -msgstr "מותקן" - #. Desktop updater modal body. The currentVersion placeholder is the installed app version. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Installed version: {currentVersion}." @@ -19794,11 +19774,6 @@ msgstr "אפליקציית לינוקס" msgid "Linux default" msgstr "ברירת מחדל של לינוקס" -#. Select label for choosing the Linux desktop package format to download. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Linux package" -msgstr "חבילת לינוקס" - #. Forum layout name shown inside an activity log row. #. Forum layout option that shows posts as rows. #: src/features/forum/utils/ForumMessageDescriptors.ts @@ -22430,11 +22405,6 @@ msgstr "שרשורים חדשים יפסיקו להופיע ברשימת הער msgid "New users can't join right now. You can still create an account or sign in, then try this link again later." msgstr "משתמשים חדשים לא יכולים להצטרף כרגע. עדיין באפשרותך ליצור חשבון או להיכנס, ואז לנסות את הקישור הזה שוב מאוחר יותר." -#. Fallback version label when the available desktop version is unknown. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "New version" -msgstr "גרסה חדשה" - #. US state name, shown next to the country name in the mature content gate. #: src/features/user/utils/UserGeo.ts msgctxt "us-state" @@ -24142,6 +24112,8 @@ msgid "Open settings" msgstr "פתח הגדרות" #. Button label that opens the software center via appstream protocol. +#. Native desktop dialog button on a Flatpak install that opens the software center page for the desktop app. +#: src/features/platform/utils/DesktopLocaleBridge.ts #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Open software center" msgstr "פתח את מרכז התוכנה" @@ -28464,11 +28436,6 @@ msgstr "תיבת סימון עגולה" msgid "Rows per page" msgstr "שורות לעמוד" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "RPM" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "RPM package" -msgstr "חבילת RPM" - #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts msgid "Run {productName} automatically when your computer starts" @@ -33120,11 +33087,6 @@ msgstr "הקש על \"שיתוף\", ואז על \"הוספה למסך הבית\" msgid "Tap to add a note" msgstr "הקש כדי להוסיף הערה" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "tar.gz" verbatim and translate only the word "archive". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "tar.gz archive" -msgstr "ארכיון tar.gz" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/SharedDescriptors.ts msgid "Taskbar badge" @@ -33513,11 +33475,6 @@ msgstr "החיבור משודרג ל-TLS רק אם השרת מציע זאת." msgid "The desktop update could not be downloaded. Try again from the update button when your connection is stable." msgstr "לא ניתן היה להוריד את עדכון שולחן העבודה. נסה שוב באמצעות כפתור העדכון כשהחיבור שלך יציב." -#. Help text below the Linux package format selector in the desktop updater modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "The downloaded file is saved to your computer. Install it with your normal package manager." -msgstr "הקובץ שהורד נשמר במחשב שלך. התקן אותו באמצעות מנהל החבילות הרגיל שלך." - #. Activity log detail line under a deleted emoji, shown only when someone other than the person who deleted it had uploaded it. {user} is the user who uploaded the emoji, shown as a clickable user chip. #: src/features/guild/utils/guild_tabs/audit_log/AuditLogExpressionMessageMessages.ts msgid "The emoji was uploaded by {user}" @@ -36548,11 +36505,21 @@ msgstr "עד 4K ב-60 FPS" msgid "Up to 720p at 30 FPS" msgstr "עד 720p ב-30 FPS" +#. Native desktop dialog body under the headline announcing a new version, shown on a Flatpak install. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} from your software center." +msgstr "עדכן את {appName} ממרכז התוכנה שלך." + #. Tray menu item shown only while a desktop update is waiting. Clicking it installs the update. {appName} is the desktop app name (typically Fluxer). #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "Update {appName} now" msgstr "עדכן את {appName} כעת" +#. Native desktop dialog body under the headline announcing a new version, shown on a Linux install managed by a package manager such as a deb, rpm or AUR package. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} with your package manager, or download the new version." +msgstr "עדכן את {appName} באמצעות מנהל החבילות שלך, או הורד את הגרסה החדשה." + #. Linux settings text when the installed desktop app is too old for system-wide shortcuts. {productName} is the app name. #: src/features/user/components/modals/tabs/components/SystemShortcutsSection.tsx msgid "Update {productName} to use system-wide shortcuts on Wayland." diff --git a/fluxer_app/src/features/i18n/locales/hi/messages.po b/fluxer_app/src/features/i18n/locales/hi/messages.po index 47ab8658a..13aedacdb 100644 --- a/fluxer_app/src/features/i18n/locales/hi/messages.po +++ b/fluxer_app/src/features/i18n/locales/hi/messages.po @@ -1802,6 +1802,16 @@ msgstr "टाइटल बार का बदलाव लागू होन msgid "{PRODUCT_NAME} needs to restart to disable Chromium's hardware acceleration. Use this only if you're troubleshooting graphics glitches or high GPU usage." msgstr "क्रोमियम का हार्डवेयर एक्सीलरेशन बंद करने के लिए {PRODUCT_NAME} को रीस्टार्ट करना होगा। इसे तभी इस्तेमाल करें जब आप ग्राफ़िक्स की गड़बड़ियों या ज़्यादा GPU इस्तेमाल की समस्या सुलझा रहे हों।" +#. Desktop updater modal body for a Flatpak install when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it from your software center." +msgstr "{productName} {version} उपलब्ध है। इसे अपने सॉफ़्टवेयर सेंटर से अपडेट करें।" + +#. Desktop updater modal body for a Linux install managed by a package manager, such as a deb, rpm or AUR package, when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it with your package manager, or download the new version." +msgstr "{productName} {version} उपलब्ध है। इसे अपने पैकेज मैनेजर से अपडेट करें, या नया संस्करण डाउनलोड करें।" + #. Accessible label for the Fluxer application icon. #: src/features/ui/components/icons/FluxerIconMark.tsx msgid "{productName} application icon" @@ -2669,11 +2679,6 @@ msgstr "एक मददगार बॉट जो कमाल के काम msgid "a hostname, e.g. {exampleDomain}" msgstr "एक होस्टनेम, जैसे {exampleDomain}" -#. Linux desktop updater modal body shown before version details and package format selector. productName is the app name. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "A Linux desktop update is available. Choose the package format for this system and {productName} will save it locally." -msgstr "लिनक्स डेस्कटॉप के लिए एक अपडेट उपलब्ध है। इस सिस्टम के लिए पैकेज फ़ॉर्मैट चुनें और {productName} उसे आपके डिवाइस पर सेव कर लेगा।" - #. Plutonium subscription card line offering a move to a lower price. {listPriceEffectiveDate} is a date already formatted and localized by code; never write a date into the translation. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx msgid "A lower price is available. Switching takes effect on <0>{listPriceEffectiveDate} and changes nothing else." @@ -11427,11 +11432,6 @@ msgstr "डीफ़ेन किया गया" msgid "Deafened by moderators" msgstr "मॉडरेटर ने डीफ़ेन किया" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "DEB" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "DEB package" -msgstr "DEB पैकेज" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label in the settings dialog mobile settings view. #: src/features/app/components/dialogs/components/MobileSettingsView.tsx @@ -12095,21 +12095,11 @@ msgstr "डेस्कटॉप नोटिफ़िकेशन ब्लॉ msgid "Desktop startup" msgstr "डेस्कटॉप स्टार्टअप" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update {version} available. Choose a Linux package." -msgstr "डेस्कटॉप अपडेट {version} उपलब्ध है। एक लिनक्स पैकेज चुनें।" - #. Modal title shown when a new desktop app update can be downloaded. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop update available" msgstr "डेस्कटॉप अपडेट उपलब्ध है" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format and no version is known. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update available. Choose a Linux package." -msgstr "डेस्कटॉप अपडेट उपलब्ध है। एक लिनक्स पैकेज चुनें।" - #. Desktop updater modal body. The version placeholder is the target app version; productName is the app name. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop version {version} is available. Download the installer, then run it to update {productName}." @@ -13049,11 +13039,6 @@ msgstr "इंस्टॉलर डाउनलोड करें" msgid "Download media" msgstr "मीडिया डाउनलोड करें" -#. Button label that downloads a selected Linux desktop update package. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Download package" -msgstr "पैकेज डाउनलोड करें" - #. Button that downloads the account recovery kit as a PDF file. #: src/features/auth/components/modals/RecoveryKitModal.tsx msgid "Download PDF" @@ -18280,11 +18265,6 @@ msgstr "अपने ब्राउज़र मेनू से इसे इ msgid "Install the new app" msgstr "नया ऐप इंस्टॉल करें" -#. Label for the installed desktop version in the Linux update modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Installed" -msgstr "इंस्टॉल किया गया" - #. Desktop updater modal body. The currentVersion placeholder is the installed app version. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Installed version: {currentVersion}." @@ -19794,11 +19774,6 @@ msgstr "लिनक्स ऐप" msgid "Linux default" msgstr "लिनक्स डिफ़ॉल्ट" -#. Select label for choosing the Linux desktop package format to download. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Linux package" -msgstr "लिनक्स पैकेज" - #. Forum layout name shown inside an activity log row. #. Forum layout option that shows posts as rows. #: src/features/forum/utils/ForumMessageDescriptors.ts @@ -22430,11 +22405,6 @@ msgstr "इस अवधि की निष्क्रियता के ब msgid "New users can't join right now. You can still create an account or sign in, then try this link again later." msgstr "नए यूज़र अभी शामिल नहीं हो सकते। आप अभी भी अकाउंट बना सकते हैं या साइन इन कर सकते हैं, फिर बाद में इस लिंक को दोबारा आज़माएँ।" -#. Fallback version label when the available desktop version is unknown. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "New version" -msgstr "नया वर्ज़न" - #. US state name, shown next to the country name in the mature content gate. #: src/features/user/utils/UserGeo.ts msgctxt "us-state" @@ -24142,6 +24112,8 @@ msgid "Open settings" msgstr "सेटिंग्स खोलें" #. Button label that opens the software center via appstream protocol. +#. Native desktop dialog button on a Flatpak install that opens the software center page for the desktop app. +#: src/features/platform/utils/DesktopLocaleBridge.ts #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Open software center" msgstr "सॉफ़्टवेयर सेंटर खोलें" @@ -28464,11 +28436,6 @@ msgstr "गोल स्टाइल चेकबॉक्स" msgid "Rows per page" msgstr "प्रति पेज पंक्तियाँ" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "RPM" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "RPM package" -msgstr "RPM पैकेज" - #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts msgid "Run {productName} automatically when your computer starts" @@ -33120,11 +33087,6 @@ msgstr "\"शेयर करें\" पर टैप करें, फिर \ msgid "Tap to add a note" msgstr "नोट जोड़ने के लिए टैप करें" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "tar.gz" verbatim and translate only the word "archive". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "tar.gz archive" -msgstr "tar.gz आर्काइव" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/SharedDescriptors.ts msgid "Taskbar badge" @@ -33513,11 +33475,6 @@ msgstr "सर्वर द्वारा प्रदान किए जा msgid "The desktop update could not be downloaded. Try again from the update button when your connection is stable." msgstr "डेस्कटॉप अपडेट डाउनलोड नहीं किया जा सका। जब आपका कनेक्शन स्थिर हो, तो अपडेट बटन से फिर से कोशिश करें।" -#. Help text below the Linux package format selector in the desktop updater modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "The downloaded file is saved to your computer. Install it with your normal package manager." -msgstr "डाउनलोड की गई फ़ाइल आपके कंप्यूटर पर सेव हो गई है। इसे अपने सामान्य पैकेज मैनेजर से इंस्टॉल करें।" - #. Activity log detail line under a deleted emoji, shown only when someone other than the person who deleted it had uploaded it. {user} is the user who uploaded the emoji, shown as a clickable user chip. #: src/features/guild/utils/guild_tabs/audit_log/AuditLogExpressionMessageMessages.ts msgid "The emoji was uploaded by {user}" @@ -36548,11 +36505,21 @@ msgstr "60 FPS पर 4K तक" msgid "Up to 720p at 30 FPS" msgstr "30 FPS पर 720p तक" +#. Native desktop dialog body under the headline announcing a new version, shown on a Flatpak install. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} from your software center." +msgstr "अपने सॉफ़्टवेयर सेंटर से {appName} अपडेट करें।" + #. Tray menu item shown only while a desktop update is waiting. Clicking it installs the update. {appName} is the desktop app name (typically Fluxer). #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "Update {appName} now" msgstr "{appName} को अभी अपडेट करें" +#. Native desktop dialog body under the headline announcing a new version, shown on a Linux install managed by a package manager such as a deb, rpm or AUR package. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} with your package manager, or download the new version." +msgstr "अपने पैकेज मैनेजर से {appName} अपडेट करें, या नया संस्करण डाउनलोड करें।" + #. Linux settings text when the installed desktop app is too old for system-wide shortcuts. {productName} is the app name. #: src/features/user/components/modals/tabs/components/SystemShortcutsSection.tsx msgid "Update {productName} to use system-wide shortcuts on Wayland." diff --git a/fluxer_app/src/features/i18n/locales/hr/messages.po b/fluxer_app/src/features/i18n/locales/hr/messages.po index d20b04719..f11de59e0 100644 --- a/fluxer_app/src/features/i18n/locales/hr/messages.po +++ b/fluxer_app/src/features/i18n/locales/hr/messages.po @@ -1802,6 +1802,16 @@ msgstr "{PRODUCT_NAME} se mora ponovno pokrenuti kako bi promjena naslovne trake msgid "{PRODUCT_NAME} needs to restart to disable Chromium's hardware acceleration. Use this only if you're troubleshooting graphics glitches or high GPU usage." msgstr "{PRODUCT_NAME} se mora ponovno pokrenuti kako bi se onemogućilo hardversko ubrzanje Chromiuma. Koristite ovo samo ako rješavate probleme s grafičkim smetnjama ili visokom upotrebom GPU-a." +#. Desktop updater modal body for a Flatpak install when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it from your software center." +msgstr "Dostupna je nova verzija {productName} {version}. Ažurirajte je iz svog centra za softver." + +#. Desktop updater modal body for a Linux install managed by a package manager, such as a deb, rpm or AUR package, when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it with your package manager, or download the new version." +msgstr "Dostupna je verzija {version} za {productName}. Ažurirajte je pomoću upravitelja paketa ili preuzmite novu verziju." + #. Accessible label for the Fluxer application icon. #: src/features/ui/components/icons/FluxerIconMark.tsx msgid "{productName} application icon" @@ -2669,11 +2679,6 @@ msgstr "Koristan bot koji radi nevjerojatne stvari!" msgid "a hostname, e.g. {exampleDomain}" msgstr "naziv hosta, npr. {exampleDomain}" -#. Linux desktop updater modal body shown before version details and package format selector. productName is the app name. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "A Linux desktop update is available. Choose the package format for this system and {productName} will save it locally." -msgstr "Dostupno je ažuriranje aplikacije za Linux. Odaberite format paketa za ovaj sustav i {productName} će ga spremiti lokalno." - #. Plutonium subscription card line offering a move to a lower price. {listPriceEffectiveDate} is a date already formatted and localized by code; never write a date into the translation. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx msgid "A lower price is available. Switching takes effect on <0>{listPriceEffectiveDate} and changes nothing else." @@ -11427,11 +11432,6 @@ msgstr "Bez zvuka" msgid "Deafened by moderators" msgstr "Moderatori su vam isključili zvuk" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "DEB" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "DEB package" -msgstr "DEB paket" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label in the settings dialog mobile settings view. #: src/features/app/components/dialogs/components/MobileSettingsView.tsx @@ -12095,21 +12095,11 @@ msgstr "Blokirane su obavijesti na radnoj površini. Možete ih omogućiti kasni msgid "Desktop startup" msgstr "Pokretanje na računalu" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update {version} available. Choose a Linux package." -msgstr "Dostupno je ažuriranje za stolno računalo {version}. Odaberite Linux paket." - #. Modal title shown when a new desktop app update can be downloaded. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop update available" msgstr "Dostupno je ažuriranje za stolno računalo" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format and no version is known. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update available. Choose a Linux package." -msgstr "Dostupno je ažuriranje za stolno računalo. Odaberite Linux paket." - #. Desktop updater modal body. The version placeholder is the target app version; productName is the app name. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop version {version} is available. Download the installer, then run it to update {productName}." @@ -13049,11 +13039,6 @@ msgstr "Preuzmi instalacijski program" msgid "Download media" msgstr "Preuzmi medij" -#. Button label that downloads a selected Linux desktop update package. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Download package" -msgstr "Preuzmi paket" - #. Button that downloads the account recovery kit as a PDF file. #: src/features/auth/components/modals/RecoveryKitModal.tsx msgid "Download PDF" @@ -18280,11 +18265,6 @@ msgstr "Instalirajte ga iz izbornika preglednika." msgid "Install the new app" msgstr "Instaliraj novu aplikaciju" -#. Label for the installed desktop version in the Linux update modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Installed" -msgstr "Instalirano" - #. Desktop updater modal body. The currentVersion placeholder is the installed app version. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Installed version: {currentVersion}." @@ -19794,11 +19774,6 @@ msgstr "Linux aplikacija" msgid "Linux default" msgstr "Linux – zadani uređaj" -#. Select label for choosing the Linux desktop package format to download. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Linux package" -msgstr "Linux paket" - #. Forum layout name shown inside an activity log row. #. Forum layout option that shows posts as rows. #: src/features/forum/utils/ForumMessageDescriptors.ts @@ -22430,11 +22405,6 @@ msgstr "Nove niti se više neće prikazivati na popisu kanala nakon ovog razdobl msgid "New users can't join right now. You can still create an account or sign in, then try this link again later." msgstr "Novi se korisnici trenutačno ne mogu pridružiti. I dalje možete stvoriti račun ili se prijaviti pa kasnije ponovno pokušati s ovom poveznicom." -#. Fallback version label when the available desktop version is unknown. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "New version" -msgstr "Nova verzija" - #. US state name, shown next to the country name in the mature content gate. #: src/features/user/utils/UserGeo.ts msgctxt "us-state" @@ -24142,6 +24112,8 @@ msgid "Open settings" msgstr "Otvori postavke" #. Button label that opens the software center via appstream protocol. +#. Native desktop dialog button on a Flatpak install that opens the software center page for the desktop app. +#: src/features/platform/utils/DesktopLocaleBridge.ts #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Open software center" msgstr "Otvori centar za softver" @@ -28464,11 +28436,6 @@ msgstr "Okrugli potvrdni okvir" msgid "Rows per page" msgstr "Redaka po stranici" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "RPM" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "RPM package" -msgstr "RPM paket" - #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts msgid "Run {productName} automatically when your computer starts" @@ -33120,11 +33087,6 @@ msgstr "Dodirnite Dijeli, a zatim Dodaj na početni zaslon." msgid "Tap to add a note" msgstr "Dodirnite za dodavanje bilješke" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "tar.gz" verbatim and translate only the word "archive". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "tar.gz archive" -msgstr "tar.gz arhiva" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/SharedDescriptors.ts msgid "Taskbar badge" @@ -33513,11 +33475,6 @@ msgstr "Veza se nadograđuje na TLS samo ako je poslužitelj ponudi." msgid "The desktop update could not be downloaded. Try again from the update button when your connection is stable." msgstr "Ažuriranje za stolno računalo nije preuzeto. Pokušajte ponovno pomoću gumba za ažuriranje kada vaša veza bude stabilna." -#. Help text below the Linux package format selector in the desktop updater modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "The downloaded file is saved to your computer. Install it with your normal package manager." -msgstr "Preuzeta datoteka sprema se na vaše računalo. Instalirajte je pomoću uobičajenog upravitelja paketa." - #. Activity log detail line under a deleted emoji, shown only when someone other than the person who deleted it had uploaded it. {user} is the user who uploaded the emoji, shown as a clickable user chip. #: src/features/guild/utils/guild_tabs/audit_log/AuditLogExpressionMessageMessages.ts msgid "The emoji was uploaded by {user}" @@ -36548,11 +36505,21 @@ msgstr "Do 4K pri 60 FPS" msgid "Up to 720p at 30 FPS" msgstr "Do 720p pri 30 FPS" +#. Native desktop dialog body under the headline announcing a new version, shown on a Flatpak install. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} from your software center." +msgstr "Ažuriraj {appName} iz svog centra za softver." + #. Tray menu item shown only while a desktop update is waiting. Clicking it installs the update. {appName} is the desktop app name (typically Fluxer). #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "Update {appName} now" msgstr "Ažuriraj {appName} sada" +#. Native desktop dialog body under the headline announcing a new version, shown on a Linux install managed by a package manager such as a deb, rpm or AUR package. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} with your package manager, or download the new version." +msgstr "Ažurirajte {appName} pomoću upravitelja paketa ili preuzmite novu verziju." + #. Linux settings text when the installed desktop app is too old for system-wide shortcuts. {productName} is the app name. #: src/features/user/components/modals/tabs/components/SystemShortcutsSection.tsx msgid "Update {productName} to use system-wide shortcuts on Wayland." diff --git a/fluxer_app/src/features/i18n/locales/hu/messages.po b/fluxer_app/src/features/i18n/locales/hu/messages.po index 293e294fe..87d85206d 100644 --- a/fluxer_app/src/features/i18n/locales/hu/messages.po +++ b/fluxer_app/src/features/i18n/locales/hu/messages.po @@ -1802,6 +1802,16 @@ msgstr "A címsor módosításának érvénybe lépéséhez újra kell indítani msgid "{PRODUCT_NAME} needs to restart to disable Chromium's hardware acceleration. Use this only if you're troubleshooting graphics glitches or high GPU usage." msgstr "A(z) {PRODUCT_NAME} újraindítása szükséges a Chromium hardveres gyorsításának kikapcsolásához. Ezt csak akkor használd, ha grafikai hibákat vagy magas GPU-használatot észlelsz." +#. Desktop updater modal body for a Flatpak install when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it from your software center." +msgstr "{productName} {version} elérhető. Frissítsd a szoftverközpontodból." + +#. Desktop updater modal body for a Linux install managed by a package manager, such as a deb, rpm or AUR package, when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it with your package manager, or download the new version." +msgstr "{productName} {version} érhető el. Frissítsd a csomagkezelőddel, vagy töltsd le az új verziót." + #. Accessible label for the Fluxer application icon. #: src/features/ui/components/icons/FluxerIconMark.tsx msgid "{productName} application icon" @@ -2669,11 +2679,6 @@ msgstr "Egy segítőkész bot, ami csodálatos dolgokra képes!" msgid "a hostname, e.g. {exampleDomain}" msgstr "állomásnév, pl. {exampleDomain}" -#. Linux desktop updater modal body shown before version details and package format selector. productName is the app name. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "A Linux desktop update is available. Choose the package format for this system and {productName} will save it locally." -msgstr "Elérhető egy frissítés az asztali Linux-alkalmazáshoz. Válaszd ki a rendszeredhez megfelelő csomagformátumot, és a(z) {productName} helyileg menti a csomagot." - #. Plutonium subscription card line offering a move to a lower price. {listPriceEffectiveDate} is a date already formatted and localized by code; never write a date into the translation. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx msgid "A lower price is available. Switching takes effect on <0>{listPriceEffectiveDate} and changes nothing else." @@ -11427,11 +11432,6 @@ msgstr "Hang kikapcsolva" msgid "Deafened by moderators" msgstr "A moderátorok kikapcsolták a hangot" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "DEB" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "DEB package" -msgstr "DEB csomag" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label in the settings dialog mobile settings view. #: src/features/app/components/dialogs/components/MobileSettingsView.tsx @@ -12095,21 +12095,11 @@ msgstr "Az asztali értesítések le vannak tiltva. Később engedélyezheted ő msgid "Desktop startup" msgstr "Asztali indítás" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update {version} available. Choose a Linux package." -msgstr "Elérhető a(z) {version} asztali frissítés. Válassz Linux-csomagot." - #. Modal title shown when a new desktop app update can be downloaded. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop update available" msgstr "Asztali frissítés érhető el" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format and no version is known. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update available. Choose a Linux package." -msgstr "Asztali frissítés érhető el. Válassz Linux-csomagot." - #. Desktop updater modal body. The version placeholder is the target app version; productName is the app name. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop version {version} is available. Download the installer, then run it to update {productName}." @@ -13049,11 +13039,6 @@ msgstr "Telepítő letöltése" msgid "Download media" msgstr "Média letöltése" -#. Button label that downloads a selected Linux desktop update package. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Download package" -msgstr "Csomag letöltése" - #. Button that downloads the account recovery kit as a PDF file. #: src/features/auth/components/modals/RecoveryKitModal.tsx msgid "Download PDF" @@ -18280,11 +18265,6 @@ msgstr "Telepítsd a böngésző menüjéből." msgid "Install the new app" msgstr "Telepítsd az új alkalmazást" -#. Label for the installed desktop version in the Linux update modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Installed" -msgstr "Telepítve" - #. Desktop updater modal body. The currentVersion placeholder is the installed app version. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Installed version: {currentVersion}." @@ -19794,11 +19774,6 @@ msgstr "Linux-alkalmazás" msgid "Linux default" msgstr "Linux alapértelmezett" -#. Select label for choosing the Linux desktop package format to download. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Linux package" -msgstr "Linux-csomag" - #. Forum layout name shown inside an activity log row. #. Forum layout option that shows posts as rows. #: src/features/forum/utils/ForumMessageDescriptors.ts @@ -22430,11 +22405,6 @@ msgstr "Az új szálak egy idő után nem jelennek meg a csatornalistán." msgid "New users can't join right now. You can still create an account or sign in, then try this link again later." msgstr "Új felhasználók jelenleg nem tudnak csatlakozni. Fiókot továbbra is létrehozhatsz, vagy bejelentkezhetsz, majd később újra megnyithatod ezt a linket." -#. Fallback version label when the available desktop version is unknown. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "New version" -msgstr "Új verzió" - #. US state name, shown next to the country name in the mature content gate. #: src/features/user/utils/UserGeo.ts msgctxt "us-state" @@ -24142,6 +24112,8 @@ msgid "Open settings" msgstr "Beállítások megnyitása" #. Button label that opens the software center via appstream protocol. +#. Native desktop dialog button on a Flatpak install that opens the software center page for the desktop app. +#: src/features/platform/utils/DesktopLocaleBridge.ts #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Open software center" msgstr "Szoftverközpont megnyitása" @@ -28464,11 +28436,6 @@ msgstr "Kerek stílusú jelölőnégyzet" msgid "Rows per page" msgstr "Sorok száma oldalanként" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "RPM" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "RPM package" -msgstr "RPM csomag" - #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts msgid "Run {productName} automatically when your computer starts" @@ -33120,11 +33087,6 @@ msgstr "Érintsd meg a Megosztás gombot, majd a Főképernyőhöz adás lehető msgid "Tap to add a note" msgstr "Koppints jegyzet hozzáadásához" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "tar.gz" verbatim and translate only the word "archive". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "tar.gz archive" -msgstr "tar.gz archívum" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/SharedDescriptors.ts msgid "Taskbar badge" @@ -33513,11 +33475,6 @@ msgstr "A kapcsolat csak akkor frissül TLS-re, ha a szerver felajánlja azt." msgid "The desktop update could not be downloaded. Try again from the update button when your connection is stable." msgstr "Nem sikerült letölteni az asztali frissítést. Próbáld újra a frissítés gombbal, ha stabil a kapcsolatod." -#. Help text below the Linux package format selector in the desktop updater modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "The downloaded file is saved to your computer. Install it with your normal package manager." -msgstr "A letöltött fájl a számítógépedre mentve. Telepítsd a szokásos csomagkezelőddel." - #. Activity log detail line under a deleted emoji, shown only when someone other than the person who deleted it had uploaded it. {user} is the user who uploaded the emoji, shown as a clickable user chip. #: src/features/guild/utils/guild_tabs/audit_log/AuditLogExpressionMessageMessages.ts msgid "The emoji was uploaded by {user}" @@ -36548,11 +36505,21 @@ msgstr "Akár 4K felbontás 60 FPS sebességgel" msgid "Up to 720p at 30 FPS" msgstr "Akár 720p felbontás 30 FPS sebességgel" +#. Native desktop dialog body under the headline announcing a new version, shown on a Flatpak install. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} from your software center." +msgstr "Frissítsd a(z) {appName} alkalmazást a szoftverközpontodból." + #. Tray menu item shown only while a desktop update is waiting. Clicking it installs the update. {appName} is the desktop app name (typically Fluxer). #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "Update {appName} now" msgstr "Frissítsd a(z) {appName} rendszert most" +#. Native desktop dialog body under the headline announcing a new version, shown on a Linux install managed by a package manager such as a deb, rpm or AUR package. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} with your package manager, or download the new version." +msgstr "Frissítsd a(z) {appName} alkalmazást a csomagkezelőddel, vagy töltsd le az új verziót." + #. Linux settings text when the installed desktop app is too old for system-wide shortcuts. {productName} is the app name. #: src/features/user/components/modals/tabs/components/SystemShortcutsSection.tsx msgid "Update {productName} to use system-wide shortcuts on Wayland." diff --git a/fluxer_app/src/features/i18n/locales/id/messages.po b/fluxer_app/src/features/i18n/locales/id/messages.po index 2425bee12..ed55f83ce 100644 --- a/fluxer_app/src/features/i18n/locales/id/messages.po +++ b/fluxer_app/src/features/i18n/locales/id/messages.po @@ -1802,6 +1802,16 @@ msgstr "{PRODUCT_NAME} perlu dimulai ulang agar perubahan bilah judul berlaku." msgid "{PRODUCT_NAME} needs to restart to disable Chromium's hardware acceleration. Use this only if you're troubleshooting graphics glitches or high GPU usage." msgstr "{PRODUCT_NAME} perlu dimulai ulang untuk menonaktifkan akselerasi perangkat keras Chromium. Gunakan ini hanya jika kamu sedang mengatasi gangguan grafis atau penggunaan GPU yang tinggi." +#. Desktop updater modal body for a Flatpak install when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it from your software center." +msgstr "{productName} {version} tersedia. Perbarui dari pusat perangkat lunak Anda." + +#. Desktop updater modal body for a Linux install managed by a package manager, such as a deb, rpm or AUR package, when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it with your package manager, or download the new version." +msgstr "{productName} {version} tersedia. Perbarui menggunakan manajer paket Anda, atau unduh versi baru." + #. Accessible label for the Fluxer application icon. #: src/features/ui/components/icons/FluxerIconMark.tsx msgid "{productName} application icon" @@ -2669,11 +2679,6 @@ msgstr "Bot penolong yang bisa melakukan hal-hal menakjubkan!" msgid "a hostname, e.g. {exampleDomain}" msgstr "nama host, mis. {exampleDomain}" -#. Linux desktop updater modal body shown before version details and package format selector. productName is the app name. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "A Linux desktop update is available. Choose the package format for this system and {productName} will save it locally." -msgstr "Pembaruan desktop untuk Linux tersedia. Pilih format paket untuk sistem ini dan {productName} akan menyimpannya secara lokal." - #. Plutonium subscription card line offering a move to a lower price. {listPriceEffectiveDate} is a date already formatted and localized by code; never write a date into the translation. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx msgid "A lower price is available. Switching takes effect on <0>{listPriceEffectiveDate} and changes nothing else." @@ -11427,11 +11432,6 @@ msgstr "Suara masuk dibisukan" msgid "Deafened by moderators" msgstr "Suara masuk dibisukan oleh moderator" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "DEB" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "DEB package" -msgstr "Paket DEB" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label in the settings dialog mobile settings view. #: src/features/app/components/dialogs/components/MobileSettingsView.tsx @@ -12095,21 +12095,11 @@ msgstr "Notifikasi desktop telah diblokir. Kamu dapat mengaktifkannya nanti di p msgid "Desktop startup" msgstr "Mulai di desktop" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update {version} available. Choose a Linux package." -msgstr "Pembaruan desktop {version} tersedia. Pilih paket Linux." - #. Modal title shown when a new desktop app update can be downloaded. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop update available" msgstr "Pembaruan desktop tersedia" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format and no version is known. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update available. Choose a Linux package." -msgstr "Pembaruan desktop tersedia. Pilih paket Linux." - #. Desktop updater modal body. The version placeholder is the target app version; productName is the app name. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop version {version} is available. Download the installer, then run it to update {productName}." @@ -13049,11 +13039,6 @@ msgstr "Unduh penginstal" msgid "Download media" msgstr "Unduh media" -#. Button label that downloads a selected Linux desktop update package. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Download package" -msgstr "Unduh paket" - #. Button that downloads the account recovery kit as a PDF file. #: src/features/auth/components/modals/RecoveryKitModal.tsx msgid "Download PDF" @@ -18280,11 +18265,6 @@ msgstr "Instal dari menu browsermu." msgid "Install the new app" msgstr "Instal aplikasi baru" -#. Label for the installed desktop version in the Linux update modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Installed" -msgstr "Terinstal" - #. Desktop updater modal body. The currentVersion placeholder is the installed app version. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Installed version: {currentVersion}." @@ -19794,11 +19774,6 @@ msgstr "Aplikasi Linux" msgid "Linux default" msgstr "Default Linux" -#. Select label for choosing the Linux desktop package format to download. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Linux package" -msgstr "Paket Linux" - #. Forum layout name shown inside an activity log row. #. Forum layout option that shows posts as rows. #: src/features/forum/utils/ForumMessageDescriptors.ts @@ -22430,11 +22405,6 @@ msgstr "Utas baru tidak akan lagi muncul di daftar saluran setelah periode tidak msgid "New users can't join right now. You can still create an account or sign in, then try this link again later." msgstr "Pengguna baru tidak dapat bergabung sekarang. Kamu masih bisa membuat akun atau masuk, lalu coba tautan ini lagi nanti." -#. Fallback version label when the available desktop version is unknown. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "New version" -msgstr "Versi baru" - #. US state name, shown next to the country name in the mature content gate. #: src/features/user/utils/UserGeo.ts msgctxt "us-state" @@ -24142,6 +24112,8 @@ msgid "Open settings" msgstr "Buka pengaturan" #. Button label that opens the software center via appstream protocol. +#. Native desktop dialog button on a Flatpak install that opens the software center page for the desktop app. +#: src/features/platform/utils/DesktopLocaleBridge.ts #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Open software center" msgstr "Buka pusat perangkat lunak" @@ -28464,11 +28436,6 @@ msgstr "Kotak centang gaya bulat" msgid "Rows per page" msgstr "Baris per halaman" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "RPM" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "RPM package" -msgstr "Paket RPM" - #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts msgid "Run {productName} automatically when your computer starts" @@ -33120,11 +33087,6 @@ msgstr "Ketuk Bagikan, lalu Tambah ke Layar Utama." msgid "Tap to add a note" msgstr "Ketuk untuk menambahkan catatan" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "tar.gz" verbatim and translate only the word "archive". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "tar.gz archive" -msgstr "Arsip tar.gz" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/SharedDescriptors.ts msgid "Taskbar badge" @@ -33513,11 +33475,6 @@ msgstr "Koneksi ditingkatkan ke TLS hanya jika server menawarkannya." msgid "The desktop update could not be downloaded. Try again from the update button when your connection is stable." msgstr "Pembaruan desktop tidak dapat diunduh. Coba lagi dari tombol pembaruan saat koneksimu stabil." -#. Help text below the Linux package format selector in the desktop updater modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "The downloaded file is saved to your computer. Install it with your normal package manager." -msgstr "File yang diunduh disimpan di komputermu. Instal dengan manajer paket yang biasa kamu pakai." - #. Activity log detail line under a deleted emoji, shown only when someone other than the person who deleted it had uploaded it. {user} is the user who uploaded the emoji, shown as a clickable user chip. #: src/features/guild/utils/guild_tabs/audit_log/AuditLogExpressionMessageMessages.ts msgid "The emoji was uploaded by {user}" @@ -36548,11 +36505,21 @@ msgstr "Hingga 4K pada 60 FPS" msgid "Up to 720p at 30 FPS" msgstr "Hingga 720p pada 30 FPS" +#. Native desktop dialog body under the headline announcing a new version, shown on a Flatpak install. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} from your software center." +msgstr "Perbarui {appName} dari pusat perangkat lunak Anda." + #. Tray menu item shown only while a desktop update is waiting. Clicking it installs the update. {appName} is the desktop app name (typically Fluxer). #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "Update {appName} now" msgstr "Perbarui {appName} sekarang" +#. Native desktop dialog body under the headline announcing a new version, shown on a Linux install managed by a package manager such as a deb, rpm or AUR package. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} with your package manager, or download the new version." +msgstr "Perbarui {appName} dengan manajer paket Anda, atau unduh versi baru." + #. Linux settings text when the installed desktop app is too old for system-wide shortcuts. {productName} is the app name. #: src/features/user/components/modals/tabs/components/SystemShortcutsSection.tsx msgid "Update {productName} to use system-wide shortcuts on Wayland." diff --git a/fluxer_app/src/features/i18n/locales/it/messages.po b/fluxer_app/src/features/i18n/locales/it/messages.po index 6d729bcc2..8312d90af 100644 --- a/fluxer_app/src/features/i18n/locales/it/messages.po +++ b/fluxer_app/src/features/i18n/locales/it/messages.po @@ -1802,6 +1802,16 @@ msgstr "{PRODUCT_NAME} deve riavviarsi affinché la modifica della barra del tit msgid "{PRODUCT_NAME} needs to restart to disable Chromium's hardware acceleration. Use this only if you're troubleshooting graphics glitches or high GPU usage." msgstr "{PRODUCT_NAME} deve riavviarsi per disabilitare l'accelerazione hardware di Chromium. Usalo solo se stai risolvendo problemi grafici o un elevato utilizzo della GPU." +#. Desktop updater modal body for a Flatpak install when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it from your software center." +msgstr "È disponibile {productName} {version}. Aggiornalo dal tuo software center." + +#. Desktop updater modal body for a Linux install managed by a package manager, such as a deb, rpm or AUR package, when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it with your package manager, or download the new version." +msgstr "È disponibile {productName} {version}. Aggiornalo con il tuo gestore di pacchetti o scarica la nuova versione." + #. Accessible label for the Fluxer application icon. #: src/features/ui/components/icons/FluxerIconMark.tsx msgid "{productName} application icon" @@ -2669,11 +2679,6 @@ msgstr "Un bot utile che fa cose straordinarie!" msgid "a hostname, e.g. {exampleDomain}" msgstr "un nome host, ad es. {exampleDomain}" -#. Linux desktop updater modal body shown before version details and package format selector. productName is the app name. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "A Linux desktop update is available. Choose the package format for this system and {productName} will save it locally." -msgstr "È disponibile un aggiornamento dell’app desktop per Linux. Scegli il formato del pacchetto per questo sistema e {productName} lo salverà localmente." - #. Plutonium subscription card line offering a move to a lower price. {listPriceEffectiveDate} is a date already formatted and localized by code; never write a date into the translation. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx msgid "A lower price is available. Switching takes effect on <0>{listPriceEffectiveDate} and changes nothing else." @@ -11427,11 +11432,6 @@ msgstr "Audio disattivato" msgid "Deafened by moderators" msgstr "Audio disattivato dai moderatori" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "DEB" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "DEB package" -msgstr "Pacchetto DEB" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label in the settings dialog mobile settings view. #: src/features/app/components/dialogs/components/MobileSettingsView.tsx @@ -12095,21 +12095,11 @@ msgstr "Le notifiche desktop sono state bloccate. Puoi riattivarle in seguito ne msgid "Desktop startup" msgstr "Avvio su desktop" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update {version} available. Choose a Linux package." -msgstr "Aggiornamento desktop {version} disponibile. Scegli un pacchetto Linux." - #. Modal title shown when a new desktop app update can be downloaded. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop update available" msgstr "Aggiornamento desktop disponibile" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format and no version is known. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update available. Choose a Linux package." -msgstr "Aggiornamento desktop disponibile. Scegli un pacchetto Linux." - #. Desktop updater modal body. The version placeholder is the target app version; productName is the app name. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop version {version} is available. Download the installer, then run it to update {productName}." @@ -13049,11 +13039,6 @@ msgstr "Scarica programma di installazione" msgid "Download media" msgstr "Scarica media" -#. Button label that downloads a selected Linux desktop update package. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Download package" -msgstr "Scarica pacchetto" - #. Button that downloads the account recovery kit as a PDF file. #: src/features/auth/components/modals/RecoveryKitModal.tsx msgid "Download PDF" @@ -18280,11 +18265,6 @@ msgstr "Installalo dal menu del tuo browser." msgid "Install the new app" msgstr "Installa la nuova app" -#. Label for the installed desktop version in the Linux update modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Installed" -msgstr "Installata" - #. Desktop updater modal body. The currentVersion placeholder is the installed app version. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Installed version: {currentVersion}." @@ -19794,11 +19774,6 @@ msgstr "App Linux" msgid "Linux default" msgstr "Linux (predefinito)" -#. Select label for choosing the Linux desktop package format to download. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Linux package" -msgstr "Pacchetto Linux" - #. Forum layout name shown inside an activity log row. #. Forum layout option that shows posts as rows. #: src/features/forum/utils/ForumMessageDescriptors.ts @@ -22430,11 +22405,6 @@ msgstr "I nuovi thread smetteranno di essere visualizzati nell'elenco dei canali msgid "New users can't join right now. You can still create an account or sign in, then try this link again later." msgstr "I nuovi utenti non possono entrare nella community in questo momento. Puoi comunque creare un account o accedere, poi riprovare questo link più tardi." -#. Fallback version label when the available desktop version is unknown. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "New version" -msgstr "Nuova versione" - #. US state name, shown next to the country name in the mature content gate. #: src/features/user/utils/UserGeo.ts msgctxt "us-state" @@ -24142,6 +24112,8 @@ msgid "Open settings" msgstr "Apri impostazioni" #. Button label that opens the software center via appstream protocol. +#. Native desktop dialog button on a Flatpak install that opens the software center page for the desktop app. +#: src/features/platform/utils/DesktopLocaleBridge.ts #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Open software center" msgstr "Apri il centro software" @@ -28464,11 +28436,6 @@ msgstr "Casella di spunta rotonda" msgid "Rows per page" msgstr "Righe per pagina" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "RPM" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "RPM package" -msgstr "Pacchetto RPM" - #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts msgid "Run {productName} automatically when your computer starts" @@ -33120,11 +33087,6 @@ msgstr "Tocca Condividi, quindi Aggiungi alla schermata Home." msgid "Tap to add a note" msgstr "Tocca per aggiungere una nota" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "tar.gz" verbatim and translate only the word "archive". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "tar.gz archive" -msgstr "Archivio tar.gz" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/SharedDescriptors.ts msgid "Taskbar badge" @@ -33513,11 +33475,6 @@ msgstr "La connessione viene aggiornata a TLS solo se il server lo offre." msgid "The desktop update could not be downloaded. Try again from the update button when your connection is stable." msgstr "Non è stato possibile scaricare l'aggiornamento desktop. Riprova dal pulsante di aggiornamento quando la tua connessione sarà stabile." -#. Help text below the Linux package format selector in the desktop updater modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "The downloaded file is saved to your computer. Install it with your normal package manager." -msgstr "Il file scaricato viene salvato sul tuo computer. Installalo con il tuo normale gestore di pacchetti." - #. Activity log detail line under a deleted emoji, shown only when someone other than the person who deleted it had uploaded it. {user} is the user who uploaded the emoji, shown as a clickable user chip. #: src/features/guild/utils/guild_tabs/audit_log/AuditLogExpressionMessageMessages.ts msgid "The emoji was uploaded by {user}" @@ -36548,11 +36505,21 @@ msgstr "Fino a 4K a 60 FPS" msgid "Up to 720p at 30 FPS" msgstr "Fino a 720p a 30 FPS" +#. Native desktop dialog body under the headline announcing a new version, shown on a Flatpak install. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} from your software center." +msgstr "Aggiorna {appName} dal tuo centro software." + #. Tray menu item shown only while a desktop update is waiting. Clicking it installs the update. {appName} is the desktop app name (typically Fluxer). #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "Update {appName} now" msgstr "Aggiorna {appName} ora" +#. Native desktop dialog body under the headline announcing a new version, shown on a Linux install managed by a package manager such as a deb, rpm or AUR package. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} with your package manager, or download the new version." +msgstr "Aggiorna {appName} con il tuo gestore di pacchetti o scarica la nuova versione." + #. Linux settings text when the installed desktop app is too old for system-wide shortcuts. {productName} is the app name. #: src/features/user/components/modals/tabs/components/SystemShortcutsSection.tsx msgid "Update {productName} to use system-wide shortcuts on Wayland." diff --git a/fluxer_app/src/features/i18n/locales/ja/messages.po b/fluxer_app/src/features/i18n/locales/ja/messages.po index be89b01bd..1cbf2ef85 100644 --- a/fluxer_app/src/features/i18n/locales/ja/messages.po +++ b/fluxer_app/src/features/i18n/locales/ja/messages.po @@ -1802,6 +1802,16 @@ msgstr "タイトルバーの変更を反映するには、{PRODUCT_NAME}を再 msgid "{PRODUCT_NAME} needs to restart to disable Chromium's hardware acceleration. Use this only if you're troubleshooting graphics glitches or high GPU usage." msgstr "Chromiumのハードウェアアクセラレーションを無効にするには、{PRODUCT_NAME}を再起動する必要があります。この設定は、グラフィックの不具合やGPU使用率の高さをトラブルシューティングする場合にのみ使用してください。" +#. Desktop updater modal body for a Flatpak install when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it from your software center." +msgstr "{productName} {version} が利用可能です。ソフトウェアセンターからアップデートしてください。" + +#. Desktop updater modal body for a Linux install managed by a package manager, such as a deb, rpm or AUR package, when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it with your package manager, or download the new version." +msgstr "{productName} {version} が利用可能です。パッケージマネージャーで更新するか、新しいバージョンをダウンロードしてください。" + #. Accessible label for the Fluxer application icon. #: src/features/ui/components/icons/FluxerIconMark.tsx msgid "{productName} application icon" @@ -2669,11 +2679,6 @@ msgstr "便利な機能が使えるボットです!" msgid "a hostname, e.g. {exampleDomain}" msgstr "ホスト名(例: {exampleDomain})" -#. Linux desktop updater modal body shown before version details and package format selector. productName is the app name. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "A Linux desktop update is available. Choose the package format for this system and {productName} will save it locally." -msgstr "Linuxデスクトップ版のアップデートが利用可能です。このシステムで使用するパッケージ形式を選択すると、{productName}がファイルをローカルに保存します。" - #. Plutonium subscription card line offering a move to a lower price. {listPriceEffectiveDate} is a date already formatted and localized by code; never write a date into the translation. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx msgid "A lower price is available. Switching takes effect on <0>{listPriceEffectiveDate} and changes nothing else." @@ -11427,11 +11432,6 @@ msgstr "スピーカーミュート中" msgid "Deafened by moderators" msgstr "モデレーターによりスピーカーミュートされています" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "DEB" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "DEB package" -msgstr "DEBパッケージ" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label in the settings dialog mobile settings view. #: src/features/app/components/dialogs/components/MobileSettingsView.tsx @@ -12095,21 +12095,11 @@ msgstr "デスクトップ通知がブロックされました。ブラウザ設 msgid "Desktop startup" msgstr "デスクトップ起動" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update {version} available. Choose a Linux package." -msgstr "デスクトップ版アップデート {version} が利用可能です。Linuxパッケージを選択してください。" - #. Modal title shown when a new desktop app update can be downloaded. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop update available" msgstr "デスクトップ版のアップデートがあります" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format and no version is known. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update available. Choose a Linux package." -msgstr "デスクトップ版のアップデートがあります。Linuxパッケージを選択してください。" - #. Desktop updater modal body. The version placeholder is the target app version; productName is the app name. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop version {version} is available. Download the installer, then run it to update {productName}." @@ -13049,11 +13039,6 @@ msgstr "インストーラーをダウンロード" msgid "Download media" msgstr "メディアをダウンロード" -#. Button label that downloads a selected Linux desktop update package. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Download package" -msgstr "パッケージをダウンロード" - #. Button that downloads the account recovery kit as a PDF file. #: src/features/auth/components/modals/RecoveryKitModal.tsx msgid "Download PDF" @@ -18280,11 +18265,6 @@ msgstr "ブラウザのメニューからインストールしてください。 msgid "Install the new app" msgstr "新しいアプリをインストール" -#. Label for the installed desktop version in the Linux update modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Installed" -msgstr "インストール済み" - #. Desktop updater modal body. The currentVersion placeholder is the installed app version. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Installed version: {currentVersion}." @@ -19794,11 +19774,6 @@ msgstr "Linuxアプリ" msgid "Linux default" msgstr "Linux デフォルト" -#. Select label for choosing the Linux desktop package format to download. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Linux package" -msgstr "Linuxパッケージ" - #. Forum layout name shown inside an activity log row. #. Forum layout option that shows posts as rows. #: src/features/forum/utils/ForumMessageDescriptors.ts @@ -22430,11 +22405,6 @@ msgstr "この期間アクティブでないスレッドは、チャンネルリ msgid "New users can't join right now. You can still create an account or sign in, then try this link again later." msgstr "現在、新しいユーザーは参加できません。アカウントを作成するかサインインしてから、後でもう一度このリンクをお試しください。" -#. Fallback version label when the available desktop version is unknown. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "New version" -msgstr "新しいバージョン" - #. US state name, shown next to the country name in the mature content gate. #: src/features/user/utils/UserGeo.ts msgctxt "us-state" @@ -24142,6 +24112,8 @@ msgid "Open settings" msgstr "設定を開く" #. Button label that opens the software center via appstream protocol. +#. Native desktop dialog button on a Flatpak install that opens the software center page for the desktop app. +#: src/features/platform/utils/DesktopLocaleBridge.ts #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Open software center" msgstr "ソフトウェアセンターを開く" @@ -28464,11 +28436,6 @@ msgstr "丸型チェックボックス" msgid "Rows per page" msgstr "ページあたりの行数" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "RPM" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "RPM package" -msgstr "RPMパッケージ" - #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts msgid "Run {productName} automatically when your computer starts" @@ -33120,11 +33087,6 @@ msgstr "「共有」をタップし、「ホーム画面に追加」をタップ msgid "Tap to add a note" msgstr "タップしてメモを追加" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "tar.gz" verbatim and translate only the word "archive". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "tar.gz archive" -msgstr "tar.gzアーカイブ" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/SharedDescriptors.ts msgid "Taskbar badge" @@ -33513,11 +33475,6 @@ msgstr "サーバーが提供する場合にのみ、接続はTLSにアップグ msgid "The desktop update could not be downloaded. Try again from the update button when your connection is stable." msgstr "デスクトップのアップデートをダウンロードできませんでした。インターネット接続が安定していることを確認し、アップデートボタンからもう一度お試しください。" -#. Help text below the Linux package format selector in the desktop updater modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "The downloaded file is saved to your computer. Install it with your normal package manager." -msgstr "ダウンロードされたファイルはコンピューターに保存されます。通常のパッケージマネージャーでインストールしてください。" - #. Activity log detail line under a deleted emoji, shown only when someone other than the person who deleted it had uploaded it. {user} is the user who uploaded the emoji, shown as a clickable user chip. #: src/features/guild/utils/guild_tabs/audit_log/AuditLogExpressionMessageMessages.ts msgid "The emoji was uploaded by {user}" @@ -36548,11 +36505,21 @@ msgstr "最大 4K 60 FPS" msgid "Up to 720p at 30 FPS" msgstr "最大720p、30FPS" +#. Native desktop dialog body under the headline announcing a new version, shown on a Flatpak install. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} from your software center." +msgstr "ソフトウェアセンターから{appName}をアップデートしてください。" + #. Tray menu item shown only while a desktop update is waiting. Clicking it installs the update. {appName} is the desktop app name (typically Fluxer). #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "Update {appName} now" msgstr "{appName} を今すぐアップデート" +#. Native desktop dialog body under the headline announcing a new version, shown on a Linux install managed by a package manager such as a deb, rpm or AUR package. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} with your package manager, or download the new version." +msgstr "パッケージマネージャーで{appName}をアップデートするか、新しいバージョンをダウンロードしてください。" + #. Linux settings text when the installed desktop app is too old for system-wide shortcuts. {productName} is the app name. #: src/features/user/components/modals/tabs/components/SystemShortcutsSection.tsx msgid "Update {productName} to use system-wide shortcuts on Wayland." diff --git a/fluxer_app/src/features/i18n/locales/ko/messages.po b/fluxer_app/src/features/i18n/locales/ko/messages.po index 875fc05c7..d32c24e42 100644 --- a/fluxer_app/src/features/i18n/locales/ko/messages.po +++ b/fluxer_app/src/features/i18n/locales/ko/messages.po @@ -1802,6 +1802,16 @@ msgstr "제목 표시줄 변경 사항을 적용하려면 {PRODUCT_NAME}을(를) msgid "{PRODUCT_NAME} needs to restart to disable Chromium's hardware acceleration. Use this only if you're troubleshooting graphics glitches or high GPU usage." msgstr "Chromium의 하드웨어 가속을 비활성화하려면 {PRODUCT_NAME}을(를) 다시 시작해야 합니다. 그래픽 오류나 GPU 사용량이 높은 문제를 해결할 때만 사용하세요." +#. Desktop updater modal body for a Flatpak install when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it from your software center." +msgstr "{productName} {version} 업데이트를 사용할 수 있습니다. 소프트웨어 센터에서 업데이트하세요." + +#. Desktop updater modal body for a Linux install managed by a package manager, such as a deb, rpm or AUR package, when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it with your package manager, or download the new version." +msgstr "{productName} {version} 버전을 사용할 수 있습니다. 패키지 관리자를 사용하여 업데이트하거나 새 버전을 다운로드하세요." + #. Accessible label for the Fluxer application icon. #: src/features/ui/components/icons/FluxerIconMark.tsx msgid "{productName} application icon" @@ -2669,11 +2679,6 @@ msgstr "놀라운 기능을 수행하는 유용한 봇입니다!" msgid "a hostname, e.g. {exampleDomain}" msgstr "호스트 이름(예: {exampleDomain})" -#. Linux desktop updater modal body shown before version details and package format selector. productName is the app name. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "A Linux desktop update is available. Choose the package format for this system and {productName} will save it locally." -msgstr "Linux 데스크톱 업데이트가 있습니다. 이 시스템에 맞는 패키지 형식을 선택하면 {productName}이 파일을 로컬에 저장합니다." - #. Plutonium subscription card line offering a move to a lower price. {listPriceEffectiveDate} is a date already formatted and localized by code; never write a date into the translation. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx msgid "A lower price is available. Switching takes effect on <0>{listPriceEffectiveDate} and changes nothing else." @@ -11427,11 +11432,6 @@ msgstr "소리 꺼짐" msgid "Deafened by moderators" msgstr "모더레이터에 의해 소리 꺼짐" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "DEB" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "DEB package" -msgstr "DEB 패키지" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label in the settings dialog mobile settings view. #: src/features/app/components/dialogs/components/MobileSettingsView.tsx @@ -12095,21 +12095,11 @@ msgstr "데스크톱 알림이 차단되었습니다. 나중에 브라우저 설 msgid "Desktop startup" msgstr "데스크톱 시작" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update {version} available. Choose a Linux package." -msgstr "데스크톱 업데이트 {version}을(를) 사용할 수 있습니다. Linux 패키지를 선택하세요." - #. Modal title shown when a new desktop app update can be downloaded. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop update available" msgstr "데스크톱 업데이트 사용 가능" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format and no version is known. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update available. Choose a Linux package." -msgstr "데스크톱 업데이트를 사용할 수 있습니다. Linux 패키지를 선택하세요." - #. Desktop updater modal body. The version placeholder is the target app version; productName is the app name. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop version {version} is available. Download the installer, then run it to update {productName}." @@ -13049,11 +13039,6 @@ msgstr "설치 프로그램 다운로드" msgid "Download media" msgstr "미디어 다운로드" -#. Button label that downloads a selected Linux desktop update package. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Download package" -msgstr "패키지 다운로드" - #. Button that downloads the account recovery kit as a PDF file. #: src/features/auth/components/modals/RecoveryKitModal.tsx msgid "Download PDF" @@ -18280,11 +18265,6 @@ msgstr "브라우저 메뉴에서 설치하세요." msgid "Install the new app" msgstr "새 앱 설치" -#. Label for the installed desktop version in the Linux update modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Installed" -msgstr "설치됨" - #. Desktop updater modal body. The currentVersion placeholder is the installed app version. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Installed version: {currentVersion}." @@ -19794,11 +19774,6 @@ msgstr "Linux 앱" msgid "Linux default" msgstr "Linux 기본값" -#. Select label for choosing the Linux desktop package format to download. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Linux package" -msgstr "Linux 패키지" - #. Forum layout name shown inside an activity log row. #. Forum layout option that shows posts as rows. #: src/features/forum/utils/ForumMessageDescriptors.ts @@ -22430,11 +22405,6 @@ msgstr "새로운 스레드는 이 기간 동안 활동이 없으면 채널 목 msgid "New users can't join right now. You can still create an account or sign in, then try this link again later." msgstr "지금은 새 사용자가 참여할 수 없습니다. 계정을 만들거나 로그인한 다음 나중에 이 링크를 다시 시도할 수 있습니다." -#. Fallback version label when the available desktop version is unknown. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "New version" -msgstr "새 버전" - #. US state name, shown next to the country name in the mature content gate. #: src/features/user/utils/UserGeo.ts msgctxt "us-state" @@ -24142,6 +24112,8 @@ msgid "Open settings" msgstr "설정 열기" #. Button label that opens the software center via appstream protocol. +#. Native desktop dialog button on a Flatpak install that opens the software center page for the desktop app. +#: src/features/platform/utils/DesktopLocaleBridge.ts #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Open software center" msgstr "소프트웨어 센터 열기" @@ -28464,11 +28436,6 @@ msgstr "원형 체크박스" msgid "Rows per page" msgstr "페이지당 행 수" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "RPM" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "RPM package" -msgstr "RPM 패키지" - #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts msgid "Run {productName} automatically when your computer starts" @@ -33120,11 +33087,6 @@ msgstr "공유를 탭한 다음 홈 화면에 추가를 탭하세요." msgid "Tap to add a note" msgstr "메모를 추가하려면 탭하세요" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "tar.gz" verbatim and translate only the word "archive". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "tar.gz archive" -msgstr "tar.gz 아카이브" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/SharedDescriptors.ts msgid "Taskbar badge" @@ -33513,11 +33475,6 @@ msgstr "서버에서 제공하는 경우에만 연결이 TLS로 업그레이드 msgid "The desktop update could not be downloaded. Try again from the update button when your connection is stable." msgstr "데스크톱 업데이트를 다운로드할 수 없습니다. 연결이 안정적일 때 업데이트 버튼을 눌러 다시 시도해 주세요." -#. Help text below the Linux package format selector in the desktop updater modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "The downloaded file is saved to your computer. Install it with your normal package manager." -msgstr "다운로드한 파일은 컴퓨터에 저장됩니다. 평소 사용하는 패키지 관리자로 설치하세요." - #. Activity log detail line under a deleted emoji, shown only when someone other than the person who deleted it had uploaded it. {user} is the user who uploaded the emoji, shown as a clickable user chip. #: src/features/guild/utils/guild_tabs/audit_log/AuditLogExpressionMessageMessages.ts msgid "The emoji was uploaded by {user}" @@ -36548,11 +36505,21 @@ msgstr "최대 4K 60 FPS" msgid "Up to 720p at 30 FPS" msgstr "최대 720p, 30 FPS" +#. Native desktop dialog body under the headline announcing a new version, shown on a Flatpak install. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} from your software center." +msgstr "소프트웨어 센터에서 {appName}을(를) 업데이트하세요." + #. Tray menu item shown only while a desktop update is waiting. Clicking it installs the update. {appName} is the desktop app name (typically Fluxer). #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "Update {appName} now" msgstr "{appName} 업데이트" +#. Native desktop dialog body under the headline announcing a new version, shown on a Linux install managed by a package manager such as a deb, rpm or AUR package. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} with your package manager, or download the new version." +msgstr "패키지 관리자를 사용하여 {appName}을(를) 업데이트하거나 새 버전을 다운로드하세요." + #. Linux settings text when the installed desktop app is too old for system-wide shortcuts. {productName} is the app name. #: src/features/user/components/modals/tabs/components/SystemShortcutsSection.tsx msgid "Update {productName} to use system-wide shortcuts on Wayland." diff --git a/fluxer_app/src/features/i18n/locales/lt/messages.po b/fluxer_app/src/features/i18n/locales/lt/messages.po index d1e0072a8..c4ae72d0b 100644 --- a/fluxer_app/src/features/i18n/locales/lt/messages.po +++ b/fluxer_app/src/features/i18n/locales/lt/messages.po @@ -1802,6 +1802,16 @@ msgstr "Kad pavadinimo juostos pakeitimas įsigaliotų, {PRODUCT_NAME} reikia pa msgid "{PRODUCT_NAME} needs to restart to disable Chromium's hardware acceleration. Use this only if you're troubleshooting graphics glitches or high GPU usage." msgstr "{PRODUCT_NAME} reikia paleisti iš naujo, kad būtų išjungtas „Chromium“ aparatinės įrangos spartinimas. Naudokite tai tik tada, kai sprendžiate grafikos trikdžių arba didelio GPU naudojimo problemas." +#. Desktop updater modal body for a Flatpak install when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it from your software center." +msgstr "Galimas naujinimas {productName} {version}. Atnaujinkite jį savo programėlių centre." + +#. Desktop updater modal body for a Linux install managed by a package manager, such as a deb, rpm or AUR package, when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it with your package manager, or download the new version." +msgstr "Galimas naujinimas {productName} {version}. Atnaujinkite naudodami paketų tvarkyklę arba atsisiųskite naują versiją." + #. Accessible label for the Fluxer application icon. #: src/features/ui/components/icons/FluxerIconMark.tsx msgid "{productName} application icon" @@ -2669,11 +2679,6 @@ msgstr "Naudingas botas, kuris daro nuostabius dalykus!" msgid "a hostname, e.g. {exampleDomain}" msgstr "pagrindinio kompiuterio pavadinimas, pvz., {exampleDomain}" -#. Linux desktop updater modal body shown before version details and package format selector. productName is the app name. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "A Linux desktop update is available. Choose the package format for this system and {productName} will save it locally." -msgstr "Prieinamas „Linux“ darbalaukio atnaujinimas. Pasirinkite šios sistemos paketų formatą, o programa „{productName}“ išsaugos jį jūsų įrenginyje." - #. Plutonium subscription card line offering a move to a lower price. {listPriceEffectiveDate} is a date already formatted and localized by code; never write a date into the translation. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx msgid "A lower price is available. Switching takes effect on <0>{listPriceEffectiveDate} and changes nothing else." @@ -11427,11 +11432,6 @@ msgstr "Garsas išjungtas" msgid "Deafened by moderators" msgstr "Garsą išjungė moderatoriai" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "DEB" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "DEB package" -msgstr "DEB paketas" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label in the settings dialog mobile settings view. #: src/features/app/components/dialogs/components/MobileSettingsView.tsx @@ -12095,21 +12095,11 @@ msgstr "Darbalaukio pranešimai buvo užblokuoti. Juos vėliau galite įjungti n msgid "Desktop startup" msgstr "Darbalaukio paleidimas" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update {version} available. Choose a Linux package." -msgstr "Galimas darbalaukio naujinimas {version}. Pasirinkite „Linux“ paketą." - #. Modal title shown when a new desktop app update can be downloaded. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop update available" msgstr "Galimas darbalaukio naujinimas" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format and no version is known. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update available. Choose a Linux package." -msgstr "Galimas darbalaukio naujinimas. Pasirinkite „Linux“ paketą." - #. Desktop updater modal body. The version placeholder is the target app version; productName is the app name. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop version {version} is available. Download the installer, then run it to update {productName}." @@ -13049,11 +13039,6 @@ msgstr "Atsisiųsti diegimo programą" msgid "Download media" msgstr "Atsisiųsti mediją" -#. Button label that downloads a selected Linux desktop update package. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Download package" -msgstr "Atsisiųsti paketą" - #. Button that downloads the account recovery kit as a PDF file. #: src/features/auth/components/modals/RecoveryKitModal.tsx msgid "Download PDF" @@ -18281,11 +18266,6 @@ msgstr "Įdiekite jį iš savo naršyklės meniu." msgid "Install the new app" msgstr "Įdiegti naująją programėlę" -#. Label for the installed desktop version in the Linux update modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Installed" -msgstr "Įdiegta" - #. Desktop updater modal body. The currentVersion placeholder is the installed app version. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Installed version: {currentVersion}." @@ -19795,11 +19775,6 @@ msgstr "„Linux“ programa" msgid "Linux default" msgstr "„Linux“ numatytasis įrenginys" -#. Select label for choosing the Linux desktop package format to download. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Linux package" -msgstr "„Linux“ paketas" - #. Forum layout name shown inside an activity log row. #. Forum layout option that shows posts as rows. #: src/features/forum/utils/ForumMessageDescriptors.ts @@ -22431,11 +22406,6 @@ msgstr "Naujos temos nustos rodytis kanalo sąraše po šio neaktyvumo laikotarp msgid "New users can't join right now. You can still create an account or sign in, then try this link again later." msgstr "Nauji naudotojai šiuo metu negali prisijungti. Vis tiek galite susikurti paskyrą arba prisijungti, o vėliau vėl pabandyti pasinaudoti šia nuoroda." -#. Fallback version label when the available desktop version is unknown. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "New version" -msgstr "Nauja versija" - #. US state name, shown next to the country name in the mature content gate. #: src/features/user/utils/UserGeo.ts msgctxt "us-state" @@ -24143,6 +24113,8 @@ msgid "Open settings" msgstr "Atidaryti nustatymus" #. Button label that opens the software center via appstream protocol. +#. Native desktop dialog button on a Flatpak install that opens the software center page for the desktop app. +#: src/features/platform/utils/DesktopLocaleBridge.ts #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Open software center" msgstr "Atidaryti programų centrą" @@ -28465,11 +28437,6 @@ msgstr "Apvalaus stiliaus žymimasis langelis" msgid "Rows per page" msgstr "Eilutės puslapyje" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "RPM" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "RPM package" -msgstr "RPM paketas" - #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts msgid "Run {productName} automatically when your computer starts" @@ -33121,11 +33088,6 @@ msgstr "Palieskite Bendrinti, tada – Pridėti prie pradžios ekrano." msgid "Tap to add a note" msgstr "Palieskite, kad pridėtumėte pastabą" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "tar.gz" verbatim and translate only the word "archive". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "tar.gz archive" -msgstr "tar.gz archyvas" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/SharedDescriptors.ts msgid "Taskbar badge" @@ -33514,11 +33476,6 @@ msgstr "Ryšys atnaujinamas į TLS tik tuo atveju, jei jį siūlo serveris." msgid "The desktop update could not be downloaded. Try again from the update button when your connection is stable." msgstr "Nepavyko atsisiųsti darbalaukio naujinimo. Bandykite dar kartą paspaudę naujinimo mygtuką, kai ryšys bus stabilus." -#. Help text below the Linux package format selector in the desktop updater modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "The downloaded file is saved to your computer. Install it with your normal package manager." -msgstr "Atsisiųstas failas išsaugotas jūsų kompiuteryje. Įdiekite jį naudodami įprastą paketų tvarkyklę." - #. Activity log detail line under a deleted emoji, shown only when someone other than the person who deleted it had uploaded it. {user} is the user who uploaded the emoji, shown as a clickable user chip. #: src/features/guild/utils/guild_tabs/audit_log/AuditLogExpressionMessageMessages.ts msgid "The emoji was uploaded by {user}" @@ -36549,11 +36506,21 @@ msgstr "Iki 4K esant 60 FPS" msgid "Up to 720p at 30 FPS" msgstr "Iki 720p esant 30 kadrų per sekundę" +#. Native desktop dialog body under the headline announcing a new version, shown on a Flatpak install. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} from your software center." +msgstr "Atnaujinkite {appName} savo programinės įrangos centre." + #. Tray menu item shown only while a desktop update is waiting. Clicking it installs the update. {appName} is the desktop app name (typically Fluxer). #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "Update {appName} now" msgstr "Atnaujinti {appName} dabar" +#. Native desktop dialog body under the headline announcing a new version, shown on a Linux install managed by a package manager such as a deb, rpm or AUR package. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} with your package manager, or download the new version." +msgstr "Atnaujinkite {appName} naudodami savo paketų tvarkytuvę arba atsisiųskite naują versiją." + #. Linux settings text when the installed desktop app is too old for system-wide shortcuts. {productName} is the app name. #: src/features/user/components/modals/tabs/components/SystemShortcutsSection.tsx msgid "Update {productName} to use system-wide shortcuts on Wayland." diff --git a/fluxer_app/src/features/i18n/locales/nl/messages.po b/fluxer_app/src/features/i18n/locales/nl/messages.po index f36fff04f..a2444f3de 100644 --- a/fluxer_app/src/features/i18n/locales/nl/messages.po +++ b/fluxer_app/src/features/i18n/locales/nl/messages.po @@ -1802,6 +1802,16 @@ msgstr "{PRODUCT_NAME} moet opnieuw worden opgestart om de wijziging van de tite msgid "{PRODUCT_NAME} needs to restart to disable Chromium's hardware acceleration. Use this only if you're troubleshooting graphics glitches or high GPU usage." msgstr "{PRODUCT_NAME} moet opnieuw worden opgestart om de hardwareversnelling van Chromium uit te schakelen. Gebruik dit alleen als je problemen met de grafische weergave of een hoog GPU-gebruik oplost." +#. Desktop updater modal body for a Flatpak install when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it from your software center." +msgstr "{productName} {version} is beschikbaar. Werk het bij vanuit je softwarecentrum." + +#. Desktop updater modal body for a Linux install managed by a package manager, such as a deb, rpm or AUR package, when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it with your package manager, or download the new version." +msgstr "{productName} {version} is beschikbaar. Werk het bij met je pakketbeheerder of download de nieuwe versie." + #. Accessible label for the Fluxer application icon. #: src/features/ui/components/icons/FluxerIconMark.tsx msgid "{productName} application icon" @@ -2669,11 +2679,6 @@ msgstr "Een behulpzame bot die geweldige dingen doet!" msgid "a hostname, e.g. {exampleDomain}" msgstr "een hostnaam, bijv. {exampleDomain}" -#. Linux desktop updater modal body shown before version details and package format selector. productName is the app name. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "A Linux desktop update is available. Choose the package format for this system and {productName} will save it locally." -msgstr "Er is een Linux-desktopupdate beschikbaar. Kies het pakketformaat voor dit systeem, en {productName} zal het lokaal opslaan." - #. Plutonium subscription card line offering a move to a lower price. {listPriceEffectiveDate} is a date already formatted and localized by code; never write a date into the translation. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx msgid "A lower price is available. Switching takes effect on <0>{listPriceEffectiveDate} and changes nothing else." @@ -11427,11 +11432,6 @@ msgstr "Geluid uitgeschakeld" msgid "Deafened by moderators" msgstr "Geluid uitgeschakeld door moderators" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "DEB" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "DEB package" -msgstr "DEB-pakket" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label in the settings dialog mobile settings view. #: src/features/app/components/dialogs/components/MobileSettingsView.tsx @@ -12095,21 +12095,11 @@ msgstr "Desktopmeldingen zijn geblokkeerd. Je kunt ze later inschakelen via je b msgid "Desktop startup" msgstr "Desktopapp opstarten" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update {version} available. Choose a Linux package." -msgstr "Desktopupdate {version} beschikbaar. Kies een Linux-pakket." - #. Modal title shown when a new desktop app update can be downloaded. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop update available" msgstr "Desktopupdate beschikbaar" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format and no version is known. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update available. Choose a Linux package." -msgstr "Desktopupdate beschikbaar. Kies een Linux-pakket." - #. Desktop updater modal body. The version placeholder is the target app version; productName is the app name. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop version {version} is available. Download the installer, then run it to update {productName}." @@ -13049,11 +13039,6 @@ msgstr "Installatieprogramma downloaden" msgid "Download media" msgstr "Media downloaden" -#. Button label that downloads a selected Linux desktop update package. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Download package" -msgstr "Pakket downloaden" - #. Button that downloads the account recovery kit as a PDF file. #: src/features/auth/components/modals/RecoveryKitModal.tsx msgid "Download PDF" @@ -18280,11 +18265,6 @@ msgstr "Installeer het vanuit je browsermenu." msgid "Install the new app" msgstr "Nieuwe app installeren" -#. Label for the installed desktop version in the Linux update modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Installed" -msgstr "Geïnstalleerd" - #. Desktop updater modal body. The currentVersion placeholder is the installed app version. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Installed version: {currentVersion}." @@ -19794,11 +19774,6 @@ msgstr "Linux-app" msgid "Linux default" msgstr "Standaard van Linux" -#. Select label for choosing the Linux desktop package format to download. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Linux package" -msgstr "Linux-pakket" - #. Forum layout name shown inside an activity log row. #. Forum layout option that shows posts as rows. #: src/features/forum/utils/ForumMessageDescriptors.ts @@ -22430,11 +22405,6 @@ msgstr "Nieuwe threads verdwijnen na deze periode van inactiviteit uit de kanaal msgid "New users can't join right now. You can still create an account or sign in, then try this link again later." msgstr "Nieuwe gebruikers kunnen nu niet deelnemen. Je kunt nog steeds een account aanmaken of inloggen en deze link later opnieuw proberen." -#. Fallback version label when the available desktop version is unknown. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "New version" -msgstr "Nieuwe versie" - #. US state name, shown next to the country name in the mature content gate. #: src/features/user/utils/UserGeo.ts msgctxt "us-state" @@ -24142,6 +24112,8 @@ msgid "Open settings" msgstr "Instellingen openen" #. Button label that opens the software center via appstream protocol. +#. Native desktop dialog button on a Flatpak install that opens the software center page for the desktop app. +#: src/features/platform/utils/DesktopLocaleBridge.ts #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Open software center" msgstr "Open softwarecentrum" @@ -28464,11 +28436,6 @@ msgstr "Rond selectievakje" msgid "Rows per page" msgstr "Rijen per pagina" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "RPM" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "RPM package" -msgstr "RPM-pakket" - #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts msgid "Run {productName} automatically when your computer starts" @@ -33120,11 +33087,6 @@ msgstr "Tik op Deel en vervolgens op Zet op beginscherm." msgid "Tap to add a note" msgstr "Tik om een notitie toe te voegen" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "tar.gz" verbatim and translate only the word "archive". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "tar.gz archive" -msgstr "tar.gz-archief" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/SharedDescriptors.ts msgid "Taskbar badge" @@ -33513,11 +33475,6 @@ msgstr "De verbinding wordt alleen opgewaardeerd naar TLS als de server dit aanb msgid "The desktop update could not be downloaded. Try again from the update button when your connection is stable." msgstr "De desktopupdate kon niet worden gedownload. Probeer het opnieuw via de updateknop wanneer je verbinding stabiel is." -#. Help text below the Linux package format selector in the desktop updater modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "The downloaded file is saved to your computer. Install it with your normal package manager." -msgstr "Het gedownloade bestand is opgeslagen op je computer. Installeer het met je normale pakketbeheerder." - #. Activity log detail line under a deleted emoji, shown only when someone other than the person who deleted it had uploaded it. {user} is the user who uploaded the emoji, shown as a clickable user chip. #: src/features/guild/utils/guild_tabs/audit_log/AuditLogExpressionMessageMessages.ts msgid "The emoji was uploaded by {user}" @@ -36548,11 +36505,21 @@ msgstr "Tot 4K bij 60 FPS" msgid "Up to 720p at 30 FPS" msgstr "Tot 720p op 30 FPS" +#. Native desktop dialog body under the headline announcing a new version, shown on a Flatpak install. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} from your software center." +msgstr "Update {appName} vanuit je softwarecentrum." + #. Tray menu item shown only while a desktop update is waiting. Clicking it installs the update. {appName} is the desktop app name (typically Fluxer). #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "Update {appName} now" msgstr "Update {appName} nu" +#. Native desktop dialog body under the headline announcing a new version, shown on a Linux install managed by a package manager such as a deb, rpm or AUR package. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} with your package manager, or download the new version." +msgstr "Update {appName} met je package manager, of download de nieuwe versie." + #. Linux settings text when the installed desktop app is too old for system-wide shortcuts. {productName} is the app name. #: src/features/user/components/modals/tabs/components/SystemShortcutsSection.tsx msgid "Update {productName} to use system-wide shortcuts on Wayland." diff --git a/fluxer_app/src/features/i18n/locales/no/messages.po b/fluxer_app/src/features/i18n/locales/no/messages.po index 6cd85dad7..f17bcf0dc 100644 --- a/fluxer_app/src/features/i18n/locales/no/messages.po +++ b/fluxer_app/src/features/i18n/locales/no/messages.po @@ -1802,6 +1802,16 @@ msgstr "{PRODUCT_NAME} må starte på nytt for at endringen i tittellinjen skal msgid "{PRODUCT_NAME} needs to restart to disable Chromium's hardware acceleration. Use this only if you're troubleshooting graphics glitches or high GPU usage." msgstr "{PRODUCT_NAME} må starte på nytt for å deaktivere Chromiums maskinvareakselerasjon. Bruk dette kun hvis du feilsøker grafikkfeil eller høy GPU-bruk." +#. Desktop updater modal body for a Flatpak install when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it from your software center." +msgstr "{productName} {version} er tilgjengelig. Oppdater den fra programvaresenteret ditt." + +#. Desktop updater modal body for a Linux install managed by a package manager, such as a deb, rpm or AUR package, when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it with your package manager, or download the new version." +msgstr "{productName} {version} er tilgjengelig. Oppdater den med pakkebehandleren din, eller last ned den nye versjonen." + #. Accessible label for the Fluxer application icon. #: src/features/ui/components/icons/FluxerIconMark.tsx msgid "{productName} application icon" @@ -2669,11 +2679,6 @@ msgstr "En hjelpsom bot som gjør fantastiske ting!" msgid "a hostname, e.g. {exampleDomain}" msgstr "et vertsnavn, f.eks. {exampleDomain}" -#. Linux desktop updater modal body shown before version details and package format selector. productName is the app name. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "A Linux desktop update is available. Choose the package format for this system and {productName} will save it locally." -msgstr "En oppdatering av skrivebordsappen for Linux er tilgjengelig. Velg pakkeformatet for dette systemet, så lagrer {productName} det lokalt." - #. Plutonium subscription card line offering a move to a lower price. {listPriceEffectiveDate} is a date already formatted and localized by code; never write a date into the translation. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx msgid "A lower price is available. Switching takes effect on <0>{listPriceEffectiveDate} and changes nothing else." @@ -11427,11 +11432,6 @@ msgstr "Lyden er slått av" msgid "Deafened by moderators" msgstr "Moderatorene har slått av lyden" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "DEB" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "DEB package" -msgstr "DEB-pakke" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label in the settings dialog mobile settings view. #: src/features/app/components/dialogs/components/MobileSettingsView.tsx @@ -12095,21 +12095,11 @@ msgstr "Skrivebordsvarsler er blokkert. Du kan aktivere dem senere i nettleserin msgid "Desktop startup" msgstr "Oppstart på datamaskin" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update {version} available. Choose a Linux package." -msgstr "Oppdatering {version} til skrivebordsappen er tilgjengelig. Velg en Linux-pakke." - #. Modal title shown when a new desktop app update can be downloaded. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop update available" msgstr "Oppdatering til skrivebordsappen er tilgjengelig" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format and no version is known. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update available. Choose a Linux package." -msgstr "En oppdatering til skrivebordsappen er tilgjengelig. Velg en Linux-pakke." - #. Desktop updater modal body. The version placeholder is the target app version; productName is the app name. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop version {version} is available. Download the installer, then run it to update {productName}." @@ -13049,11 +13039,6 @@ msgstr "Last ned installasjonsprogram" msgid "Download media" msgstr "Last ned medier" -#. Button label that downloads a selected Linux desktop update package. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Download package" -msgstr "Last ned pakke" - #. Button that downloads the account recovery kit as a PDF file. #: src/features/auth/components/modals/RecoveryKitModal.tsx msgid "Download PDF" @@ -18280,11 +18265,6 @@ msgstr "Installer det fra nettlesermenyen din." msgid "Install the new app" msgstr "Installer den nye appen" -#. Label for the installed desktop version in the Linux update modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Installed" -msgstr "Installert" - #. Desktop updater modal body. The currentVersion placeholder is the installed app version. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Installed version: {currentVersion}." @@ -19794,11 +19774,6 @@ msgstr "Linux-app" msgid "Linux default" msgstr "Linux – standard" -#. Select label for choosing the Linux desktop package format to download. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Linux package" -msgstr "Linux-pakke" - #. Forum layout name shown inside an activity log row. #. Forum layout option that shows posts as rows. #: src/features/forum/utils/ForumMessageDescriptors.ts @@ -22430,11 +22405,6 @@ msgstr "Nye tråder slutter å vises i kanallisten etter denne perioden med inak msgid "New users can't join right now. You can still create an account or sign in, then try this link again later." msgstr "Nye brukere kan ikke bli med akkurat nå. Du kan fortsatt opprette en konto eller logge på, og deretter prøve denne lenken igjen senere." -#. Fallback version label when the available desktop version is unknown. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "New version" -msgstr "Ny versjon" - #. US state name, shown next to the country name in the mature content gate. #: src/features/user/utils/UserGeo.ts msgctxt "us-state" @@ -24142,6 +24112,8 @@ msgid "Open settings" msgstr "Åpne innstillinger" #. Button label that opens the software center via appstream protocol. +#. Native desktop dialog button on a Flatpak install that opens the software center page for the desktop app. +#: src/features/platform/utils/DesktopLocaleBridge.ts #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Open software center" msgstr "Åpne programvaresenteret" @@ -28464,11 +28436,6 @@ msgstr "Rund avkrysningsboks" msgid "Rows per page" msgstr "Rader per side" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "RPM" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "RPM package" -msgstr "RPM-pakke" - #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts msgid "Run {productName} automatically when your computer starts" @@ -33120,11 +33087,6 @@ msgstr "Trykk Del, deretter Legg til på Hjem-skjerm." msgid "Tap to add a note" msgstr "Trykk for å legge til et notat" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "tar.gz" verbatim and translate only the word "archive". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "tar.gz archive" -msgstr "tar.gz-arkiv" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/SharedDescriptors.ts msgid "Taskbar badge" @@ -33513,11 +33475,6 @@ msgstr "Tilkoblingen oppgraderes til TLS kun hvis serveren tilbyr det." msgid "The desktop update could not be downloaded. Try again from the update button when your connection is stable." msgstr "Skrivebordsoppdateringen kunne ikke lastes ned. Prøv igjen fra oppdateringsknappen når tilkoblingen din er stabil." -#. Help text below the Linux package format selector in the desktop updater modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "The downloaded file is saved to your computer. Install it with your normal package manager." -msgstr "Den nedlastede filen er lagret på datamaskinen din. Installer den med din vanlige pakkebehandler." - #. Activity log detail line under a deleted emoji, shown only when someone other than the person who deleted it had uploaded it. {user} is the user who uploaded the emoji, shown as a clickable user chip. #: src/features/guild/utils/guild_tabs/audit_log/AuditLogExpressionMessageMessages.ts msgid "The emoji was uploaded by {user}" @@ -36548,11 +36505,21 @@ msgstr "Opptil 4K ved 60 FPS" msgid "Up to 720p at 30 FPS" msgstr "Opptil 720p med 30 FPS" +#. Native desktop dialog body under the headline announcing a new version, shown on a Flatpak install. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} from your software center." +msgstr "Oppdater {appName} fra programvaresenteret ditt." + #. Tray menu item shown only while a desktop update is waiting. Clicking it installs the update. {appName} is the desktop app name (typically Fluxer). #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "Update {appName} now" msgstr "Oppdater {appName} nå" +#. Native desktop dialog body under the headline announcing a new version, shown on a Linux install managed by a package manager such as a deb, rpm or AUR package. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} with your package manager, or download the new version." +msgstr "Oppdater {appName} med pakkebehandleren din, eller last ned den nye versjonen." + #. Linux settings text when the installed desktop app is too old for system-wide shortcuts. {productName} is the app name. #: src/features/user/components/modals/tabs/components/SystemShortcutsSection.tsx msgid "Update {productName} to use system-wide shortcuts on Wayland." diff --git a/fluxer_app/src/features/i18n/locales/pl/messages.po b/fluxer_app/src/features/i18n/locales/pl/messages.po index 825ead8e7..f137cf116 100644 --- a/fluxer_app/src/features/i18n/locales/pl/messages.po +++ b/fluxer_app/src/features/i18n/locales/pl/messages.po @@ -1802,6 +1802,16 @@ msgstr "Uruchom ponownie aplikację {PRODUCT_NAME}, aby zastosować zmianę pask msgid "{PRODUCT_NAME} needs to restart to disable Chromium's hardware acceleration. Use this only if you're troubleshooting graphics glitches or high GPU usage." msgstr "Uruchom ponownie aplikację {PRODUCT_NAME}, aby wyłączyć przyspieszenie sprzętowe Chromium. Używaj tej opcji tylko przy rozwiązywaniu problemów z grafiką lub wysokim obciążeniem GPU." +#. Desktop updater modal body for a Flatpak install when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it from your software center." +msgstr "{productName} w wersji {version} jest dostępna. Zaktualizuj ją z centrum oprogramowania." + +#. Desktop updater modal body for a Linux install managed by a package manager, such as a deb, rpm or AUR package, when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it with your package manager, or download the new version." +msgstr "Dostępna jest nowa wersja {productName} {version}. Zaktualizuj ją za pomocą menedżera pakietów lub pobierz nową wersję." + #. Accessible label for the Fluxer application icon. #: src/features/ui/components/icons/FluxerIconMark.tsx msgid "{productName} application icon" @@ -2669,11 +2679,6 @@ msgstr "Pomocny bot, który potrafi zdziałać cuda!" msgid "a hostname, e.g. {exampleDomain}" msgstr "nazwa hosta, np. {exampleDomain}" -#. Linux desktop updater modal body shown before version details and package format selector. productName is the app name. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "A Linux desktop update is available. Choose the package format for this system and {productName} will save it locally." -msgstr "Dostępna jest aktualizacja aplikacji komputerowej dla systemu Linux. Wybierz format pakietu dla tego systemu, a {productName} zapisze go lokalnie." - #. Plutonium subscription card line offering a move to a lower price. {listPriceEffectiveDate} is a date already formatted and localized by code; never write a date into the translation. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx msgid "A lower price is available. Switching takes effect on <0>{listPriceEffectiveDate} and changes nothing else." @@ -11427,11 +11432,6 @@ msgstr "Dźwięk wyłączony" msgid "Deafened by moderators" msgstr "Dźwięk wyłączony przez moderatorów" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "DEB" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "DEB package" -msgstr "Pakiet DEB" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label in the settings dialog mobile settings view. #: src/features/app/components/dialogs/components/MobileSettingsView.tsx @@ -12095,21 +12095,11 @@ msgstr "Powiadomienia na komputerze zostały zablokowane. Możesz je włączyć msgid "Desktop startup" msgstr "Uruchamianie z systemem" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update {version} available. Choose a Linux package." -msgstr "Dostępna aktualizacja aplikacji komputerowej {version}. Wybierz pakiet dla systemu Linux." - #. Modal title shown when a new desktop app update can be downloaded. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop update available" msgstr "Dostępna aktualizacja aplikacji komputerowej" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format and no version is known. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update available. Choose a Linux package." -msgstr "Dostępna aktualizacja aplikacji. Wybierz pakiet dla systemu Linux." - #. Desktop updater modal body. The version placeholder is the target app version; productName is the app name. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop version {version} is available. Download the installer, then run it to update {productName}." @@ -13049,11 +13039,6 @@ msgstr "Pobierz instalator" msgid "Download media" msgstr "Pobierz multimedia" -#. Button label that downloads a selected Linux desktop update package. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Download package" -msgstr "Pobierz pakiet" - #. Button that downloads the account recovery kit as a PDF file. #: src/features/auth/components/modals/RecoveryKitModal.tsx msgid "Download PDF" @@ -18280,11 +18265,6 @@ msgstr "Zainstaluj ją z menu przeglądarki." msgid "Install the new app" msgstr "Zainstaluj nową aplikację" -#. Label for the installed desktop version in the Linux update modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Installed" -msgstr "Zainstalowano" - #. Desktop updater modal body. The currentVersion placeholder is the installed app version. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Installed version: {currentVersion}." @@ -19794,11 +19774,6 @@ msgstr "Aplikacja dla systemu Linux" msgid "Linux default" msgstr "Domyślne urządzenie audio systemu Linux." -#. Select label for choosing the Linux desktop package format to download. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Linux package" -msgstr "Pakiet dla systemu Linux" - #. Forum layout name shown inside an activity log row. #. Forum layout option that shows posts as rows. #: src/features/forum/utils/ForumMessageDescriptors.ts @@ -22430,11 +22405,6 @@ msgstr "Nowe wątki przestaną być widoczne na liście kanału po tym okresie b msgid "New users can't join right now. You can still create an account or sign in, then try this link again later." msgstr "Nowi użytkownicy nie mogą teraz dołączyć. Nadal możesz utworzyć konto lub się zalogować, a później spróbować ponownie użyć tego linku." -#. Fallback version label when the available desktop version is unknown. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "New version" -msgstr "Nowa wersja" - #. US state name, shown next to the country name in the mature content gate. #: src/features/user/utils/UserGeo.ts msgctxt "us-state" @@ -24142,6 +24112,8 @@ msgid "Open settings" msgstr "Otwórz ustawienia" #. Button label that opens the software center via appstream protocol. +#. Native desktop dialog button on a Flatpak install that opens the software center page for the desktop app. +#: src/features/platform/utils/DesktopLocaleBridge.ts #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Open software center" msgstr "Otwórz centrum oprogramowania" @@ -28464,11 +28436,6 @@ msgstr "Okrągłe pole wyboru" msgid "Rows per page" msgstr "Wiersze na stronę" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "RPM" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "RPM package" -msgstr "Pakiet RPM" - #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts msgid "Run {productName} automatically when your computer starts" @@ -33120,11 +33087,6 @@ msgstr "Stuknij Udostępnij, a następnie Do ekranu początkowego." msgid "Tap to add a note" msgstr "Dotknij, aby dodać notatkę" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "tar.gz" verbatim and translate only the word "archive". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "tar.gz archive" -msgstr "Archiwum tar.gz" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/SharedDescriptors.ts msgid "Taskbar badge" @@ -33513,11 +33475,6 @@ msgstr "Połączenie jest szyfrowane protokołem TLS tylko wtedy, gdy serwer go msgid "The desktop update could not be downloaded. Try again from the update button when your connection is stable." msgstr "Nie udało się pobrać aktualizacji aplikacji komputerowej. Gdy połączenie będzie stabilne, spróbuj ponownie za pomocą przycisku aktualizacji." -#. Help text below the Linux package format selector in the desktop updater modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "The downloaded file is saved to your computer. Install it with your normal package manager." -msgstr "Pobrany plik został zapisany na Twoim komputerze. Zainstaluj go za pomocą swojego menedżera pakietów." - #. Activity log detail line under a deleted emoji, shown only when someone other than the person who deleted it had uploaded it. {user} is the user who uploaded the emoji, shown as a clickable user chip. #: src/features/guild/utils/guild_tabs/audit_log/AuditLogExpressionMessageMessages.ts msgid "The emoji was uploaded by {user}" @@ -36548,11 +36505,21 @@ msgstr "Do 4K przy 60 kl./s" msgid "Up to 720p at 30 FPS" msgstr "Do 720p przy 30 kl./s" +#. Native desktop dialog body under the headline announcing a new version, shown on a Flatpak install. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} from your software center." +msgstr "Zaktualizuj {appName} z centrum oprogramowania." + #. Tray menu item shown only while a desktop update is waiting. Clicking it installs the update. {appName} is the desktop app name (typically Fluxer). #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "Update {appName} now" msgstr "Zaktualizuj {appName} teraz" +#. Native desktop dialog body under the headline announcing a new version, shown on a Linux install managed by a package manager such as a deb, rpm or AUR package. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} with your package manager, or download the new version." +msgstr "Zaktualizuj {appName} za pomocą menedżera pakietów lub pobierz nową wersję." + #. Linux settings text when the installed desktop app is too old for system-wide shortcuts. {productName} is the app name. #: src/features/user/components/modals/tabs/components/SystemShortcutsSection.tsx msgid "Update {productName} to use system-wide shortcuts on Wayland." diff --git a/fluxer_app/src/features/i18n/locales/pt-BR/messages.po b/fluxer_app/src/features/i18n/locales/pt-BR/messages.po index e00a3882f..106b926a9 100644 --- a/fluxer_app/src/features/i18n/locales/pt-BR/messages.po +++ b/fluxer_app/src/features/i18n/locales/pt-BR/messages.po @@ -1802,6 +1802,16 @@ msgstr "O {PRODUCT_NAME} precisa reiniciar para que a alteração na barra de t msgid "{PRODUCT_NAME} needs to restart to disable Chromium's hardware acceleration. Use this only if you're troubleshooting graphics glitches or high GPU usage." msgstr "O {PRODUCT_NAME} precisa reiniciar para desativar a aceleração de hardware do Chromium. Use isso apenas se você estiver solucionando problemas gráficos ou alto uso da GPU." +#. Desktop updater modal body for a Flatpak install when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it from your software center." +msgstr "{productName} {version} está disponível. Atualize-o pelo seu gerenciador de pacotes." + +#. Desktop updater modal body for a Linux install managed by a package manager, such as a deb, rpm or AUR package, when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it with your package manager, or download the new version." +msgstr "{productName} {version} está disponível. Atualize-o com seu gerenciador de pacotes ou baixe a nova versão." + #. Accessible label for the Fluxer application icon. #: src/features/ui/components/icons/FluxerIconMark.tsx msgid "{productName} application icon" @@ -2669,11 +2679,6 @@ msgstr "Um bot prestativo que faz coisas incríveis!" msgid "a hostname, e.g. {exampleDomain}" msgstr "um nome de host, por exemplo, {exampleDomain}" -#. Linux desktop updater modal body shown before version details and package format selector. productName is the app name. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "A Linux desktop update is available. Choose the package format for this system and {productName} will save it locally." -msgstr "Há uma atualização do aplicativo para Linux. Escolha o formato do pacote para este sistema e o {productName} salvará o arquivo localmente." - #. Plutonium subscription card line offering a move to a lower price. {listPriceEffectiveDate} is a date already formatted and localized by code; never write a date into the translation. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx msgid "A lower price is available. Switching takes effect on <0>{listPriceEffectiveDate} and changes nothing else." @@ -11427,11 +11432,6 @@ msgstr "Com áudio desativado" msgid "Deafened by moderators" msgstr "Áudio desativado pelos moderadores" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "DEB" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "DEB package" -msgstr "Pacote DEB" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label in the settings dialog mobile settings view. #: src/features/app/components/dialogs/components/MobileSettingsView.tsx @@ -12095,21 +12095,11 @@ msgstr "As notificações no computador foram bloqueadas. Você pode ativá-las msgid "Desktop startup" msgstr "Inicialização no computador" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update {version} available. Choose a Linux package." -msgstr "Atualização {version} do aplicativo para computador disponível. Escolha um pacote Linux." - #. Modal title shown when a new desktop app update can be downloaded. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop update available" msgstr "Atualização do aplicativo para computador disponível" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format and no version is known. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update available. Choose a Linux package." -msgstr "Atualização para computador disponível. Escolha um pacote Linux." - #. Desktop updater modal body. The version placeholder is the target app version; productName is the app name. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop version {version} is available. Download the installer, then run it to update {productName}." @@ -13049,11 +13039,6 @@ msgstr "Baixar instalador" msgid "Download media" msgstr "Baixar mídia" -#. Button label that downloads a selected Linux desktop update package. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Download package" -msgstr "Baixar pacote" - #. Button that downloads the account recovery kit as a PDF file. #: src/features/auth/components/modals/RecoveryKitModal.tsx msgid "Download PDF" @@ -18280,11 +18265,6 @@ msgstr "Instale-o pelo menu do seu navegador." msgid "Install the new app" msgstr "Instalar o novo app" -#. Label for the installed desktop version in the Linux update modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Installed" -msgstr "Instalado" - #. Desktop updater modal body. The currentVersion placeholder is the installed app version. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Installed version: {currentVersion}." @@ -19794,11 +19774,6 @@ msgstr "Aplicativo Linux" msgid "Linux default" msgstr "Dispositivo de áudio padrão do Linux" -#. Select label for choosing the Linux desktop package format to download. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Linux package" -msgstr "Pacote Linux" - #. Forum layout name shown inside an activity log row. #. Forum layout option that shows posts as rows. #: src/features/forum/utils/ForumMessageDescriptors.ts @@ -22430,11 +22405,6 @@ msgstr "Novos tópicos deixarão de aparecer na lista de canais após este perí msgid "New users can't join right now. You can still create an account or sign in, then try this link again later." msgstr "Novos usuários não podem entrar agora. Você ainda pode criar uma conta ou fazer login e tentar este link novamente mais tarde." -#. Fallback version label when the available desktop version is unknown. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "New version" -msgstr "Nova versão" - #. US state name, shown next to the country name in the mature content gate. #: src/features/user/utils/UserGeo.ts msgctxt "us-state" @@ -24142,6 +24112,8 @@ msgid "Open settings" msgstr "Abrir configurações" #. Button label that opens the software center via appstream protocol. +#. Native desktop dialog button on a Flatpak install that opens the software center page for the desktop app. +#: src/features/platform/utils/DesktopLocaleBridge.ts #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Open software center" msgstr "Abrir central de software" @@ -28464,11 +28436,6 @@ msgstr "Caixa de seleção redonda" msgid "Rows per page" msgstr "Linhas por página" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "RPM" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "RPM package" -msgstr "Pacote RPM" - #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts msgid "Run {productName} automatically when your computer starts" @@ -33120,11 +33087,6 @@ msgstr "Toque em Compartilhar e, em seguida, em Adicionar à Tela de Início." msgid "Tap to add a note" msgstr "Toque para adicionar uma nota" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "tar.gz" verbatim and translate only the word "archive". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "tar.gz archive" -msgstr "Arquivo tar.gz" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/SharedDescriptors.ts msgid "Taskbar badge" @@ -33513,11 +33475,6 @@ msgstr "A conexão é atualizada para TLS somente se o servidor a oferecer." msgid "The desktop update could not be downloaded. Try again from the update button when your connection is stable." msgstr "Não foi possível baixar a atualização para computador. Tente novamente no botão de atualização quando sua conexão estiver estável." -#. Help text below the Linux package format selector in the desktop updater modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "The downloaded file is saved to your computer. Install it with your normal package manager." -msgstr "O arquivo baixado é salvo no seu computador. Instale-o com seu gerenciador de pacotes normal." - #. Activity log detail line under a deleted emoji, shown only when someone other than the person who deleted it had uploaded it. {user} is the user who uploaded the emoji, shown as a clickable user chip. #: src/features/guild/utils/guild_tabs/audit_log/AuditLogExpressionMessageMessages.ts msgid "The emoji was uploaded by {user}" @@ -36548,11 +36505,21 @@ msgstr "Até 4K a 60 FPS" msgid "Up to 720p at 30 FPS" msgstr "Até 720p a 30 FPS" +#. Native desktop dialog body under the headline announcing a new version, shown on a Flatpak install. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} from your software center." +msgstr "Atualize o {appName} no seu gerenciador de softwares." + #. Tray menu item shown only while a desktop update is waiting. Clicking it installs the update. {appName} is the desktop app name (typically Fluxer). #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "Update {appName} now" msgstr "Atualizar {appName} agora" +#. Native desktop dialog body under the headline announcing a new version, shown on a Linux install managed by a package manager such as a deb, rpm or AUR package. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} with your package manager, or download the new version." +msgstr "Atualize o {appName} com seu gerenciador de pacotes ou baixe a nova versão." + #. Linux settings text when the installed desktop app is too old for system-wide shortcuts. {productName} is the app name. #: src/features/user/components/modals/tabs/components/SystemShortcutsSection.tsx msgid "Update {productName} to use system-wide shortcuts on Wayland." diff --git a/fluxer_app/src/features/i18n/locales/ro/messages.po b/fluxer_app/src/features/i18n/locales/ro/messages.po index 9f45667f2..67794bed6 100644 --- a/fluxer_app/src/features/i18n/locales/ro/messages.po +++ b/fluxer_app/src/features/i18n/locales/ro/messages.po @@ -1802,6 +1802,16 @@ msgstr "{PRODUCT_NAME} trebuie să repornească pentru ca modificarea barei de t msgid "{PRODUCT_NAME} needs to restart to disable Chromium's hardware acceleration. Use this only if you're troubleshooting graphics glitches or high GPU usage." msgstr "{PRODUCT_NAME} trebuie să repornească pentru a dezactiva accelerarea hardware a Chromium. Folosește această opțiune doar dacă depanezi erori grafice sau o utilizare ridicată a GPU-ului." +#. Desktop updater modal body for a Flatpak install when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it from your software center." +msgstr "{productName} {version} este disponibil. Actualizați-l din centrul dvs. software." + +#. Desktop updater modal body for a Linux install managed by a package manager, such as a deb, rpm or AUR package, when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it with your package manager, or download the new version." +msgstr "{productName} {version} este disponibil. Actualizează-l cu managerul tău de pachete sau descarcă noua versiune." + #. Accessible label for the Fluxer application icon. #: src/features/ui/components/icons/FluxerIconMark.tsx msgid "{productName} application icon" @@ -2669,11 +2679,6 @@ msgstr "Un bot util care face lucruri uimitoare!" msgid "a hostname, e.g. {exampleDomain}" msgstr "un nume de gazdă, de ex. {exampleDomain}" -#. Linux desktop updater modal body shown before version details and package format selector. productName is the app name. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "A Linux desktop update is available. Choose the package format for this system and {productName} will save it locally." -msgstr "Este disponibilă o actualizare desktop pentru Linux. Alege formatul de pachet pentru acest sistem, iar {productName} îl va salva local." - #. Plutonium subscription card line offering a move to a lower price. {listPriceEffectiveDate} is a date already formatted and localized by code; never write a date into the translation. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx msgid "A lower price is available. Switching takes effect on <0>{listPriceEffectiveDate} and changes nothing else." @@ -11427,11 +11432,6 @@ msgstr "Sunet dezactivat" msgid "Deafened by moderators" msgstr "Sunet dezactivat de moderatori" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "DEB" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "DEB package" -msgstr "Pachet DEB" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label in the settings dialog mobile settings view. #: src/features/app/components/dialogs/components/MobileSettingsView.tsx @@ -12095,21 +12095,11 @@ msgstr "Notificările desktop au fost blocate. Le poți activa mai târziu din s msgid "Desktop startup" msgstr "Pornire desktop" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update {version} available. Choose a Linux package." -msgstr "Actualizare desktop {version} disponibilă. Alege un pachet Linux." - #. Modal title shown when a new desktop app update can be downloaded. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop update available" msgstr "Actualizare desktop disponibilă" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format and no version is known. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update available. Choose a Linux package." -msgstr "Actualizare desktop disponibilă. Alege un pachet Linux." - #. Desktop updater modal body. The version placeholder is the target app version; productName is the app name. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop version {version} is available. Download the installer, then run it to update {productName}." @@ -13049,11 +13039,6 @@ msgstr "Descarcă programul de instalare" msgid "Download media" msgstr "Descarcă media" -#. Button label that downloads a selected Linux desktop update package. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Download package" -msgstr "Descarcă pachetul" - #. Button that downloads the account recovery kit as a PDF file. #: src/features/auth/components/modals/RecoveryKitModal.tsx msgid "Download PDF" @@ -18280,11 +18265,6 @@ msgstr "Instalează-l din meniul browserului tău." msgid "Install the new app" msgstr "Instalează aplicația nouă" -#. Label for the installed desktop version in the Linux update modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Installed" -msgstr "Instalat" - #. Desktop updater modal body. The currentVersion placeholder is the installed app version. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Installed version: {currentVersion}." @@ -19794,11 +19774,6 @@ msgstr "Aplicație Linux" msgid "Linux default" msgstr "Implicit Linux" -#. Select label for choosing the Linux desktop package format to download. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Linux package" -msgstr "Pachet Linux" - #. Forum layout name shown inside an activity log row. #. Forum layout option that shows posts as rows. #: src/features/forum/utils/ForumMessageDescriptors.ts @@ -22430,11 +22405,6 @@ msgstr "Noile discuții vor înceta să apară în lista canalului după aceast msgid "New users can't join right now. You can still create an account or sign in, then try this link again later." msgstr "Utilizatorii noi nu se pot alătura momentan. Poți crea în continuare un cont sau te poți conecta, apoi încearcă din nou acest link mai târziu." -#. Fallback version label when the available desktop version is unknown. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "New version" -msgstr "Versiune nouă" - #. US state name, shown next to the country name in the mature content gate. #: src/features/user/utils/UserGeo.ts msgctxt "us-state" @@ -24142,6 +24112,8 @@ msgid "Open settings" msgstr "Deschide setările" #. Button label that opens the software center via appstream protocol. +#. Native desktop dialog button on a Flatpak install that opens the software center page for the desktop app. +#: src/features/platform/utils/DesktopLocaleBridge.ts #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Open software center" msgstr "Deschide centrul software" @@ -28464,11 +28436,6 @@ msgstr "Casetă de selectare rotundă" msgid "Rows per page" msgstr "Rânduri pe pagină" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "RPM" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "RPM package" -msgstr "Pachet RPM" - #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts msgid "Run {productName} automatically when your computer starts" @@ -33120,11 +33087,6 @@ msgstr "Atinge Partajează, apoi Adaugă pe ecranul principal." msgid "Tap to add a note" msgstr "Atinge pentru a adăuga o notă" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "tar.gz" verbatim and translate only the word "archive". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "tar.gz archive" -msgstr "Arhivă tar.gz" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/SharedDescriptors.ts msgid "Taskbar badge" @@ -33513,11 +33475,6 @@ msgstr "Conexiunea este actualizată la TLS doar dacă serverul o oferă." msgid "The desktop update could not be downloaded. Try again from the update button when your connection is stable." msgstr "Actualizarea pentru desktop nu a putut fi descărcată. Încearcă din nou de la butonul de actualizare când conexiunea ta este stabilă." -#. Help text below the Linux package format selector in the desktop updater modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "The downloaded file is saved to your computer. Install it with your normal package manager." -msgstr "Fișierul descărcat este salvat pe computerul tău. Instalează-l cu managerul tău de pachete obișnuit." - #. Activity log detail line under a deleted emoji, shown only when someone other than the person who deleted it had uploaded it. {user} is the user who uploaded the emoji, shown as a clickable user chip. #: src/features/guild/utils/guild_tabs/audit_log/AuditLogExpressionMessageMessages.ts msgid "The emoji was uploaded by {user}" @@ -36548,11 +36505,21 @@ msgstr "Până la 4K la 60 FPS" msgid "Up to 720p at 30 FPS" msgstr "Până la 720p la 30 FPS" +#. Native desktop dialog body under the headline announcing a new version, shown on a Flatpak install. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} from your software center." +msgstr "Actualizează {appName} din centrul tău software." + #. Tray menu item shown only while a desktop update is waiting. Clicking it installs the update. {appName} is the desktop app name (typically Fluxer). #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "Update {appName} now" msgstr "Actualizează {appName} acum" +#. Native desktop dialog body under the headline announcing a new version, shown on a Linux install managed by a package manager such as a deb, rpm or AUR package. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} with your package manager, or download the new version." +msgstr "Actualizează {appName} cu managerul tău de pachete sau descarcă noua versiune." + #. Linux settings text when the installed desktop app is too old for system-wide shortcuts. {productName} is the app name. #: src/features/user/components/modals/tabs/components/SystemShortcutsSection.tsx msgid "Update {productName} to use system-wide shortcuts on Wayland." diff --git a/fluxer_app/src/features/i18n/locales/ru/messages.po b/fluxer_app/src/features/i18n/locales/ru/messages.po index 6307f2afa..4253d3e31 100644 --- a/fluxer_app/src/features/i18n/locales/ru/messages.po +++ b/fluxer_app/src/features/i18n/locales/ru/messages.po @@ -1802,6 +1802,16 @@ msgstr "Чтобы изменения в строке заголовка вст msgid "{PRODUCT_NAME} needs to restart to disable Chromium's hardware acceleration. Use this only if you're troubleshooting graphics glitches or high GPU usage." msgstr "{PRODUCT_NAME} нужно перезапустить, чтобы отключить аппаратное ускорение Chromium. Используйте это, только если устраняете графические сбои или высокую нагрузку на GPU." +#. Desktop updater modal body for a Flatpak install when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it from your software center." +msgstr "{productName} {version} доступна. Обновите её из вашего центра приложений." + +#. Desktop updater modal body for a Linux install managed by a package manager, such as a deb, rpm or AUR package, when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it with your package manager, or download the new version." +msgstr "Доступна новая версия {productName} {version}. Обновите её с помощью менеджера пакетов или скачайте новую версию." + #. Accessible label for the Fluxer application icon. #: src/features/ui/components/icons/FluxerIconMark.tsx msgid "{productName} application icon" @@ -2669,11 +2679,6 @@ msgstr "Полезный бот, который делает удивитель msgid "a hostname, e.g. {exampleDomain}" msgstr "имя хоста, например {exampleDomain}" -#. Linux desktop updater modal body shown before version details and package format selector. productName is the app name. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "A Linux desktop update is available. Choose the package format for this system and {productName} will save it locally." -msgstr "Доступно обновление для Linux. Выберите формат пакета для этой системы, и {productName} сохранит его локально." - #. Plutonium subscription card line offering a move to a lower price. {listPriceEffectiveDate} is a date already formatted and localized by code; never write a date into the translation. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx msgid "A lower price is available. Switching takes effect on <0>{listPriceEffectiveDate} and changes nothing else." @@ -11427,11 +11432,6 @@ msgstr "Звук отключён" msgid "Deafened by moderators" msgstr "Звук отключён модераторами" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "DEB" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "DEB package" -msgstr "Пакет DEB" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label in the settings dialog mobile settings view. #: src/features/app/components/dialogs/components/MobileSettingsView.tsx @@ -12095,21 +12095,11 @@ msgstr "Уведомления на компьютере заблокирова msgid "Desktop startup" msgstr "Автозапуск" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update {version} available. Choose a Linux package." -msgstr "Доступно обновление для ПК {version}. Выберите пакет Linux." - #. Modal title shown when a new desktop app update can be downloaded. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop update available" msgstr "Доступно обновление для ПК" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format and no version is known. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update available. Choose a Linux package." -msgstr "Доступно обновление для ПК. Выберите пакет Linux." - #. Desktop updater modal body. The version placeholder is the target app version; productName is the app name. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop version {version} is available. Download the installer, then run it to update {productName}." @@ -13049,11 +13039,6 @@ msgstr "Скачать установщик" msgid "Download media" msgstr "Скачать медиа" -#. Button label that downloads a selected Linux desktop update package. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Download package" -msgstr "Скачать пакет" - #. Button that downloads the account recovery kit as a PDF file. #: src/features/auth/components/modals/RecoveryKitModal.tsx msgid "Download PDF" @@ -18280,11 +18265,6 @@ msgstr "Установите его из меню браузера." msgid "Install the new app" msgstr "Установить новое приложение" -#. Label for the installed desktop version in the Linux update modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Installed" -msgstr "Установлено" - #. Desktop updater modal body. The currentVersion placeholder is the installed app version. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Installed version: {currentVersion}." @@ -19794,11 +19774,6 @@ msgstr "Приложение Linux" msgid "Linux default" msgstr "Linux (устройство по умолчанию)" -#. Select label for choosing the Linux desktop package format to download. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Linux package" -msgstr "Пакет для Linux" - #. Forum layout name shown inside an activity log row. #. Forum layout option that shows posts as rows. #: src/features/forum/utils/ForumMessageDescriptors.ts @@ -22430,11 +22405,6 @@ msgstr "Новые ветки перестанут отображаться в msgid "New users can't join right now. You can still create an account or sign in, then try this link again later." msgstr "Новые пользователи пока не могут присоединиться. Вы можете создать аккаунт или войти, а затем попробовать эту ссылку позже." -#. Fallback version label when the available desktop version is unknown. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "New version" -msgstr "Новая версия" - #. US state name, shown next to the country name in the mature content gate. #: src/features/user/utils/UserGeo.ts msgctxt "us-state" @@ -24142,6 +24112,8 @@ msgid "Open settings" msgstr "Открыть настройки" #. Button label that opens the software center via appstream protocol. +#. Native desktop dialog button on a Flatpak install that opens the software center page for the desktop app. +#: src/features/platform/utils/DesktopLocaleBridge.ts #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Open software center" msgstr "Открыть центр приложений" @@ -28464,11 +28436,6 @@ msgstr "Круглый флажок" msgid "Rows per page" msgstr "Строк на странице" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "RPM" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "RPM package" -msgstr "Пакет RPM" - #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts msgid "Run {productName} automatically when your computer starts" @@ -33120,11 +33087,6 @@ msgstr "Нажмите «Поделиться», затем «На экран « msgid "Tap to add a note" msgstr "Нажмите, чтобы добавить заметку" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "tar.gz" verbatim and translate only the word "archive". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "tar.gz archive" -msgstr "Архив tar.gz" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/SharedDescriptors.ts msgid "Taskbar badge" @@ -33513,11 +33475,6 @@ msgstr "Соединение будет обновлено до TLS только msgid "The desktop update could not be downloaded. Try again from the update button when your connection is stable." msgstr "Не удалось загрузить обновление для ПК. Повторите попытку с помощью кнопки обновления, когда соединение станет стабильным." -#. Help text below the Linux package format selector in the desktop updater modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "The downloaded file is saved to your computer. Install it with your normal package manager." -msgstr "Загруженный файл сохранен на вашем компьютере. Установите его с помощью обычного менеджера пакетов." - #. Activity log detail line under a deleted emoji, shown only when someone other than the person who deleted it had uploaded it. {user} is the user who uploaded the emoji, shown as a clickable user chip. #: src/features/guild/utils/guild_tabs/audit_log/AuditLogExpressionMessageMessages.ts msgid "The emoji was uploaded by {user}" @@ -36548,11 +36505,21 @@ msgstr "До 4K при 60 FPS" msgid "Up to 720p at 30 FPS" msgstr "До 720p при 30 FPS" +#. Native desktop dialog body under the headline announcing a new version, shown on a Flatpak install. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} from your software center." +msgstr "Обновите {appName} из вашего центра приложений." + #. Tray menu item shown only while a desktop update is waiting. Clicking it installs the update. {appName} is the desktop app name (typically Fluxer). #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "Update {appName} now" msgstr "Обновить {appName} сейчас" +#. Native desktop dialog body under the headline announcing a new version, shown on a Linux install managed by a package manager such as a deb, rpm or AUR package. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} with your package manager, or download the new version." +msgstr "Обновите {appName} с помощью вашего менеджера пакетов или скачайте новую версию." + #. Linux settings text when the installed desktop app is too old for system-wide shortcuts. {productName} is the app name. #: src/features/user/components/modals/tabs/components/SystemShortcutsSection.tsx msgid "Update {productName} to use system-wide shortcuts on Wayland." diff --git a/fluxer_app/src/features/i18n/locales/sv-SE/messages.po b/fluxer_app/src/features/i18n/locales/sv-SE/messages.po index 04ce2100b..f4abd67d1 100644 --- a/fluxer_app/src/features/i18n/locales/sv-SE/messages.po +++ b/fluxer_app/src/features/i18n/locales/sv-SE/messages.po @@ -1802,6 +1802,16 @@ msgstr "{PRODUCT_NAME} behöver startas om för att ändringen av namnlisten ska msgid "{PRODUCT_NAME} needs to restart to disable Chromium's hardware acceleration. Use this only if you're troubleshooting graphics glitches or high GPU usage." msgstr "{PRODUCT_NAME} måste startas om för att inaktivera Chromiums hårdvaruacceleration. Använd detta endast om du felsöker grafikproblem eller hög GPU-användning." +#. Desktop updater modal body for a Flatpak install when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it from your software center." +msgstr "{productName} {version} är tillgänglig. Uppdatera den från ditt programcenter." + +#. Desktop updater modal body for a Linux install managed by a package manager, such as a deb, rpm or AUR package, when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it with your package manager, or download the new version." +msgstr "{productName} {version} är tillgängligt. Uppdatera det med din pakethanterare, eller ladda ner den nya versionen." + #. Accessible label for the Fluxer application icon. #: src/features/ui/components/icons/FluxerIconMark.tsx msgid "{productName} application icon" @@ -2669,11 +2679,6 @@ msgstr "En hjälpsam bot som gör fantastiska saker!" msgid "a hostname, e.g. {exampleDomain}" msgstr "ett värdnamn, t.ex. {exampleDomain}" -#. Linux desktop updater modal body shown before version details and package format selector. productName is the app name. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "A Linux desktop update is available. Choose the package format for this system and {productName} will save it locally." -msgstr "En uppdatering av datorappen för Linux är tillgänglig. Välj paketformatet för det här systemet så sparar {productName} paketet lokalt." - #. Plutonium subscription card line offering a move to a lower price. {listPriceEffectiveDate} is a date already formatted and localized by code; never write a date into the translation. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx msgid "A lower price is available. Switching takes effect on <0>{listPriceEffectiveDate} and changes nothing else." @@ -11427,11 +11432,6 @@ msgstr "Ljudet avstängt" msgid "Deafened by moderators" msgstr "Ljudet avstängt av moderatorer" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "DEB" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "DEB package" -msgstr "DEB-paket" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label in the settings dialog mobile settings view. #: src/features/app/components/dialogs/components/MobileSettingsView.tsx @@ -12095,21 +12095,11 @@ msgstr "Datoraviseringar har blockerats. Du kan aktivera dem senare i webbläsar msgid "Desktop startup" msgstr "Start av datorappen" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update {version} available. Choose a Linux package." -msgstr "Uppdatering {version} för datorappen är tillgänglig. Välj ett Linux-paket." - #. Modal title shown when a new desktop app update can be downloaded. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop update available" msgstr "Uppdatering för datorappen är tillgänglig" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format and no version is known. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update available. Choose a Linux package." -msgstr "Uppdatering för datorappen är tillgänglig. Välj ett Linux-paket." - #. Desktop updater modal body. The version placeholder is the target app version; productName is the app name. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop version {version} is available. Download the installer, then run it to update {productName}." @@ -13049,11 +13039,6 @@ msgstr "Ladda ner installationsprogram" msgid "Download media" msgstr "Ladda ner medieobjekt" -#. Button label that downloads a selected Linux desktop update package. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Download package" -msgstr "Ladda ner paket" - #. Button that downloads the account recovery kit as a PDF file. #: src/features/auth/components/modals/RecoveryKitModal.tsx msgid "Download PDF" @@ -18280,11 +18265,6 @@ msgstr "Installera det från din webbläsarmeny." msgid "Install the new app" msgstr "Installera den nya appen" -#. Label for the installed desktop version in the Linux update modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Installed" -msgstr "Installerad" - #. Desktop updater modal body. The currentVersion placeholder is the installed app version. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Installed version: {currentVersion}." @@ -19794,11 +19774,6 @@ msgstr "Linux-app" msgid "Linux default" msgstr "Standard för Linux" -#. Select label for choosing the Linux desktop package format to download. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Linux package" -msgstr "Linux-paket" - #. Forum layout name shown inside an activity log row. #. Forum layout option that shows posts as rows. #: src/features/forum/utils/ForumMessageDescriptors.ts @@ -22430,11 +22405,6 @@ msgstr "Nya trådar slutar visas i kanalens lista efter denna inaktivitetsperiod msgid "New users can't join right now. You can still create an account or sign in, then try this link again later." msgstr "Nya användare kan inte gå med just nu. Du kan fortfarande skapa ett konto eller logga in och sedan försöka med den här länken igen senare." -#. Fallback version label when the available desktop version is unknown. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "New version" -msgstr "Ny version" - #. US state name, shown next to the country name in the mature content gate. #: src/features/user/utils/UserGeo.ts msgctxt "us-state" @@ -24142,6 +24112,8 @@ msgid "Open settings" msgstr "Öppna inställningar" #. Button label that opens the software center via appstream protocol. +#. Native desktop dialog button on a Flatpak install that opens the software center page for the desktop app. +#: src/features/platform/utils/DesktopLocaleBridge.ts #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Open software center" msgstr "Öppna programcenter" @@ -28464,11 +28436,6 @@ msgstr "Rund kryssruta" msgid "Rows per page" msgstr "Rader per sida" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "RPM" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "RPM package" -msgstr "RPM-paket" - #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts msgid "Run {productName} automatically when your computer starts" @@ -33120,11 +33087,6 @@ msgstr "Tryck på Dela och sedan på Lägg till på hemskärmen." msgid "Tap to add a note" msgstr "Tryck för att lägga till en anteckning" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "tar.gz" verbatim and translate only the word "archive". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "tar.gz archive" -msgstr "tar.gz-arkiv" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/SharedDescriptors.ts msgid "Taskbar badge" @@ -33513,11 +33475,6 @@ msgstr "Anslutningen uppgraderas till TLS endast om servern erbjuder det." msgid "The desktop update could not be downloaded. Try again from the update button when your connection is stable." msgstr "Uppdateringen för datorappen kunde inte laddas ner. Försök igen via uppdateringsknappen när anslutningen är stabil." -#. Help text below the Linux package format selector in the desktop updater modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "The downloaded file is saved to your computer. Install it with your normal package manager." -msgstr "Den nedladdade filen sparas på din dator. Installera den med din vanliga pakethanterare." - #. Activity log detail line under a deleted emoji, shown only when someone other than the person who deleted it had uploaded it. {user} is the user who uploaded the emoji, shown as a clickable user chip. #: src/features/guild/utils/guild_tabs/audit_log/AuditLogExpressionMessageMessages.ts msgid "The emoji was uploaded by {user}" @@ -36548,11 +36505,21 @@ msgstr "Upp till 4K med 60 FPS" msgid "Up to 720p at 30 FPS" msgstr "Upp till 720p i 30 FPS" +#. Native desktop dialog body under the headline announcing a new version, shown on a Flatpak install. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} from your software center." +msgstr "Uppdatera {appName} från ditt programcenter." + #. Tray menu item shown only while a desktop update is waiting. Clicking it installs the update. {appName} is the desktop app name (typically Fluxer). #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "Update {appName} now" msgstr "Uppdatera {appName} nu" +#. Native desktop dialog body under the headline announcing a new version, shown on a Linux install managed by a package manager such as a deb, rpm or AUR package. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} with your package manager, or download the new version." +msgstr "Uppdatera {appName} med din pakethanterare, eller ladda ner den nya versionen." + #. Linux settings text when the installed desktop app is too old for system-wide shortcuts. {productName} is the app name. #: src/features/user/components/modals/tabs/components/SystemShortcutsSection.tsx msgid "Update {productName} to use system-wide shortcuts on Wayland." diff --git a/fluxer_app/src/features/i18n/locales/th/messages.po b/fluxer_app/src/features/i18n/locales/th/messages.po index 83bf0b2df..4aa3725d4 100644 --- a/fluxer_app/src/features/i18n/locales/th/messages.po +++ b/fluxer_app/src/features/i18n/locales/th/messages.po @@ -1802,6 +1802,16 @@ msgstr "{PRODUCT_NAME} ต้องรีสตาร์ทเพื่อให msgid "{PRODUCT_NAME} needs to restart to disable Chromium's hardware acceleration. Use this only if you're troubleshooting graphics glitches or high GPU usage." msgstr "{PRODUCT_NAME} ต้องรีสตาร์ทเพื่อปิดการเร่งฮาร์ดแวร์ของ Chromium ใช้ตัวเลือกนี้เฉพาะเมื่อคุณกำลังแก้ปัญหาภาพกราฟิกผิดปกติหรือการใช้งาน GPU สูง" +#. Desktop updater modal body for a Flatpak install when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it from your software center." +msgstr "มี {productName} {version} พร้อมให้อัปเดตแล้ว อัปเดตได้จากศูนย์ซอฟต์แวร์ของคุณ" + +#. Desktop updater modal body for a Linux install managed by a package manager, such as a deb, rpm or AUR package, when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it with your package manager, or download the new version." +msgstr "มี {productName} {version} พร้อมให้อัปเดตแล้ว อัปเดตด้วยตัวจัดการแพ็กเกจของคุณ หรือดาวน์โหลดเวอร์ชันใหม่" + #. Accessible label for the Fluxer application icon. #: src/features/ui/components/icons/FluxerIconMark.tsx msgid "{productName} application icon" @@ -2669,11 +2679,6 @@ msgstr "บอทตัวช่วยที่ทำสิ่งน่าทึ msgid "a hostname, e.g. {exampleDomain}" msgstr "ชื่อโฮสต์ เช่น {exampleDomain}" -#. Linux desktop updater modal body shown before version details and package format selector. productName is the app name. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "A Linux desktop update is available. Choose the package format for this system and {productName} will save it locally." -msgstr "มีการอัปเดตเดสก์ท็อปสำหรับ Linux แล้ว เลือกรูปแบบแพ็กเกจสำหรับระบบนี้ แล้ว {productName} จะบันทึกไฟล์ไว้ในเครื่อง" - #. Plutonium subscription card line offering a move to a lower price. {listPriceEffectiveDate} is a date already formatted and localized by code; never write a date into the translation. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx msgid "A lower price is available. Switching takes effect on <0>{listPriceEffectiveDate} and changes nothing else." @@ -11427,11 +11432,6 @@ msgstr "ปิดเสียงทั้งหมดแล้ว" msgid "Deafened by moderators" msgstr "ปิดเสียงทั้งหมดโดยผู้ดูแล" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "DEB" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "DEB package" -msgstr "แพ็กเกจ DEB" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label in the settings dialog mobile settings view. #: src/features/app/components/dialogs/components/MobileSettingsView.tsx @@ -12095,21 +12095,11 @@ msgstr "การแจ้งเตือนบนเดสก์ท็อปถ msgid "Desktop startup" msgstr "การเริ่มต้นบนเดสก์ท็อป" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update {version} available. Choose a Linux package." -msgstr "มีอัปเดตเดสก์ท็อป {version} ให้ใช้งาน เลือกแพ็กเกจ Linux" - #. Modal title shown when a new desktop app update can be downloaded. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop update available" msgstr "มีอัปเดตสำหรับเดสก์ท็อป" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format and no version is known. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update available. Choose a Linux package." -msgstr "มีการอัปเดตเดสก์ท็อป เลือกแพ็กเกจ Linux" - #. Desktop updater modal body. The version placeholder is the target app version; productName is the app name. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop version {version} is available. Download the installer, then run it to update {productName}." @@ -13049,11 +13039,6 @@ msgstr "ดาวน์โหลดตัวติดตั้ง" msgid "Download media" msgstr "ดาวน์โหลดสื่อ" -#. Button label that downloads a selected Linux desktop update package. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Download package" -msgstr "ดาวน์โหลดแพ็กเกจ" - #. Button that downloads the account recovery kit as a PDF file. #: src/features/auth/components/modals/RecoveryKitModal.tsx msgid "Download PDF" @@ -18280,11 +18265,6 @@ msgstr "ติดตั้งจากเมนูเบราว์เซอร msgid "Install the new app" msgstr "ติดตั้งแอปใหม่" -#. Label for the installed desktop version in the Linux update modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Installed" -msgstr "ติดตั้งแล้ว" - #. Desktop updater modal body. The currentVersion placeholder is the installed app version. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Installed version: {currentVersion}." @@ -19794,11 +19774,6 @@ msgstr "แอป Linux" msgid "Linux default" msgstr "ค่าเริ่มต้นของ Linux" -#. Select label for choosing the Linux desktop package format to download. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Linux package" -msgstr "แพ็กเกจ Linux" - #. Forum layout name shown inside an activity log row. #. Forum layout option that shows posts as rows. #: src/features/forum/utils/ForumMessageDescriptors.ts @@ -22430,11 +22405,6 @@ msgstr "เธรดใหม่จะหยุดแสดงในรายก msgid "New users can't join right now. You can still create an account or sign in, then try this link again later." msgstr "ผู้ใช้ใหม่ไม่สามารถเข้าร่วมได้ในขณะนี้ คุณยังสามารถสร้างบัญชีหรือลงชื่อเข้าใช้ แล้วลองใช้ลิงก์นี้อีกครั้งในภายหลังได้" -#. Fallback version label when the available desktop version is unknown. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "New version" -msgstr "เวอร์ชันใหม่" - #. US state name, shown next to the country name in the mature content gate. #: src/features/user/utils/UserGeo.ts msgctxt "us-state" @@ -24142,6 +24112,8 @@ msgid "Open settings" msgstr "เปิดการตั้งค่า" #. Button label that opens the software center via appstream protocol. +#. Native desktop dialog button on a Flatpak install that opens the software center page for the desktop app. +#: src/features/platform/utils/DesktopLocaleBridge.ts #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Open software center" msgstr "เปิดศูนย์ซอฟต์แวร์" @@ -28464,11 +28436,6 @@ msgstr "ช่องทำเครื่องหมายแบบวงกล msgid "Rows per page" msgstr "จำนวนแถวต่อหน้า" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "RPM" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "RPM package" -msgstr "แพ็กเกจ RPM" - #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts msgid "Run {productName} automatically when your computer starts" @@ -33120,11 +33087,6 @@ msgstr "แตะแชร์ แล้วแตะ เพิ่มไปยั msgid "Tap to add a note" msgstr "แตะเพื่อเพิ่มโน้ต" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "tar.gz" verbatim and translate only the word "archive". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "tar.gz archive" -msgstr "ไฟล์บีบอัด tar.gz" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/SharedDescriptors.ts msgid "Taskbar badge" @@ -33513,11 +33475,6 @@ msgstr "การเชื่อมต่อจะอัปเกรดเป็ msgid "The desktop update could not be downloaded. Try again from the update button when your connection is stable." msgstr "ไม่สามารถดาวน์โหลดการอัปเดตเดสก์ท็อปได้ โปรดลองอีกครั้งจากปุ่มอัปเดตเมื่อการเชื่อมต่อของคุณเสถียร" -#. Help text below the Linux package format selector in the desktop updater modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "The downloaded file is saved to your computer. Install it with your normal package manager." -msgstr "ไฟล์ที่ดาวน์โหลดจะถูกบันทึกลงในคอมพิวเตอร์ของคุณ ติดตั้งด้วยตัวจัดการแพ็กเกจปกติของคุณ" - #. Activity log detail line under a deleted emoji, shown only when someone other than the person who deleted it had uploaded it. {user} is the user who uploaded the emoji, shown as a clickable user chip. #: src/features/guild/utils/guild_tabs/audit_log/AuditLogExpressionMessageMessages.ts msgid "The emoji was uploaded by {user}" @@ -36548,11 +36505,21 @@ msgstr "สูงสุด 4K ที่ 60 FPS" msgid "Up to 720p at 30 FPS" msgstr "สูงสุด 720p ที่ 30 FPS" +#. Native desktop dialog body under the headline announcing a new version, shown on a Flatpak install. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} from your software center." +msgstr "อัปเดต {appName} จากศูนย์ซอฟต์แวร์ของคุณ" + #. Tray menu item shown only while a desktop update is waiting. Clicking it installs the update. {appName} is the desktop app name (typically Fluxer). #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "Update {appName} now" msgstr "อัปเดต {appName} ตอนนี้" +#. Native desktop dialog body under the headline announcing a new version, shown on a Linux install managed by a package manager such as a deb, rpm or AUR package. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} with your package manager, or download the new version." +msgstr "อัปเดต {appName} ด้วยตัวจัดการแพ็กเกจของคุณ หรือดาวน์โหลดเวอร์ชันใหม่" + #. Linux settings text when the installed desktop app is too old for system-wide shortcuts. {productName} is the app name. #: src/features/user/components/modals/tabs/components/SystemShortcutsSection.tsx msgid "Update {productName} to use system-wide shortcuts on Wayland." diff --git a/fluxer_app/src/features/i18n/locales/tr/messages.po b/fluxer_app/src/features/i18n/locales/tr/messages.po index c10152d1f..ec7d5ab48 100644 --- a/fluxer_app/src/features/i18n/locales/tr/messages.po +++ b/fluxer_app/src/features/i18n/locales/tr/messages.po @@ -1802,6 +1802,16 @@ msgstr "Başlık çubuğu değişikliğinin etkili olması için {PRODUCT_NAME} msgid "{PRODUCT_NAME} needs to restart to disable Chromium's hardware acceleration. Use this only if you're troubleshooting graphics glitches or high GPU usage." msgstr "Chromium'un donanım hızlandırmasını devre dışı bırakmak için {PRODUCT_NAME} uygulamasının yeniden başlatılması gerekiyor. Bunu yalnızca grafik hatalarını veya yüksek GPU kullanımını gideriyorsanız kullanın." +#. Desktop updater modal body for a Flatpak install when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it from your software center." +msgstr "{productName} {version} sürümü mevcut. Yazılım merkezinizden güncelleyin." + +#. Desktop updater modal body for a Linux install managed by a package manager, such as a deb, rpm or AUR package, when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it with your package manager, or download the new version." +msgstr "{productName} {version} sürümü mevcut. Paket yöneticinizle güncelleyebilir veya yeni sürümü indirebilirsiniz." + #. Accessible label for the Fluxer application icon. #: src/features/ui/components/icons/FluxerIconMark.tsx msgid "{productName} application icon" @@ -2669,11 +2679,6 @@ msgstr "Harika şeyler yapan yardımcı bir bot!" msgid "a hostname, e.g. {exampleDomain}" msgstr "bir ana bilgisayar adı, örn. {exampleDomain}" -#. Linux desktop updater modal body shown before version details and package format selector. productName is the app name. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "A Linux desktop update is available. Choose the package format for this system and {productName} will save it locally." -msgstr "Linux masaüstü için bir güncelleme mevcut. Bu sistem için paket biçimini seçin ve {productName} uygulaması bunu yerel olarak kaydedecektir." - #. Plutonium subscription card line offering a move to a lower price. {listPriceEffectiveDate} is a date already formatted and localized by code; never write a date into the translation. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx msgid "A lower price is available. Switching takes effect on <0>{listPriceEffectiveDate} and changes nothing else." @@ -11427,11 +11432,6 @@ msgstr "Sağırlaştırıldı" msgid "Deafened by moderators" msgstr "Moderatörler tarafından sağırlaştırıldı" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "DEB" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "DEB package" -msgstr "DEB paketi" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label in the settings dialog mobile settings view. #: src/features/app/components/dialogs/components/MobileSettingsView.tsx @@ -12095,21 +12095,11 @@ msgstr "Masaüstü bildirimleri engellendi. Daha sonra tarayıcı ayarlarınızd msgid "Desktop startup" msgstr "Masaüstü başlangıcı" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update {version} available. Choose a Linux package." -msgstr "Masaüstü güncellemesi {version} mevcut. Bir Linux paketi seçin." - #. Modal title shown when a new desktop app update can be downloaded. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop update available" msgstr "Masaüstü güncellemesi mevcut" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format and no version is known. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update available. Choose a Linux package." -msgstr "Masaüstü güncellemesi mevcut. Bir Linux paketi seçin." - #. Desktop updater modal body. The version placeholder is the target app version; productName is the app name. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop version {version} is available. Download the installer, then run it to update {productName}." @@ -13049,11 +13039,6 @@ msgstr "Yükleyiciyi indir" msgid "Download media" msgstr "Medyayı indir" -#. Button label that downloads a selected Linux desktop update package. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Download package" -msgstr "Paketi indir" - #. Button that downloads the account recovery kit as a PDF file. #: src/features/auth/components/modals/RecoveryKitModal.tsx msgid "Download PDF" @@ -18280,11 +18265,6 @@ msgstr "Tarayıcı menünüzden yükleyin." msgid "Install the new app" msgstr "Yeni uygulamayı yükle" -#. Label for the installed desktop version in the Linux update modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Installed" -msgstr "Yüklü" - #. Desktop updater modal body. The currentVersion placeholder is the installed app version. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Installed version: {currentVersion}." @@ -19794,11 +19774,6 @@ msgstr "Linux uygulaması" msgid "Linux default" msgstr "Linux varsayılanı" -#. Select label for choosing the Linux desktop package format to download. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Linux package" -msgstr "Linux paketi" - #. Forum layout name shown inside an activity log row. #. Forum layout option that shows posts as rows. #: src/features/forum/utils/ForumMessageDescriptors.ts @@ -22430,11 +22405,6 @@ msgstr "Bu süre boyunca etkin olmayan yeni başlıklar kanal listesinde görün msgid "New users can't join right now. You can still create an account or sign in, then try this link again later." msgstr "Yeni kullanıcılar şu anda katılamıyor. Yine de bir hesap oluşturabilir veya oturum açabilir, ardından bu bağlantıyı daha sonra tekrar deneyebilirsiniz." -#. Fallback version label when the available desktop version is unknown. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "New version" -msgstr "Yeni sürüm" - #. US state name, shown next to the country name in the mature content gate. #: src/features/user/utils/UserGeo.ts msgctxt "us-state" @@ -24142,6 +24112,8 @@ msgid "Open settings" msgstr "Ayarları aç" #. Button label that opens the software center via appstream protocol. +#. Native desktop dialog button on a Flatpak install that opens the software center page for the desktop app. +#: src/features/platform/utils/DesktopLocaleBridge.ts #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Open software center" msgstr "Yazılım merkezini aç" @@ -28464,11 +28436,6 @@ msgstr "Yuvarlak stil onay kutusu" msgid "Rows per page" msgstr "Sayfa başına satır" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "RPM" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "RPM package" -msgstr "RPM paketi" - #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts msgid "Run {productName} automatically when your computer starts" @@ -33120,11 +33087,6 @@ msgstr "Paylaş'a dokunun, ardından Ana Ekrana Ekle'yi seçin." msgid "Tap to add a note" msgstr "Not eklemek için dokun" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "tar.gz" verbatim and translate only the word "archive". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "tar.gz archive" -msgstr "tar.gz arşivi" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/SharedDescriptors.ts msgid "Taskbar badge" @@ -33513,11 +33475,6 @@ msgstr "Sunucu teklif ederse bağlantı TLS'ye yükseltilir." msgid "The desktop update could not be downloaded. Try again from the update button when your connection is stable." msgstr "Masaüstü güncellemesi indirilemedi. Bağlantınız kararlı olduğunda güncelleme düğmesinden tekrar deneyin." -#. Help text below the Linux package format selector in the desktop updater modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "The downloaded file is saved to your computer. Install it with your normal package manager." -msgstr "İndirilen dosya bilgisayarınıza kaydedildi. Normal paket yöneticinizle yükleyin." - #. Activity log detail line under a deleted emoji, shown only when someone other than the person who deleted it had uploaded it. {user} is the user who uploaded the emoji, shown as a clickable user chip. #: src/features/guild/utils/guild_tabs/audit_log/AuditLogExpressionMessageMessages.ts msgid "The emoji was uploaded by {user}" @@ -36548,11 +36505,21 @@ msgstr "En fazla 4K 60 FPS" msgid "Up to 720p at 30 FPS" msgstr "30 FPS ile 720p'ye kadar" +#. Native desktop dialog body under the headline announcing a new version, shown on a Flatpak install. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} from your software center." +msgstr "Yazılım merkezinizden {appName}'i güncelleyin." + #. Tray menu item shown only while a desktop update is waiting. Clicking it installs the update. {appName} is the desktop app name (typically Fluxer). #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "Update {appName} now" msgstr "Şimdi {appName} uygulamasını güncelle" +#. Native desktop dialog body under the headline announcing a new version, shown on a Linux install managed by a package manager such as a deb, rpm or AUR package. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} with your package manager, or download the new version." +msgstr "Paket yöneticinizle {appName}'i güncelleyin veya yeni sürümü indirin." + #. Linux settings text when the installed desktop app is too old for system-wide shortcuts. {productName} is the app name. #: src/features/user/components/modals/tabs/components/SystemShortcutsSection.tsx msgid "Update {productName} to use system-wide shortcuts on Wayland." diff --git a/fluxer_app/src/features/i18n/locales/uk/messages.po b/fluxer_app/src/features/i18n/locales/uk/messages.po index bb91dd945..4b17a00a5 100644 --- a/fluxer_app/src/features/i18n/locales/uk/messages.po +++ b/fluxer_app/src/features/i18n/locales/uk/messages.po @@ -1802,6 +1802,16 @@ msgstr "Щоб зміни в рядку заголовка набули чинн msgid "{PRODUCT_NAME} needs to restart to disable Chromium's hardware acceleration. Use this only if you're troubleshooting graphics glitches or high GPU usage." msgstr "{PRODUCT_NAME} потрібно перезапустити, щоб вимкнути апаратне прискорення Chromium. Використовуйте це лише для усунення несправностей графіки або високого завантаження графічного процесора." +#. Desktop updater modal body for a Flatpak install when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it from your software center." +msgstr "{productName} {version} доступно. Оновіть його з вашого програмного центру." + +#. Desktop updater modal body for a Linux install managed by a package manager, such as a deb, rpm or AUR package, when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it with your package manager, or download the new version." +msgstr "Доступна нова версія {productName} {version}. Оновіть її за допомогою менеджера пакунків або завантажте нову версію." + #. Accessible label for the Fluxer application icon. #: src/features/ui/components/icons/FluxerIconMark.tsx msgid "{productName} application icon" @@ -2669,11 +2679,6 @@ msgstr "Корисний бот, який робить дивовижні реч msgid "a hostname, e.g. {exampleDomain}" msgstr "ім'я хоста, наприклад, {exampleDomain}" -#. Linux desktop updater modal body shown before version details and package format selector. productName is the app name. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "A Linux desktop update is available. Choose the package format for this system and {productName} will save it locally." -msgstr "Доступне оновлення для комп'ютера в Linux. Оберіть формат пакета для цієї системи, і {productName} збереже його локально." - #. Plutonium subscription card line offering a move to a lower price. {listPriceEffectiveDate} is a date already formatted and localized by code; never write a date into the translation. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx msgid "A lower price is available. Switching takes effect on <0>{listPriceEffectiveDate} and changes nothing else." @@ -11427,11 +11432,6 @@ msgstr "Звук вимкнено" msgid "Deafened by moderators" msgstr "Звук вимкнено модераторами" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "DEB" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "DEB package" -msgstr "Пакет DEB" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label in the settings dialog mobile settings view. #: src/features/app/components/dialogs/components/MobileSettingsView.tsx @@ -12095,21 +12095,11 @@ msgstr "Сповіщення на комп'ютері заблоковано. В msgid "Desktop startup" msgstr "Запуск на комп'ютері" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update {version} available. Choose a Linux package." -msgstr "Доступне оновлення для комп'ютера {version}. Виберіть пакет Linux." - #. Modal title shown when a new desktop app update can be downloaded. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop update available" msgstr "Доступне оновлення для комп'ютера" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format and no version is known. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update available. Choose a Linux package." -msgstr "Доступне оновлення для комп'ютера. Виберіть пакет Linux." - #. Desktop updater modal body. The version placeholder is the target app version; productName is the app name. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop version {version} is available. Download the installer, then run it to update {productName}." @@ -13049,11 +13039,6 @@ msgstr "Завантажити інсталятор" msgid "Download media" msgstr "Завантажити медіа" -#. Button label that downloads a selected Linux desktop update package. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Download package" -msgstr "Завантажити пакунок" - #. Button that downloads the account recovery kit as a PDF file. #: src/features/auth/components/modals/RecoveryKitModal.tsx msgid "Download PDF" @@ -18280,11 +18265,6 @@ msgstr "Встановіть його з меню браузера." msgid "Install the new app" msgstr "Встановити новий застосунок" -#. Label for the installed desktop version in the Linux update modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Installed" -msgstr "Встановлено" - #. Desktop updater modal body. The currentVersion placeholder is the installed app version. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Installed version: {currentVersion}." @@ -19794,11 +19774,6 @@ msgstr "Застосунок для Linux" msgid "Linux default" msgstr "Стандартний аудіопристрій Linux" -#. Select label for choosing the Linux desktop package format to download. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Linux package" -msgstr "Пакет для Linux" - #. Forum layout name shown inside an activity log row. #. Forum layout option that shows posts as rows. #: src/features/forum/utils/ForumMessageDescriptors.ts @@ -22430,11 +22405,6 @@ msgstr "Нові теми перестануть відображатися в msgid "New users can't join right now. You can still create an account or sign in, then try this link again later." msgstr "Нові користувачі зараз не можуть приєднатися. Ви все ще можете створити обліковий запис або ввійти, а потім скористатися цим посиланням пізніше." -#. Fallback version label when the available desktop version is unknown. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "New version" -msgstr "Нова версія" - #. US state name, shown next to the country name in the mature content gate. #: src/features/user/utils/UserGeo.ts msgctxt "us-state" @@ -24142,6 +24112,8 @@ msgid "Open settings" msgstr "Відкрити налаштування" #. Button label that opens the software center via appstream protocol. +#. Native desktop dialog button on a Flatpak install that opens the software center page for the desktop app. +#: src/features/platform/utils/DesktopLocaleBridge.ts #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Open software center" msgstr "Відкрити центр програм" @@ -28464,11 +28436,6 @@ msgstr "Круглий прапорець" msgid "Rows per page" msgstr "Рядків на сторінку" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "RPM" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "RPM package" -msgstr "Пакет RPM" - #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts msgid "Run {productName} automatically when your computer starts" @@ -33120,11 +33087,6 @@ msgstr "Торкніться «Поширити», а потім «На Поча msgid "Tap to add a note" msgstr "Натисніть, щоб додати примітку" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "tar.gz" verbatim and translate only the word "archive". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "tar.gz archive" -msgstr "Архів tar.gz" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/SharedDescriptors.ts msgid "Taskbar badge" @@ -33513,11 +33475,6 @@ msgstr "З'єднання оновлюється до TLS лише тоді, к msgid "The desktop update could not be downloaded. Try again from the update button when your connection is stable." msgstr "Не вдалося завантажити оновлення для комп'ютера. Спробуйте ще раз за допомогою кнопки оновлення, коли ваше з'єднання буде стабільним." -#. Help text below the Linux package format selector in the desktop updater modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "The downloaded file is saved to your computer. Install it with your normal package manager." -msgstr "Завантажений файл збережено на вашому комп'ютері. Установіть його за допомогою звичайного менеджера пакунків." - #. Activity log detail line under a deleted emoji, shown only when someone other than the person who deleted it had uploaded it. {user} is the user who uploaded the emoji, shown as a clickable user chip. #: src/features/guild/utils/guild_tabs/audit_log/AuditLogExpressionMessageMessages.ts msgid "The emoji was uploaded by {user}" @@ -36548,11 +36505,21 @@ msgstr "До 4K при 60 FPS" msgid "Up to 720p at 30 FPS" msgstr "До 720p зі швидкістю 30 кадрів/сек" +#. Native desktop dialog body under the headline announcing a new version, shown on a Flatpak install. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} from your software center." +msgstr "Оновіть {appName} у своєму центрі програмного забезпечення." + #. Tray menu item shown only while a desktop update is waiting. Clicking it installs the update. {appName} is the desktop app name (typically Fluxer). #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "Update {appName} now" msgstr "Оновити {appName} зараз" +#. Native desktop dialog body under the headline announcing a new version, shown on a Linux install managed by a package manager such as a deb, rpm or AUR package. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} with your package manager, or download the new version." +msgstr "Оновіть {appName} за допомогою менеджера пакунків або завантажте нову версію." + #. Linux settings text when the installed desktop app is too old for system-wide shortcuts. {productName} is the app name. #: src/features/user/components/modals/tabs/components/SystemShortcutsSection.tsx msgid "Update {productName} to use system-wide shortcuts on Wayland." diff --git a/fluxer_app/src/features/i18n/locales/vi/messages.po b/fluxer_app/src/features/i18n/locales/vi/messages.po index 2643d98c7..0503a32b5 100644 --- a/fluxer_app/src/features/i18n/locales/vi/messages.po +++ b/fluxer_app/src/features/i18n/locales/vi/messages.po @@ -1802,6 +1802,16 @@ msgstr "{PRODUCT_NAME} cần khởi động lại để thay đổi về thanh t msgid "{PRODUCT_NAME} needs to restart to disable Chromium's hardware acceleration. Use this only if you're troubleshooting graphics glitches or high GPU usage." msgstr "{PRODUCT_NAME} cần khởi động lại để tắt tính năng tăng tốc phần cứng của Chromium. Chỉ sử dụng tùy chọn này nếu bạn đang khắc phục sự cố đồ họa hoặc mức sử dụng GPU cao." +#. Desktop updater modal body for a Flatpak install when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it from your software center." +msgstr "{productName} phiên bản {version} đã có sẵn. Hãy cập nhật từ trung tâm phần mềm của bạn." + +#. Desktop updater modal body for a Linux install managed by a package manager, such as a deb, rpm or AUR package, when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it with your package manager, or download the new version." +msgstr "{productName} {version} đã có sẵn. Hãy cập nhật bằng trình quản lý gói của bạn hoặc tải xuống phiên bản mới." + #. Accessible label for the Fluxer application icon. #: src/features/ui/components/icons/FluxerIconMark.tsx msgid "{productName} application icon" @@ -2669,11 +2679,6 @@ msgstr "Một bot hữu ích có thể làm được nhiều điều tuyệt v msgid "a hostname, e.g. {exampleDomain}" msgstr "một tên máy chủ, ví dụ: {exampleDomain}" -#. Linux desktop updater modal body shown before version details and package format selector. productName is the app name. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "A Linux desktop update is available. Choose the package format for this system and {productName} will save it locally." -msgstr "Đã có bản cập nhật cho ứng dụng máy tính trên Linux. Chọn định dạng gói cho hệ thống này và {productName} sẽ lưu về máy." - #. Plutonium subscription card line offering a move to a lower price. {listPriceEffectiveDate} is a date already formatted and localized by code; never write a date into the translation. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx msgid "A lower price is available. Switching takes effect on <0>{listPriceEffectiveDate} and changes nothing else." @@ -11427,11 +11432,6 @@ msgstr "Đã tắt tiếng nghe" msgid "Deafened by moderators" msgstr "Đã bị người điều hành tắt tiếng nghe" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "DEB" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "DEB package" -msgstr "Gói DEB" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label in the settings dialog mobile settings view. #: src/features/app/components/dialogs/components/MobileSettingsView.tsx @@ -12095,21 +12095,11 @@ msgstr "Thông báo trên máy tính đã bị chặn. Bạn có thể bật l msgid "Desktop startup" msgstr "Khởi động cùng máy tính" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update {version} available. Choose a Linux package." -msgstr "Có bản cập nhật {version} cho ứng dụng máy tính. Chọn gói Linux." - #. Modal title shown when a new desktop app update can be downloaded. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop update available" msgstr "Có bản cập nhật cho ứng dụng máy tính" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format and no version is known. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update available. Choose a Linux package." -msgstr "Có bản cập nhật cho ứng dụng máy tính. Chọn gói Linux." - #. Desktop updater modal body. The version placeholder is the target app version; productName is the app name. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop version {version} is available. Download the installer, then run it to update {productName}." @@ -13049,11 +13039,6 @@ msgstr "Tải trình cài đặt" msgid "Download media" msgstr "Tải phương tiện" -#. Button label that downloads a selected Linux desktop update package. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Download package" -msgstr "Tải gói" - #. Button that downloads the account recovery kit as a PDF file. #: src/features/auth/components/modals/RecoveryKitModal.tsx msgid "Download PDF" @@ -18280,11 +18265,6 @@ msgstr "Cài đặt từ menu trình duyệt của bạn." msgid "Install the new app" msgstr "Cài đặt ứng dụng mới" -#. Label for the installed desktop version in the Linux update modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Installed" -msgstr "Đã cài đặt" - #. Desktop updater modal body. The currentVersion placeholder is the installed app version. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Installed version: {currentVersion}." @@ -19794,11 +19774,6 @@ msgstr "Ứng dụng Linux" msgid "Linux default" msgstr "Mặc định của Linux" -#. Select label for choosing the Linux desktop package format to download. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Linux package" -msgstr "Gói Linux" - #. Forum layout name shown inside an activity log row. #. Forum layout option that shows posts as rows. #: src/features/forum/utils/ForumMessageDescriptors.ts @@ -22430,11 +22405,6 @@ msgstr "Luồng mới sẽ ngừng hiển thị trong danh sách kênh sau kho msgid "New users can't join right now. You can still create an account or sign in, then try this link again later." msgstr "Người dùng mới hiện chưa thể tham gia. Bạn vẫn có thể tạo tài khoản hoặc đăng nhập, sau đó thử lại liên kết này sau." -#. Fallback version label when the available desktop version is unknown. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "New version" -msgstr "Phiên bản mới" - #. US state name, shown next to the country name in the mature content gate. #: src/features/user/utils/UserGeo.ts msgctxt "us-state" @@ -24142,6 +24112,8 @@ msgid "Open settings" msgstr "Mở cài đặt" #. Button label that opens the software center via appstream protocol. +#. Native desktop dialog button on a Flatpak install that opens the software center page for the desktop app. +#: src/features/platform/utils/DesktopLocaleBridge.ts #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Open software center" msgstr "Mở trung tâm phần mềm" @@ -28464,11 +28436,6 @@ msgstr "Ô đánh dấu kiểu tròn" msgid "Rows per page" msgstr "Số hàng mỗi trang" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "RPM" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "RPM package" -msgstr "Gói RPM" - #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts msgid "Run {productName} automatically when your computer starts" @@ -33120,11 +33087,6 @@ msgstr "Chạm vào Chia sẻ, sau đó chọn Thêm vào MH chính." msgid "Tap to add a note" msgstr "Nhấn để thêm ghi chú" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "tar.gz" verbatim and translate only the word "archive". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "tar.gz archive" -msgstr "Tệp nén tar.gz" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/SharedDescriptors.ts msgid "Taskbar badge" @@ -33513,11 +33475,6 @@ msgstr "Kết nối sẽ nâng cấp lên TLS chỉ khi máy chủ cung cấp t msgid "The desktop update could not be downloaded. Try again from the update button when your connection is stable." msgstr "Không thể tải bản cập nhật cho máy tính. Hãy thử lại bằng nút cập nhật khi kết nối của bạn ổn định." -#. Help text below the Linux package format selector in the desktop updater modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "The downloaded file is saved to your computer. Install it with your normal package manager." -msgstr "Tệp đã tải xuống được lưu vào máy tính của bạn. Cài đặt bằng trình quản lý gói thông thường của bạn." - #. Activity log detail line under a deleted emoji, shown only when someone other than the person who deleted it had uploaded it. {user} is the user who uploaded the emoji, shown as a clickable user chip. #: src/features/guild/utils/guild_tabs/audit_log/AuditLogExpressionMessageMessages.ts msgid "The emoji was uploaded by {user}" @@ -36548,11 +36505,21 @@ msgstr "Lên đến 4K ở 60 FPS" msgid "Up to 720p at 30 FPS" msgstr "Lên đến 720p ở 30 FPS" +#. Native desktop dialog body under the headline announcing a new version, shown on a Flatpak install. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} from your software center." +msgstr "Cập nhật {appName} từ trung tâm phần mềm của bạn." + #. Tray menu item shown only while a desktop update is waiting. Clicking it installs the update. {appName} is the desktop app name (typically Fluxer). #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "Update {appName} now" msgstr "Cập nhật {appName} ngay bây giờ" +#. Native desktop dialog body under the headline announcing a new version, shown on a Linux install managed by a package manager such as a deb, rpm or AUR package. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} with your package manager, or download the new version." +msgstr "Cập nhật {appName} bằng trình quản lý gói của bạn hoặc tải xuống phiên bản mới." + #. Linux settings text when the installed desktop app is too old for system-wide shortcuts. {productName} is the app name. #: src/features/user/components/modals/tabs/components/SystemShortcutsSection.tsx msgid "Update {productName} to use system-wide shortcuts on Wayland." diff --git a/fluxer_app/src/features/i18n/locales/zh-CN/messages.po b/fluxer_app/src/features/i18n/locales/zh-CN/messages.po index efd802110..1abb46dab 100644 --- a/fluxer_app/src/features/i18n/locales/zh-CN/messages.po +++ b/fluxer_app/src/features/i18n/locales/zh-CN/messages.po @@ -1802,6 +1802,16 @@ msgstr "{PRODUCT_NAME} 需要重启才能使标题栏更改生效。" msgid "{PRODUCT_NAME} needs to restart to disable Chromium's hardware acceleration. Use this only if you're troubleshooting graphics glitches or high GPU usage." msgstr "{PRODUCT_NAME} 需要重启才能禁用 Chromium 的硬件加速。仅在排查图形故障或 GPU 占用过高时使用此功能。" +#. Desktop updater modal body for a Flatpak install when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it from your software center." +msgstr "{productName} {version} 可用。请从您的软件中心更新。" + +#. Desktop updater modal body for a Linux install managed by a package manager, such as a deb, rpm or AUR package, when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it with your package manager, or download the new version." +msgstr "{productName} {version} 已发布。请使用你的包管理器进行更新,或下载新版本。" + #. Accessible label for the Fluxer application icon. #: src/features/ui/components/icons/FluxerIconMark.tsx msgid "{productName} application icon" @@ -2669,11 +2679,6 @@ msgstr "一个能做很多厉害事情的实用机器人!" msgid "a hostname, e.g. {exampleDomain}" msgstr "主机名,例如 {exampleDomain}" -#. Linux desktop updater modal body shown before version details and package format selector. productName is the app name. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "A Linux desktop update is available. Choose the package format for this system and {productName} will save it locally." -msgstr "Linux 桌面端有可用更新。请选择适用于此系统的软件包格式,{productName} 会将其保存到本地。" - #. Plutonium subscription card line offering a move to a lower price. {listPriceEffectiveDate} is a date already formatted and localized by code; never write a date into the translation. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx msgid "A lower price is available. Switching takes effect on <0>{listPriceEffectiveDate} and changes nothing else." @@ -11427,11 +11432,6 @@ msgstr "已拒听" msgid "Deafened by moderators" msgstr "被管理员设为拒听" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "DEB" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "DEB package" -msgstr "DEB 软件包" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label in the settings dialog mobile settings view. #: src/features/app/components/dialogs/components/MobileSettingsView.tsx @@ -12095,21 +12095,11 @@ msgstr "桌面通知已被屏蔽。你之后可以在浏览器设置、用户设 msgid "Desktop startup" msgstr "桌面端启动" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update {version} available. Choose a Linux package." -msgstr "桌面版更新 {version} 已可用。请选择一个 Linux 软件包。" - #. Modal title shown when a new desktop app update can be downloaded. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop update available" msgstr "有可用的桌面版更新" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format and no version is known. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update available. Choose a Linux package." -msgstr "有可用的桌面版更新。请选择一个 Linux 软件包。" - #. Desktop updater modal body. The version placeholder is the target app version; productName is the app name. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop version {version} is available. Download the installer, then run it to update {productName}." @@ -13049,11 +13039,6 @@ msgstr "下载安装程序" msgid "Download media" msgstr "下载媒体文件" -#. Button label that downloads a selected Linux desktop update package. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Download package" -msgstr "下载安装包" - #. Button that downloads the account recovery kit as a PDF file. #: src/features/auth/components/modals/RecoveryKitModal.tsx msgid "Download PDF" @@ -18280,11 +18265,6 @@ msgstr "从浏览器菜单中安装。" msgid "Install the new app" msgstr "安装新应用" -#. Label for the installed desktop version in the Linux update modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Installed" -msgstr "已安装" - #. Desktop updater modal body. The currentVersion placeholder is the installed app version. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Installed version: {currentVersion}." @@ -19794,11 +19774,6 @@ msgstr "Linux 应用" msgid "Linux default" msgstr "Linux 默认" -#. Select label for choosing the Linux desktop package format to download. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Linux package" -msgstr "Linux 安装包" - #. Forum layout name shown inside an activity log row. #. Forum layout option that shows posts as rows. #: src/features/forum/utils/ForumMessageDescriptors.ts @@ -22430,11 +22405,6 @@ msgstr "新创建的帖子在不活跃一段时间后将不再显示在频道列 msgid "New users can't join right now. You can still create an account or sign in, then try this link again later." msgstr "新用户暂时无法加入。你仍然可以创建账号或登录,稍后再次尝试此链接。" -#. Fallback version label when the available desktop version is unknown. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "New version" -msgstr "新版本" - #. US state name, shown next to the country name in the mature content gate. #: src/features/user/utils/UserGeo.ts msgctxt "us-state" @@ -24142,6 +24112,8 @@ msgid "Open settings" msgstr "打开设置" #. Button label that opens the software center via appstream protocol. +#. Native desktop dialog button on a Flatpak install that opens the software center page for the desktop app. +#: src/features/platform/utils/DesktopLocaleBridge.ts #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Open software center" msgstr "打开软件中心" @@ -28464,11 +28436,6 @@ msgstr "圆形样式复选框" msgid "Rows per page" msgstr "每页行数" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "RPM" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "RPM package" -msgstr "RPM 软件包" - #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts msgid "Run {productName} automatically when your computer starts" @@ -33120,11 +33087,6 @@ msgstr "轻点“共享”,然后轻点“添加到主屏幕”。" msgid "Tap to add a note" msgstr "轻触添加备注" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "tar.gz" verbatim and translate only the word "archive". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "tar.gz archive" -msgstr "tar.gz 压缩包" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/SharedDescriptors.ts msgid "Taskbar badge" @@ -33513,11 +33475,6 @@ msgstr "如果服务器提供该选项,连接将升级到 TLS。" msgid "The desktop update could not be downloaded. Try again from the update button when your connection is stable." msgstr "桌面更新下载失败。请在网络连接稳定后通过更新按钮重试。" -#. Help text below the Linux package format selector in the desktop updater modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "The downloaded file is saved to your computer. Install it with your normal package manager." -msgstr "下载的文件已保存到你的电脑。请使用你的常规软件包管理器进行安装。" - #. Activity log detail line under a deleted emoji, shown only when someone other than the person who deleted it had uploaded it. {user} is the user who uploaded the emoji, shown as a clickable user chip. #: src/features/guild/utils/guild_tabs/audit_log/AuditLogExpressionMessageMessages.ts msgid "The emoji was uploaded by {user}" @@ -36548,11 +36505,21 @@ msgstr "最高 4K 60 帧" msgid "Up to 720p at 30 FPS" msgstr "最高 720p,30 帧/秒" +#. Native desktop dialog body under the headline announcing a new version, shown on a Flatpak install. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} from your software center." +msgstr "从你的软件中心更新 {appName}。" + #. Tray menu item shown only while a desktop update is waiting. Clicking it installs the update. {appName} is the desktop app name (typically Fluxer). #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "Update {appName} now" msgstr "立即更新 {appName}" +#. Native desktop dialog body under the headline announcing a new version, shown on a Linux install managed by a package manager such as a deb, rpm or AUR package. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} with your package manager, or download the new version." +msgstr "使用你的包管理器更新 {appName},或下载新版本。" + #. Linux settings text when the installed desktop app is too old for system-wide shortcuts. {productName} is the app name. #: src/features/user/components/modals/tabs/components/SystemShortcutsSection.tsx msgid "Update {productName} to use system-wide shortcuts on Wayland." diff --git a/fluxer_app/src/features/i18n/locales/zh-TW/messages.po b/fluxer_app/src/features/i18n/locales/zh-TW/messages.po index 3273318b8..956f4f582 100644 --- a/fluxer_app/src/features/i18n/locales/zh-TW/messages.po +++ b/fluxer_app/src/features/i18n/locales/zh-TW/messages.po @@ -1802,6 +1802,16 @@ msgstr "{PRODUCT_NAME} 需要重新啟動才能套用標題列的變更。" msgid "{PRODUCT_NAME} needs to restart to disable Chromium's hardware acceleration. Use this only if you're troubleshooting graphics glitches or high GPU usage." msgstr "{PRODUCT_NAME} 需要重新啟動才能停用 Chromium 的硬體加速。只有在排解顯示異常或 GPU 使用率過高的問題時才使用。" +#. Desktop updater modal body for a Flatpak install when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it from your software center." +msgstr "{productName} {version} 已推出。請從你的軟體中心進行更新。" + +#. Desktop updater modal body for a Linux install managed by a package manager, such as a deb, rpm or AUR package, when a newer desktop version exists. The version placeholder is the new app version and productName is the app name. +#: src/features/updater/commands/UpdaterModalCommands.tsx +msgid "{productName} {version} is available. Update it with your package manager, or download the new version." +msgstr "{productName} {version} 已推出。請使用您的套件管理程式進行更新,或下載新版本。" + #. Accessible label for the Fluxer application icon. #: src/features/ui/components/icons/FluxerIconMark.tsx msgid "{productName} application icon" @@ -2669,11 +2679,6 @@ msgstr "一個能做許多驚人事情的實用機器人!" msgid "a hostname, e.g. {exampleDomain}" msgstr "主機名稱,例如 {exampleDomain}" -#. Linux desktop updater modal body shown before version details and package format selector. productName is the app name. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "A Linux desktop update is available. Choose the package format for this system and {productName} will save it locally." -msgstr "有可用的 Linux 桌面版更新。請選擇此系統適用的套件格式,{productName} 會將檔案儲存在本機。" - #. Plutonium subscription card line offering a move to a lower price. {listPriceEffectiveDate} is a date already formatted and localized by code; never write a date into the translation. #: src/features/app/components/dialogs/components/plutonium/SubscriptionCard.tsx msgid "A lower price is available. Switching takes effect on <0>{listPriceEffectiveDate} and changes nothing else." @@ -11427,11 +11432,6 @@ msgstr "已耳機靜音" msgid "Deafened by moderators" msgstr "遭管理員設為耳機靜音" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "DEB" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "DEB package" -msgstr "DEB 套件" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #. Short label in the settings dialog mobile settings view. #: src/features/app/components/dialogs/components/MobileSettingsView.tsx @@ -12095,21 +12095,11 @@ msgstr "桌面通知已被封鎖。您稍後可以在瀏覽器設定、使用者 msgid "Desktop startup" msgstr "桌面版啟動" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update {version} available. Choose a Linux package." -msgstr "桌面版更新 {version} 已推出。請選擇 Linux 套件。" - #. Modal title shown when a new desktop app update can be downloaded. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop update available" msgstr "有可用的桌面版更新" -#. Tooltip on the updater icon for a Linux desktop update that requires choosing a package format and no version is known. -#: src/features/channel/components/channel_header_components/UpdaterIcon.tsx -msgid "Desktop update available. Choose a Linux package." -msgstr "有可用的桌面版更新。請選擇 Linux 套件。" - #. Desktop updater modal body. The version placeholder is the target app version; productName is the app name. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Desktop version {version} is available. Download the installer, then run it to update {productName}." @@ -13049,11 +13039,6 @@ msgstr "下載安裝程式" msgid "Download media" msgstr "下載媒體" -#. Button label that downloads a selected Linux desktop update package. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Download package" -msgstr "下載套件" - #. Button that downloads the account recovery kit as a PDF file. #: src/features/auth/components/modals/RecoveryKitModal.tsx msgid "Download PDF" @@ -18280,11 +18265,6 @@ msgstr "從瀏覽器選單安裝。" msgid "Install the new app" msgstr "安裝新版應用程式" -#. Label for the installed desktop version in the Linux update modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Installed" -msgstr "已安裝" - #. Desktop updater modal body. The currentVersion placeholder is the installed app version. #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Installed version: {currentVersion}." @@ -19794,11 +19774,6 @@ msgstr "Linux 應用程式" msgid "Linux default" msgstr "Linux 預設值" -#. Select label for choosing the Linux desktop package format to download. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "Linux package" -msgstr "Linux 套件" - #. Forum layout name shown inside an activity log row. #. Forum layout option that shows posts as rows. #: src/features/forum/utils/ForumMessageDescriptors.ts @@ -22430,11 +22405,6 @@ msgstr "閒置一段時間後,新討論串將不再顯示於頻道列表中。 msgid "New users can't join right now. You can still create an account or sign in, then try this link again later." msgstr "新使用者目前無法加入。您仍然可以建立帳號或登入,稍後再試一次此連結。" -#. Fallback version label when the available desktop version is unknown. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "New version" -msgstr "新版本" - #. US state name, shown next to the country name in the mature content gate. #: src/features/user/utils/UserGeo.ts msgctxt "us-state" @@ -24142,6 +24112,8 @@ msgid "Open settings" msgstr "開啟設定" #. Button label that opens the software center via appstream protocol. +#. Native desktop dialog button on a Flatpak install that opens the software center page for the desktop app. +#: src/features/platform/utils/DesktopLocaleBridge.ts #: src/features/updater/commands/UpdaterModalCommands.tsx msgid "Open software center" msgstr "開啟軟體中心" @@ -28464,11 +28436,6 @@ msgstr "圓形樣式核取方塊" msgid "Rows per page" msgstr "每頁列數" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "RPM" verbatim and translate only the word "package". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "RPM package" -msgstr "RPM 套件" - #. Settings search entry description. One-line summary of what the settings search entry controls. #: src/features/user/components/settings_utils/search_index/DesktopSettingsIndex.ts msgid "Run {productName} automatically when your computer starts" @@ -33120,11 +33087,6 @@ msgstr "點一下「分享」,然後點一下「加入主畫面」。" msgid "Tap to add a note" msgstr "輕觸以新增記事" -#. Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "tar.gz" verbatim and translate only the word "archive". -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "tar.gz archive" -msgstr "tar.gz 封存檔" - #. Settings search synonym. Used to match this term when the user types it in the settings search bar. #: src/features/user/components/settings_utils/section_registry/SharedDescriptors.ts msgid "Taskbar badge" @@ -33513,11 +33475,6 @@ msgstr "如果伺服器提供,連線才會升級至 TLS。" msgid "The desktop update could not be downloaded. Try again from the update button when your connection is stable." msgstr "無法下載桌面版更新。請在網路連線穩定後,從更新按鈕重試。" -#. Help text below the Linux package format selector in the desktop updater modal. -#: src/features/updater/commands/UpdaterModalCommands.tsx -msgid "The downloaded file is saved to your computer. Install it with your normal package manager." -msgstr "下載的檔案已儲存到您的電腦。請使用您慣用的套件管理員進行安裝。" - #. Activity log detail line under a deleted emoji, shown only when someone other than the person who deleted it had uploaded it. {user} is the user who uploaded the emoji, shown as a clickable user chip. #: src/features/guild/utils/guild_tabs/audit_log/AuditLogExpressionMessageMessages.ts msgid "The emoji was uploaded by {user}" @@ -36548,11 +36505,21 @@ msgstr "最高 4K 60 FPS" msgid "Up to 720p at 30 FPS" msgstr "最高 720p 30 FPS" +#. Native desktop dialog body under the headline announcing a new version, shown on a Flatpak install. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} from your software center." +msgstr "從您的軟體中心更新 {appName}。" + #. Tray menu item shown only while a desktop update is waiting. Clicking it installs the update. {appName} is the desktop app name (typically Fluxer). #: src/features/platform/utils/DesktopLocaleBridge.ts msgid "Update {appName} now" msgstr "立即更新 {appName}" +#. Native desktop dialog body under the headline announcing a new version, shown on a Linux install managed by a package manager such as a deb, rpm or AUR package. {appName} is the desktop app name (typically Fluxer). +#: src/features/platform/utils/DesktopLocaleBridge.ts +msgid "Update {appName} with your package manager, or download the new version." +msgstr "使用你的套件管理程式更新 {appName},或下載新版本。" + #. Linux settings text when the installed desktop app is too old for system-wide shortcuts. {productName} is the app name. #: src/features/user/components/modals/tabs/components/SystemShortcutsSection.tsx msgid "Update {productName} to use system-wide shortcuts on Wayland." diff --git a/fluxer_app/src/features/platform/utils/DesktopLocaleBridge.ts b/fluxer_app/src/features/platform/utils/DesktopLocaleBridge.ts index 83e7c19fa..660709bca 100644 --- a/fluxer_app/src/features/platform/utils/DesktopLocaleBridge.ts +++ b/fluxer_app/src/features/platform/utils/DesktopLocaleBridge.ts @@ -275,6 +275,20 @@ const UPDATE_MANUAL_DETAIL_DESCRIPTOR = msg({ message: 'Download the new version to update {appName}.', comment: 'Native desktop dialog body under the headline announcing a new version that must be downloaded by hand.', }); +const UPDATE_MANAGED_DETAIL_DESCRIPTOR = msg({ + message: 'Update {appName} with your package manager, or download the new version.', + comment: + 'Native desktop dialog body under the headline announcing a new version, shown on a Linux install managed by a package manager such as a deb, rpm or AUR package. {appName} is the desktop app name (typically Fluxer).', +}); +const UPDATE_FLATPAK_DETAIL_DESCRIPTOR = msg({ + message: 'Update {appName} from your software center.', + comment: + 'Native desktop dialog body under the headline announcing a new version, shown on a Flatpak install. {appName} is the desktop app name (typically Fluxer).', +}); +const UPDATE_OPEN_SOFTWARE_CENTER_DESCRIPTOR = msg({ + message: 'Open software center', + comment: 'Native desktop dialog button on a Flatpak install that opens the software center page for the desktop app.', +}); const UPDATE_UP_TO_DATE_MESSAGE_DESCRIPTOR = msg({ message: '{appName} is up to date.', comment: 'Native desktop dialog headline after a manual update check found nothing newer.', @@ -513,6 +527,9 @@ const NATIVE_MESSAGES: Record = { 'desktop.update.download': UPDATE_DOWNLOAD_DESCRIPTOR, 'desktop.update.manualMessage': UPDATE_MANUAL_MESSAGE_DESCRIPTOR, 'desktop.update.manualDetail': UPDATE_MANUAL_DETAIL_DESCRIPTOR, + 'desktop.update.managedDetail': UPDATE_MANAGED_DETAIL_DESCRIPTOR, + 'desktop.update.flatpakDetail': UPDATE_FLATPAK_DETAIL_DESCRIPTOR, + 'desktop.update.openSoftwareCenter': UPDATE_OPEN_SOFTWARE_CENTER_DESCRIPTOR, 'desktop.update.upToDateMessage': UPDATE_UP_TO_DATE_MESSAGE_DESCRIPTOR, 'desktop.update.upToDateDetail': UPDATE_UP_TO_DATE_DETAIL_DESCRIPTOR, 'desktop.update.checkFailedMessage': UPDATE_CHECK_FAILED_MESSAGE_DESCRIPTOR, diff --git a/fluxer_app/src/features/updater/commands/UpdaterModalCommands.module.css b/fluxer_app/src/features/updater/commands/UpdaterModalCommands.module.css deleted file mode 100644 index 9cdae81c1..000000000 --- a/fluxer_app/src/features/updater/commands/UpdaterModalCommands.module.css +++ /dev/null @@ -1,43 +0,0 @@ -/* SPDX-License-Identifier: AGPL-3.0-or-later */ - -.manualUpdateBody { - display: flex; - flex-direction: column; - gap: 0.875rem; -} - -.manualUpdateIntro { - margin: 0; -} - -.versionList { - display: grid; - grid-template-columns: max-content minmax(0, 1fr); - gap: 0.375rem 0.75rem; - margin: 0; - color: var(--text-secondary); - font-size: 0.875rem; -} - -.versionLabel { - color: var(--text-primary-muted); - font-weight: 600; -} - -.versionValue { - margin: 0; - min-width: 0; - overflow-wrap: anywhere; - color: var(--text-primary); -} - -.packageSelect { - min-width: 0; -} - -.manualUpdateHelp { - margin: -0.125rem 0 0; - color: var(--text-primary-muted); - font-size: 0.8125rem; - line-height: 1.4; -} diff --git a/fluxer_app/src/features/updater/commands/UpdaterModalCommands.tsx b/fluxer_app/src/features/updater/commands/UpdaterModalCommands.tsx index 3ff0a3e17..72a6dfcc6 100644 --- a/fluxer_app/src/features/updater/commands/UpdaterModalCommands.tsx +++ b/fluxer_app/src/features/updater/commands/UpdaterModalCommands.tsx @@ -3,20 +3,12 @@ import {ConfirmModal} from '@app/features/app/components/dialogs/ConfirmModal'; import {DESKTOP_DOWNLOAD_URL} from '@app/features/app/config/I18nDisplayConstants'; import RuntimeConfig from '@app/features/app/state/RuntimeConfig'; -import { - AVAILABLE_DESCRIPTOR as AVAILABLE_VERSION_LABEL_DESCRIPTOR, - CLOSE_DESCRIPTOR, - UNKNOWN_DESCRIPTOR as UNKNOWN_VERSION_DESCRIPTOR, -} from '@app/features/i18n/utils/CommonMessageDescriptors'; +import {CLOSE_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors'; import * as ModalCommands from '@app/features/ui/commands/ModalCommands'; import {modal} from '@app/features/ui/commands/ModalCommands'; -import {Combobox} from '@app/features/ui/components/form/FormCombobox'; import {isCanaryDesktop, openExternalUrl} from '@app/features/ui/utils/NativeUtils'; -import styles from '@app/features/updater/commands/UpdaterModalCommands.module.css'; -import type {UpdaterDownloadFormat, UpdaterDownloadOption} from '@app/types/electron.d'; -import {i18n, type MessageDescriptor} from '@lingui/core'; +import {i18n} from '@lingui/core'; import {msg} from '@lingui/core/macro'; -import {useMemo, useState} from 'react'; const DESKTOP_UPDATE_AVAILABLE_DESCRIPTOR = msg({ message: 'Desktop update available', @@ -35,10 +27,6 @@ const DOWNLOAD_INSTALLER_DESCRIPTOR = msg({ message: 'Download installer', comment: 'Button label that downloads a desktop update installer.', }); -const DOWNLOAD_PACKAGE_DESCRIPTOR = msg({ - message: 'Download package', - comment: 'Button label that downloads a selected Linux desktop update package.', -}); const DESKTOP_UPDATE_RESTARTS_APP_DESCRIPTOR = msg({ message: '{productName} closes, installs the update and opens again.', comment: @@ -90,6 +78,16 @@ const SYSTEM_MANAGED_UPDATE_BODY_DESCRIPTOR = msg({ message: 'This install is managed by your system. Update {productName} from your software center or package manager.', comment: 'Desktop updater modal body for managed package builds such as Flatpak. productName is the app name.', }); +const MANAGED_SHELL_UPDATE_FLATPAK_BODY_DESCRIPTOR = msg({ + message: '{productName} {version} is available. Update it from your software center.', + comment: + 'Desktop updater modal body for a Flatpak install when a newer desktop version exists. The version placeholder is the new app version and productName is the app name.', +}); +const MANAGED_SHELL_UPDATE_PACKAGE_BODY_DESCRIPTOR = msg({ + message: '{productName} {version} is available. Update it with your package manager, or download the new version.', + comment: + 'Desktop updater modal body for a Linux install managed by a package manager, such as a deb, rpm or AUR package, when a newer desktop version exists. The version placeholder is the new app version and productName is the app name.', +}); const OPEN_SOFTWARE_CENTER_DESCRIPTOR = msg({ message: 'Open software center', comment: 'Button label that opens the software center via appstream protocol.', @@ -115,56 +113,25 @@ const DESKTOP_UPDATE_DOWNLOAD_FAILED_BODY_DESCRIPTOR = msg({ 'The desktop update could not be downloaded. Try again from the update button when your connection is stable.', comment: 'Modal body shown when a desktop update download fails.', }); -const LINUX_PACKAGE_UPDATE_INTRO_DESCRIPTOR = msg({ - message: - 'A Linux desktop update is available. Choose the package format for this system and {productName} will save it locally.', - comment: - 'Linux desktop updater modal body shown before version details and package format selector. productName is the app name.', -}); -const INSTALLED_VERSION_LABEL_DESCRIPTOR = msg({ - message: 'Installed', - comment: 'Label for the installed desktop version in the Linux update modal.', -}); - -const NEW_VERSION_DESCRIPTOR = msg({ - message: 'New version', - comment: 'Fallback version label when the available desktop version is unknown.', -}); -const LINUX_PACKAGE_LABEL_DESCRIPTOR = msg({ - message: 'Linux package', - comment: 'Select label for choosing the Linux desktop package format to download.', -}); -const PACKAGE_HELP_DESCRIPTOR = msg({ - message: 'The downloaded file is saved to your computer. Install it with your normal package manager.', - comment: 'Help text below the Linux package format selector in the desktop updater modal.', -}); -const DEB_PACKAGE_FORMAT_DESCRIPTOR = msg({ - message: 'DEB package', - comment: - 'Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "DEB" verbatim and translate only the word "package".', -}); -const RPM_PACKAGE_FORMAT_DESCRIPTOR = msg({ - message: 'RPM package', - comment: - 'Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "RPM" verbatim and translate only the word "package".', -}); -const TAR_GZ_ARCHIVE_FORMAT_DESCRIPTOR = msg({ - message: 'tar.gz archive', - comment: - 'Option in the Linux package format dropdown of the desktop updater modal. Keep the format token "tar.gz" verbatim and translate only the word "archive".', -}); - -const DOWNLOAD_FORMAT_LABEL_DESCRIPTORS: Partial> = { - deb: DEB_PACKAGE_FORMAT_DESCRIPTOR, - rpm: RPM_PACKAGE_FORMAT_DESCRIPTOR, - tar_gz: TAR_GZ_ARCHIVE_FORMAT_DESCRIPTOR, -}; - const UPDATE_AVAILABLE_KEY = 'updater-available'; const UP_TO_DATE_KEY = 'updater-up-to-date'; const UNSUPPORTED_KEY = 'updater-unsupported'; const ERROR_KEY = 'updater-error'; +function openSoftwareCenter(appId: string = isCanaryDesktop() ? 'app.fluxer.FluxerCanary' : 'app.fluxer.Fluxer'): void { + void openExternalUrl(`appstream://${appId}`); +} + +function managedShellUpdateBody(version: string | null, flatpakAppId: string | null): string { + const productName = RuntimeConfig.productName; + if (version == null) { + return i18n._(SYSTEM_MANAGED_UPDATE_BODY_DESCRIPTOR, {productName}); + } + return flatpakAppId != null + ? i18n._(MANAGED_SHELL_UPDATE_FLATPAK_BODY_DESCRIPTOR, {productName, version}) + : i18n._(MANAGED_SHELL_UPDATE_PACKAGE_BODY_DESCRIPTOR, {productName, version}); +} + export function pushUpdateAvailableModal(version: string | null, onDownload: () => void | Promise): void { ModalCommands.pushWithKey( modal(() => ( @@ -187,6 +154,36 @@ export function pushUpdateAvailableModal(version: string | null, onDownload: () ); } +export function pushManagedShellUpdateModal({ + version, + flatpakAppId, +}: { + version: string | null; + flatpakAppId: string | null; +}): void { + ModalCommands.pushWithKey( + modal(() => ( + { + if (flatpakAppId != null) { + openSoftwareCenter(flatpakAppId); + return; + } + void openExternalUrl(DESKTOP_DOWNLOAD_URL); + }} + data-flx="updater.updater-modal-commands.push-managed-shell-update-modal.confirm-modal" + /> + )), + UPDATE_AVAILABLE_KEY, + ); +} + export function pushDesktopUpdateAvailableModal(onUpdate: () => void | Promise): void { ModalCommands.pushWithKey( modal(() => ( @@ -205,90 +202,6 @@ export function pushDesktopUpdateAvailableModal(onUpdate: () => void | Promise; - onDownload: (option: UpdaterDownloadOption) => void | Promise; -} - -function ManualUpdateAvailableModal({currentVersion, version, options, onDownload}: ManualUpdateAvailableModalProps) { - const [selectedFormat, setSelectedFormat] = useState( - options[0]?.format ?? null, - ); - const selectOptions = useMemo( - () => - options.map((option) => { - const descriptor = DOWNLOAD_FORMAT_LABEL_DESCRIPTORS[option.format]; - return { - value: option.format, - label: descriptor ? i18n._(descriptor) : option.label, - }; - }), - [options], - ); - const selectedOption = options.find((option) => option.format === selectedFormat) ?? options[0]; - return ( - -

- {i18n._(LINUX_PACKAGE_UPDATE_INTRO_DESCRIPTOR, {productName: RuntimeConfig.productName})} -

-
-
- {i18n._(INSTALLED_VERSION_LABEL_DESCRIPTOR)} -
-
- {currentVersion ?? i18n._(UNKNOWN_VERSION_DESCRIPTOR)} -
-
- {i18n._(AVAILABLE_VERSION_LABEL_DESCRIPTOR)} -
-
- {version ?? i18n._(NEW_VERSION_DESCRIPTOR)} -
-
- -

- {i18n._(PACKAGE_HELP_DESCRIPTOR)} -

- - } - primaryText={i18n._(DOWNLOAD_PACKAGE_DESCRIPTOR)} - secondaryText={i18n._(LATER_DESCRIPTOR)} - onPrimary={async () => { - if (selectedOption) { - await onDownload(selectedOption); - } - }} - data-flx="updater.updater-modal-commands.push-manual-update-available-modal.confirm-modal" - /> - ); -} - -export function pushManualUpdateAvailableModal(options: ManualUpdateAvailableModalProps): void { - ModalCommands.pushWithKey( - modal(() => ( - - )), - UPDATE_AVAILABLE_KEY, - ); -} - export function pushUpToDateModal(currentVersion: string | null): void { ModalCommands.pushWithKey( modal(() => ( @@ -320,13 +233,7 @@ export function pushUnsupportedUpdateModal( description={i18n._(SYSTEM_MANAGED_UPDATE_BODY_DESCRIPTOR, {productName: RuntimeConfig.productName})} primaryText={i18n._(OPEN_SOFTWARE_CENTER_DESCRIPTOR)} secondaryText={i18n._(CLOSE_DESCRIPTOR)} - onPrimary={() => { - const appstreamUrl = isCanaryDesktop() - ? 'appstream://app.fluxer.FluxerCanary' - : 'appstream://app.fluxer.Fluxer'; - - void openExternalUrl(appstreamUrl); - }} + onPrimary={() => openSoftwareCenter()} data-flx="updater.updater-modal-commands.push-unsupported-update-modal.confirm-modal" /> ); diff --git a/fluxer_app/src/features/updater/state/UpdaterStateMachine.ts b/fluxer_app/src/features/updater/state/UpdaterStateMachine.ts index eb2f3696d..5b21a44b1 100644 --- a/fluxer_app/src/features/updater/state/UpdaterStateMachine.ts +++ b/fluxer_app/src/features/updater/state/UpdaterStateMachine.ts @@ -1,6 +1,5 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import type {UpdaterDownloadOption} from '@app/types/electron.d'; import {assign, initialTransition, type SnapshotFrom, setup, transition} from 'xstate'; interface NativeUpdateInfo { @@ -31,7 +30,6 @@ interface UpdaterMachineContext { manualNativeDownloadInFlight: boolean; nativeUnsupported: NativeUnsupportedUpdate | null; nativeManualDownloadUrl: string | null; - nativeManualDownloadOptions: ReadonlyArray; } export type UpdaterMachineEvent = @@ -43,7 +41,6 @@ export type UpdaterMachineEvent = type: 'native.available'; version: string | null; downloadUrl: string | null; - downloadOptions: ReadonlyArray; } | {type: 'native.notAvailable'; now: number} | {type: 'native.error'} @@ -56,8 +53,6 @@ export type UpdaterMachineEvent = | {type: 'manualDownload.started'} | {type: 'manualDownload.finished'}; -const EMPTY_DOWNLOAD_OPTIONS: ReadonlyArray = Object.freeze([]); - function createEmptyNativeUpdateInfo(): NativeUpdateInfo { return { available: false, @@ -81,7 +76,6 @@ function createInitialUpdaterContext(): UpdaterMachineContext { manualNativeDownloadInFlight: false, nativeUnsupported: null, nativeManualDownloadUrl: null, - nativeManualDownloadOptions: EMPTY_DOWNLOAD_OPTIONS, }; } @@ -97,7 +91,6 @@ function clearNativeUpdate(context: UpdaterMachineContext): UpdaterMachineContex native: createEmptyNativeUpdateInfo(), }, nativeManualDownloadUrl: null, - nativeManualDownloadOptions: EMPTY_DOWNLOAD_OPTIONS, }; } @@ -141,7 +134,6 @@ const updaterStateMachine = setup({ isChecking: false, nativeUnsupported: null, nativeManualDownloadUrl: event.downloadUrl, - nativeManualDownloadOptions: [...event.downloadOptions], }; }), applyNativeNotAvailable: assign(({context, event}) => { @@ -231,7 +223,7 @@ export function transitionUpdaterMachineSnapshot( } export function hasManualNativeDownload(snapshot: UpdaterMachineSnapshot): boolean { - return Boolean(snapshot.context.nativeManualDownloadUrl) || snapshot.context.nativeManualDownloadOptions.length > 0; + return Boolean(snapshot.context.nativeManualDownloadUrl); } export function getUpdaterDisplayVersion(snapshot: UpdaterMachineSnapshot): string | null { diff --git a/fluxer_app/src/features/updater/utils/LinuxManualUpdateOptions.ts b/fluxer_app/src/features/updater/utils/LinuxManualUpdateOptions.ts deleted file mode 100644 index 82956dd6c..000000000 --- a/fluxer_app/src/features/updater/utils/LinuxManualUpdateOptions.ts +++ /dev/null @@ -1,181 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import RuntimeConfig from '@app/features/app/state/RuntimeConfig'; -import type {UpdaterDownloadFormat, UpdaterDownloadOption} from '@app/types/electron.d'; - -type LinuxManualDownloadFormat = Extract; -type LinuxDownloadArch = 'x64' | 'arm64'; -type DesktopDownloadChannel = 'stable' | 'canary'; - -const LINUX_MANUAL_DOWNLOAD_FORMATS: ReadonlyArray = ['appimage', 'deb', 'rpm', 'tar_gz']; - -const LINUX_MANUAL_FORMAT_LABELS: Record = { - appimage: 'AppImage', - deb: 'DEB package', - rpm: 'RPM package', - tar_gz: 'tar.gz archive', -}; - -const LINUX_MANUAL_FORMAT_EXTENSIONS: Record = { - appimage: '.AppImage', - deb: '.deb', - rpm: '.rpm', - tar_gz: '.tar.gz', -}; - -const LINUX_MANUAL_ARCH_TOKENS: Record> = { - appimage: {x64: 'x86_64', arm64: 'arm64'}, - deb: {x64: 'amd64', arm64: 'arm64'}, - rpm: {x64: 'x86_64', arm64: 'aarch64'}, - tar_gz: {x64: 'x64', arm64: 'arm64'}, -}; - -const PACKAGE_ORIGIN_BASE = 'https://pkgs.fluxer.com'; - -interface ParsedLinuxDownloadUrl { - downloadBase: string; - channel: DesktopDownloadChannel; - arch: LinuxDownloadArch; - search: string; -} - -export interface LinuxManualUpdateOptionsInput { - downloadUrl?: string | null; - channel?: string | null; - arch?: string | null; - version?: string | null; - knownOptions?: ReadonlyArray; -} - -function normalizeLinuxDownloadArch(value: string | null | undefined): LinuxDownloadArch { - const normalized = value?.trim().toLowerCase(); - if (normalized === 'arm64' || normalized === 'aarch64') { - return 'arm64'; - } - return 'x64'; -} - -function normalizeLinuxDownloadArchOrNull(value: string | null | undefined): LinuxDownloadArch | null { - const normalized = value?.trim(); - if (!normalized) { - return null; - } - return normalizeLinuxDownloadArch(normalized); -} - -function normalizeDesktopDownloadChannel(value: string | null | undefined): DesktopDownloadChannel { - return value?.trim().toLowerCase() === 'canary' ? 'canary' : 'stable'; -} - -function parseLinuxDownloadUrl(value: string | null | undefined): ParsedLinuxDownloadUrl | null { - if (!value) { - return null; - } - try { - const parsed = new URL(value); - const segments = parsed.pathname.split('/').filter(Boolean); - const start = segments.findIndex((segment, index) => segment === 'desktop' && segments[index + 2] === 'linux'); - if (start < 0) { - return null; - } - const channel = normalizeDesktopDownloadChannel(segments[start + 1]); - const arch = normalizeLinuxDownloadArch(segments[start + 3]); - const version = segments[start + 4]; - const format = segments[start + 5]; - if (!version || !format) { - return null; - } - const prefix = segments.slice(0, start); - const prefixPath = prefix.length > 0 ? `/${prefix.join('/')}` : ''; - return { - downloadBase: `${parsed.origin}${prefixPath}`, - channel, - arch, - search: parsed.search, - }; - } catch { - return null; - } -} - -function getLinuxDownloadArchFromText(value: string | null | undefined): LinuxDownloadArch | null { - const normalized = value?.trim().toLowerCase(); - if (!normalized) { - return null; - } - if (/(^|[-_/(\s])(?:arm64|aarch64)(?=$|[-_/).\s])/u.test(normalized)) { - return 'arm64'; - } - if (/(^|[-_/(\s])(?:x64|x86_64|amd64)(?=$|[-_/).\s])/u.test(normalized)) { - return 'x64'; - } - return null; -} - -function getLinuxDownloadOptionArch(option: UpdaterDownloadOption): LinuxDownloadArch | null { - return ( - parseLinuxDownloadUrl(option.url)?.arch ?? - getLinuxDownloadArchFromText(option.suggestedName) ?? - getLinuxDownloadArchFromText(option.label) - ); -} - -function getKnownLinuxManualOption( - options: ReadonlyArray, - format: LinuxManualDownloadFormat, - arch: LinuxDownloadArch, -): UpdaterDownloadOption | null { - const formatOptions = options.filter((option) => option.format === format); - return ( - formatOptions.find((option) => getLinuxDownloadOptionArch(option) === arch) ?? - (formatOptions.length === 1 && getLinuxDownloadOptionArch(formatOptions[0]) === null ? formatOptions[0] : null) - ); -} - -function buildLinuxDownloadUrl(params: { - downloadBase: string; - channel: DesktopDownloadChannel; - arch: LinuxDownloadArch; - versionToken: string; - format: LinuxManualDownloadFormat; - search: string; -}): string { - return `${params.downloadBase}/desktop/${params.channel}/linux/${params.arch}/${params.versionToken}/${params.format}${params.search}`; -} - -function getModernProductName(channel: DesktopDownloadChannel): string { - return channel === 'canary' ? `${RuntimeConfig.productName} Canary` : RuntimeConfig.productName; -} - -function getSuggestedName( - format: LinuxManualDownloadFormat, - channel: DesktopDownloadChannel, - arch: LinuxDownloadArch, - versionToken: string, -): string { - const archToken = LINUX_MANUAL_ARCH_TOKENS[format][arch]; - const extension = LINUX_MANUAL_FORMAT_EXTENSIONS[format]; - return `${getModernProductName(channel)}-${versionToken}-linux-${archToken}${extension}`; -} - -export function buildLinuxManualUpdateOptions(input: LinuxManualUpdateOptionsInput): Array { - const parsedUrl = parseLinuxDownloadUrl(input.downloadUrl); - const channel = parsedUrl?.channel ?? normalizeDesktopDownloadChannel(input.channel); - const arch = normalizeLinuxDownloadArchOrNull(input.arch) ?? parsedUrl?.arch ?? 'x64'; - const downloadBase = parsedUrl?.downloadBase ?? PACKAGE_ORIGIN_BASE; - const search = parsedUrl?.search ?? ''; - const version = input.version?.trim() ?? ''; - const hasVersion = /^\d+\.\d+\.\d+$/u.test(version); - const versionToken = hasVersion ? version : 'latest'; - const knownOptions = hasVersion ? (input.knownOptions ?? []) : []; - return LINUX_MANUAL_DOWNLOAD_FORMATS.map((format) => { - const knownOption = getKnownLinuxManualOption(knownOptions, format, arch); - return { - format, - label: LINUX_MANUAL_FORMAT_LABELS[format], - url: buildLinuxDownloadUrl({downloadBase, channel, arch, versionToken, format, search}), - suggestedName: knownOption?.suggestedName ?? getSuggestedName(format, channel, arch, versionToken), - sha256: knownOption?.sha256 ?? null, - }; - }); -} diff --git a/fluxer_desktop/src/common/DesktopIdentity.ts b/fluxer_desktop/src/common/DesktopIdentity.ts index e2d17ec0c..3572b434f 100644 --- a/fluxer_desktop/src/common/DesktopIdentity.ts +++ b/fluxer_desktop/src/common/DesktopIdentity.ts @@ -7,11 +7,6 @@ const DESKTOP_APP_NAMES: Record = { canary: 'Fluxer Canary', development: 'Fluxer Development', }; -const DESKTOP_ARTIFACT_PRODUCT_NAMES: Record = { - stable: 'Fluxer', - canary: 'Fluxer-Canary', - development: 'Fluxer-Development', -}; const MACOS_BUNDLE_IDS: Record = { stable: 'app.fluxer', canary: 'app.fluxer.canary', @@ -49,7 +44,6 @@ const WINDOWS_TOAST_ACTIVATOR_CLSIDS: Record = { }; export const DESKTOP_APP_NAME = DESKTOP_APP_NAMES[BUILD_CHANNEL]; -export const DESKTOP_ARTIFACT_PRODUCT_NAME = DESKTOP_ARTIFACT_PRODUCT_NAMES[BUILD_CHANNEL]; export const MACOS_BUNDLE_ID = MACOS_BUNDLE_IDS[BUILD_CHANNEL]; export const LINUX_DESKTOP_ENTRY_ID = LINUX_DESKTOP_ENTRY_IDS[BUILD_CHANNEL]; export const LINUX_PORTAL_SESSION_TOKEN = LINUX_PORTAL_SESSION_TOKENS[BUILD_CHANNEL]; diff --git a/fluxer_desktop/src/common/Types.ts b/fluxer_desktop/src/common/Types.ts index a0f0b9eab..ecd7ff6d5 100644 --- a/fluxer_desktop/src/common/Types.ts +++ b/fluxer_desktop/src/common/Types.ts @@ -42,7 +42,6 @@ import type { ThemeLocalFileReference, TrayPresenceStatus, TrayRuntimeStatePayload, - UpdaterDownloadOption, VirtmicBackend, VirtmicNode, VoiceBackgroundMediaCacheRequest, @@ -115,7 +114,6 @@ export type UpdaterEvent = context: UpdaterContext; version: string | null; downloadUrl?: string; - downloadOptions?: Array; } | { type: 'not-available'; @@ -129,7 +127,7 @@ export type UpdaterEvent = | { type: 'unsupported'; context: UpdaterContext; - reason: 'platform' | 'unpackaged' | 'managed-package'; + reason: 'platform' | 'unpackaged'; downloadUrl?: string; }; diff --git a/fluxer_desktop/src/main/Bootstrap.ts b/fluxer_desktop/src/main/Bootstrap.ts index a95b32f1c..cc5d3a480 100644 --- a/fluxer_desktop/src/main/Bootstrap.ts +++ b/fluxer_desktop/src/main/Bootstrap.ts @@ -104,7 +104,6 @@ const SPLASH_STATUS_BY_UPDATER_STATUS: Readonly { }; armSplashActions(); - const runShellSelfUpdateOnSplash = async ( - requiredSecurityUpdate: boolean, - ): Promise<{readonly reason: string; readonly detail: string | null}> => { + const runShellSelfUpdateOnSplash = async (): Promise<{readonly reason: string; readonly detail: string | null}> => { if (shellUpdatePlan.capability !== ShellUpdateCapability.SELF_UPDATE) { return {reason: 'unsupported', detail: null}; } const {runShellSelfUpdate} = await import('@electron/main/ShellSelfUpdate'); return await runShellSelfUpdate(shellUpdatePlan, { onDownloading: (progress) => { - setSplashState({status: 'shell-update-downloading', requiredSecurityUpdate, progress}); + setSplashState({status: 'shell-update-downloading', progress}); }, onRestarting: () => { - setSplashState({status: 'shell-update-restarting', requiredSecurityUpdate}); + setSplashState({status: 'shell-update-restarting'}); }, }); }; @@ -404,7 +401,7 @@ async function runModuleBootstrap(): Promise { } const recordPendingUpdate = (check: {readonly shellNewer: boolean; readonly modulesChanged: boolean}): void => { try { - if (check.modulesChanged) { + if (check.modulesChanged || check.shellNewer) { fs.writeFileSync(pendingUpdateMarker, ''); } else { fs.rmSync(pendingUpdateMarker, {force: true}); @@ -422,13 +419,14 @@ async function runModuleBootstrap(): Promise { forceStartupUpdate: store.shellVersionChanged || updateWasPending || isDesktopUpdateRequested(), selfUpdateShellFirst: shellUpdatePlan.capability === ShellUpdateCapability.SELF_UPDATE - ? async (latestVersion, requiredSecurityUpdate) => { + ? async (latestVersion) => { logger.info('A newer shell is available, updating it before the modules', {latestVersion}); - const result = await runShellSelfUpdateOnSplash(requiredSecurityUpdate); + const result = await runShellSelfUpdateOnSplash(); logger.warn('The shell self update did not restart the app, converging the modules for this shell', { latestVersion, ...result, }); + return result; } : undefined, fetch: moduleNetworkFetch, @@ -461,11 +459,7 @@ async function runModuleBootstrap(): Promise { outcome = await updater.run(); } catch (error) { logger.error('The module updater failed', error); - return await blockUntilRelaunch( - updater.getLastState().requiredSecurityUpdate - ? 'blocked-security-update-required' - : 'blocked-update-required', - ); + return await blockUntilRelaunch('blocked-update-required'); } switch (outcome.status) { case 'launching': @@ -490,12 +484,8 @@ async function runModuleBootstrap(): Promise { minimumVersion: outcome.minimumVersion, capability: shellUpdatePlan.capability, }); - if (shellUpdatePlan.capability === ShellUpdateCapability.SELF_UPDATE) { - const failure = await runShellSelfUpdateOnSplash(outcome.requiredSecurityUpdate); - logger.error('The shell self update failed, falling back to a manual download', failure); - } openSplashWindow(); - armBlockedShellUpdate(shellUpdatePlan, outcome.latestVersion, outcome.requiredSecurityUpdate); + armBlockedShellUpdate(); setSecondInstanceSink(focusSplashWindow); return await new Promise(() => {}); } @@ -507,17 +497,12 @@ async function runModuleBootstrap(): Promise { updateServerUnreachable: outcome.updateServerUnreachable, }); if (outcome.updateServerUnreachable) { - showBlockedSplash( - outcome.requiredSecurityUpdate ? 'blocked-security-update-required' : 'blocked-update-unreachable', - UPDATE_SERVER_UNREACHABLE_MESSAGE, - ); + showBlockedSplash('blocked-update-unreachable', UPDATE_SERVER_UNREACHABLE_MESSAGE); await updateServerRetry.holdUntilNextAttempt(); logger.info('Retrying the update server'); return null; } - return await blockUntilRelaunch( - outcome.requiredSecurityUpdate ? 'blocked-security-update-required' : 'blocked-update-required', - ); + return await blockUntilRelaunch('blocked-update-required'); default: return unhandledModuleUpdaterOutcome(outcome); } @@ -723,7 +708,7 @@ async function runModuleBootstrap(): Promise { const desktopUpdate = new DesktopUpdateRun({ probe: () => updater.checkForUpdate(), canSelfUpdateShell: shellUpdatePlan.capability === ShellUpdateCapability.SELF_UPDATE, - runShellSelfUpdate: () => runShellSelfUpdateOnSplash(false), + runShellSelfUpdate: () => runShellSelfUpdateOnSplash(), installModules: async () => { const launchAttempt = await updater.installPending(); if (launchAttempt != null) { diff --git a/fluxer_desktop/src/main/BootstrapEntry.test.mjs b/fluxer_desktop/src/main/BootstrapEntry.test.mjs index c68825faf..93d121401 100644 --- a/fluxer_desktop/src/main/BootstrapEntry.test.mjs +++ b/fluxer_desktop/src/main/BootstrapEntry.test.mjs @@ -68,19 +68,20 @@ describe('Bootstrap entry point', () => { 'Relaunching runs the same binary, refetches the same manifest and blocks again. On Linux, portable and Flatpak the shell cannot self update at all, so a retry is an unescapable loop.', ); assert.match(source, /const shellUpdatePlan = resolveShellUpdatePlan\(\);/); + assert.match(source, /armBlockedShellUpdate\(\);/); + }); + test('a shell that can update itself tries to before the modules, and the block only arms the download page', () => { assert.match( source, - /armBlockedShellUpdate\(shellUpdatePlan, outcome\.latestVersion, outcome\.requiredSecurityUpdate\);/, + /shellUpdatePlan\.capability === ShellUpdateCapability\.SELF_UPDATE\n\t+\? async \(latestVersion\) => \{/, ); - }); - test('a shell that can update itself tries to, and still falls through to the manual screen when it cannot', () => { - assert.match(source, /if \(shellUpdatePlan\.capability === ShellUpdateCapability\.SELF_UPDATE\) \{/); assert.match(source, /return await runShellSelfUpdate\(shellUpdatePlan, \{/); assert.match( source, - /logger\.error\('The shell self update failed, falling back to a manual download', failure\);\n\t{5}\}\n\t{5}openSplashWindow\(\);\n\t{5}armBlockedShellUpdate\(shellUpdatePlan, outcome\.latestVersion, outcome\.requiredSecurityUpdate\);/, - 'The manual screen is the fallback, so arming it must sit after the self update attempt rather than inside its else branch.', + /capability: shellUpdatePlan\.capability,\n\t{5}\}\);\n\t{5}openSplashWindow\(\);\n\t{5}armBlockedShellUpdate\(\);/, + 'The updater already attempted the self update before it returned the block, so the block must not try again.', ); + assert.doesNotMatch(source, /The shell self update failed, falling back to a manual download/); }); test('the self update is imported inside the update loop, never at the top level', () => { assert.doesNotMatch( @@ -92,16 +93,13 @@ describe('Bootstrap entry point', () => { }); test('the self update paints its own progress rather than leaving the checking screen up', () => { - assert.match(source, /setSplashState\(\{status: 'shell-update-downloading', requiredSecurityUpdate, progress\}\);/); - assert.match(source, /setSplashState\(\{status: 'shell-update-restarting', requiredSecurityUpdate\}\);/); - assert.match(source, /await runShellSelfUpdateOnSplash\(outcome\.requiredSecurityUpdate\)/); + assert.match(source, /setSplashState\(\{status: 'shell-update-downloading', progress\}\);/); + assert.match(source, /setSplashState\(\{status: 'shell-update-restarting'\}\);/); + assert.match(source, /const result = await runShellSelfUpdateOnSplash\(\);/); }); test('the retry affordance still belongs to the module update block', () => { assert.match(source, /setSplashState\(\{status, action: SplashAction\.RETRY, message\}\);/); - assert.match( - source, - /return await blockUntilRelaunch\(\n\t+outcome\.requiredSecurityUpdate \? 'blocked-security-update-required' : 'blocked-update-required',\n\t+\);/, - ); + assert.match(source, /return await blockUntilRelaunch\('blocked-update-required'\);/); }); test('the retry listener is registered once, beside the quit listener, never inside the block helper', () => { @@ -170,16 +168,13 @@ describe('Bootstrap entry point', () => { /setSplashState\(\{status, action: SplashAction\.RETRY, message\}\);\n\t+setSecondInstanceSink\(focusSplashWindow\);/, 'blockUntilRelaunch never settles, so index.ts never installs handleSecondInstance and the buffered launches would sit there forever. Pointing the sink at the splash is the only response left.', ); - assert.match( - source, - /armBlockedShellUpdate\(shellUpdatePlan, outcome\.latestVersion, outcome\.requiredSecurityUpdate\);\n\t+setSecondInstanceSink\(focusSplashWindow\);/, - ); + assert.match(source, /armBlockedShellUpdate\(\);\n\t+setSecondInstanceSink\(focusSplashWindow\);/); }); test('an unreachable update server is retried in process instead of waiting for a click', () => { const bootstrap = source.slice(source.indexOf('async function runModuleBootstrap()')); assert.match( bootstrap, - /if \(outcome\.updateServerUnreachable\) \{\n\t+showBlockedSplash\([^;]*UPDATE_SERVER_UNREACHABLE_MESSAGE,\n\t+\);\n\t+await updateServerRetry\.holdUntilNextAttempt\(\);\n[^\n]*\n\t+return null;/, + /if \(outcome\.updateServerUnreachable\) \{\n\t+showBlockedSplash\('blocked-update-unreachable', UPDATE_SERVER_UNREACHABLE_MESSAGE\);\n\t+await updateServerRetry\.holdUntilNextAttempt\(\);\n[^\n]*\n\t+return null;/, 'blockUntilRelaunch never settles, so a splash that reached it while the network was down stayed there after the network came back until the user pressed Retry now.', ); assert.match(bootstrap, /onSplashNetworkOnline\(\(\) => \{\n[^\n]*\n\t+updateServerRetry\.networkReturned\(\);/); @@ -232,10 +227,7 @@ describe('Bootstrap entry point', () => { /const showBlockedSplash = \(status: SplashStatus, message: string \| null = null\): void => \{\n\t\topenSplashWindow\(\);\n\t\tsetSplashState\(\{status, action: SplashAction\.RETRY, message\}\);/, 'A blocked or failed boot never reaches the main window, so a minimized login launch would otherwise sit invisible forever with nothing in the tray to bring it back.', ); - assert.match( - source, - /openSplashWindow\(\);\n\t+armBlockedShellUpdate\(shellUpdatePlan, outcome\.latestVersion, outcome\.requiredSecurityUpdate\);/, - ); + assert.match(source, /openSplashWindow\(\);\n\t+armBlockedShellUpdate\(\);/); }); test('the update splash hides the app only once it is on screen, in its own theme', () => { diff --git a/fluxer_desktop/src/main/DesktopUpdatePrompt.test.mjs b/fluxer_desktop/src/main/DesktopUpdatePrompt.test.mjs new file mode 100644 index 000000000..6bff99c69 --- /dev/null +++ b/fluxer_desktop/src/main/DesktopUpdatePrompt.test.mjs @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +import assert from 'node:assert/strict'; +import {readFileSync} from 'node:fs'; +import {createRequire} from 'node:module'; +import {describe, test} from 'node:test'; +import {fileURLToPath} from 'node:url'; +import vm from 'node:vm'; + +const require = createRequire(import.meta.url); +const esbuild = require('esbuild'); + +const sourcePath = fileURLToPath(new URL('./DesktopUpdatePrompt.ts', import.meta.url)); +const transformedSource = esbuild.transformSync(readFileSync(sourcePath, 'utf8'), { + loader: 'ts', + format: 'cjs', + platform: 'node', + target: 'node20', +}).code; + +const DOWNLOAD_PAGE_URL = 'https://fluxer.app/download'; +const FLATPAK_APP_ID = 'app.fluxer.FluxerDesktop'; +const NEWER_SHELL = '2026.1013.15209'; + +function loadPrompt({plan, flatpakAppId = null, response = 0}) { + const dialogs = []; + const opened = []; + const logger = {info: () => {}, warn: () => {}, error: () => {}}; + function requireStub(specifier) { + switch (specifier) { + case '@electron/common/DesktopIdentity': + return {DESKTOP_APP_NAME: 'Fluxer'}; + case '@electron/common/ErrorMessage': + return {errorMessage: (error) => String(error)}; + case '@electron/common/Logger': + return {createChildLogger: () => logger}; + case '@electron/main/DesktopUpdateGate': + return { + checkDesktopUpdateNow: async () => ({available: false}), + desktopUpdateReplacesShell: () => false, + getDesktopUpdateState: () => ({available: false}), + startDesktopUpdate: () => false, + }; + case '@electron/main/LinuxSandbox': + return {getFlatpakAppId: () => flatpakAppId, isFlatpakRuntime: () => flatpakAppId != null}; + case '@electron/main/MainI18n': + return {t: (key) => key}; + case '@electron/main/OpenExternal': + return { + openExternalDeduped: async (url) => { + opened.push(url); + }, + }; + case '@electron/main/ShellUpdateCapability': + return { + resolveShellUpdatePlan: () => plan, + ShellUpdateCapability: { + SELF_UPDATE: 'self-update', + MANUAL_DOWNLOAD: 'manual-download', + MANAGED_PACKAGE: 'managed-package', + }, + }; + case '@electron/main/Updater': + return {findNewerManualShell: async () => ({version: NEWER_SHELL})}; + case '@electron/main/UpdaterDownloads': + return {DOWNLOAD_PAGE_URL}; + case 'electron': + return { + app: {getVersion: () => '2026.1006.171735'}, + BrowserWindow: {getFocusedWindow: () => null}, + dialog: { + showMessageBox: async (options) => { + dialogs.push(options); + return {response}; + }, + }, + }; + default: + throw new Error(`Unexpected import: ${specifier}`); + } + } + const module = {exports: {}}; + vm.runInContext( + transformedSource, + vm.createContext({require: requireStub, module, exports: module.exports, console}), + {filename: sourcePath}, + ); + return {prompt: module.exports, dialogs, opened}; +} + +describe('the native update prompt for a shell that cannot update itself', () => { + test('a Flatpak with a newer shell offers the software center', async () => { + const {prompt, dialogs, opened} = loadPrompt({ + plan: {capability: 'managed-package'}, + flatpakAppId: FLATPAK_APP_ID, + }); + + await prompt.checkForUpdatesFromShell(); + + assert.equal(dialogs.length, 1); + assert.equal(dialogs[0].message, 'desktop.update.manualMessage'); + assert.equal(dialogs[0].detail, 'desktop.update.flatpakDetail'); + assert.deepEqual([...dialogs[0].buttons], ['desktop.update.openSoftwareCenter', 'desktop.update.later']); + assert.deepEqual(opened, [`appstream://${FLATPAK_APP_ID}`]); + }); + + test('a deb install with a newer shell offers the download page', async () => { + const {prompt, dialogs, opened} = loadPrompt({plan: {capability: 'managed-package'}}); + + await prompt.checkForUpdatesFromShell(); + + assert.equal(dialogs[0].detail, 'desktop.update.managedDetail'); + assert.deepEqual([...dialogs[0].buttons], ['desktop.update.download', 'desktop.update.later']); + assert.deepEqual(opened, [DOWNLOAD_PAGE_URL]); + }); + + test('a portable build keeps the plain download wording', async () => { + const {prompt, dialogs, opened} = loadPrompt({plan: {capability: 'manual-download', reason: 'platform'}}); + + await prompt.checkForUpdatesFromShell(); + + assert.equal(dialogs[0].detail, 'desktop.update.manualDetail'); + assert.deepEqual(opened, [DOWNLOAD_PAGE_URL]); + }); + + test('choosing later opens nothing', async () => { + const {prompt, opened} = loadPrompt({ + plan: {capability: 'managed-package'}, + flatpakAppId: FLATPAK_APP_ID, + response: 1, + }); + + await prompt.checkForUpdatesFromShell(); + + assert.deepEqual(opened, []); + }); +}); diff --git a/fluxer_desktop/src/main/DesktopUpdatePrompt.ts b/fluxer_desktop/src/main/DesktopUpdatePrompt.ts index ff936ffdc..d239b90ae 100644 --- a/fluxer_desktop/src/main/DesktopUpdatePrompt.ts +++ b/fluxer_desktop/src/main/DesktopUpdatePrompt.ts @@ -9,8 +9,10 @@ import { getDesktopUpdateState, startDesktopUpdate, } from '@electron/main/DesktopUpdateGate'; +import {getFlatpakAppId, isFlatpakRuntime} from '@electron/main/LinuxSandbox'; import {t} from '@electron/main/MainI18n'; import {openExternalDeduped} from '@electron/main/OpenExternal'; +import {resolveShellUpdatePlan, ShellUpdateCapability} from '@electron/main/ShellUpdateCapability'; import {findNewerManualShell} from '@electron/main/Updater'; import {DOWNLOAD_PAGE_URL} from '@electron/main/UpdaterDownloads'; import {app, BrowserWindow, dialog, type MessageBoxOptions} from 'electron'; @@ -67,17 +69,7 @@ async function runUpdateCheck(): Promise { return; } if (manualVersion != null) { - const download = await ask({ - type: 'info', - message: t('desktop.update.manualMessage', {version: manualVersion}), - detail: t('desktop.update.manualDetail'), - buttons: [t('desktop.update.download'), t('desktop.update.later')], - defaultId: UpdatePromptChoice.PRIMARY, - cancelId: UpdatePromptChoice.SECONDARY, - }); - if (download === UpdatePromptChoice.PRIMARY) { - await openExternalDeduped(DOWNLOAD_PAGE_URL); - } + await offerManualUpdate(manualVersion); return; } await ask({ @@ -91,6 +83,42 @@ async function runUpdateCheck(): Promise { } } +interface ManualUpdateOffer { + readonly detail: string; + readonly button: string; + readonly url: string; +} + +function resolveManualUpdateOffer(): ManualUpdateOffer { + if (resolveShellUpdatePlan().capability !== ShellUpdateCapability.MANAGED_PACKAGE) { + return {detail: t('desktop.update.manualDetail'), button: t('desktop.update.download'), url: DOWNLOAD_PAGE_URL}; + } + const flatpakAppId = isFlatpakRuntime() ? getFlatpakAppId() : null; + if (flatpakAppId != null) { + return { + detail: t('desktop.update.flatpakDetail'), + button: t('desktop.update.openSoftwareCenter'), + url: `appstream://${flatpakAppId}`, + }; + } + return {detail: t('desktop.update.managedDetail'), button: t('desktop.update.download'), url: DOWNLOAD_PAGE_URL}; +} + +async function offerManualUpdate(version: string): Promise { + const offer = resolveManualUpdateOffer(); + const choice = await ask({ + type: 'info', + message: t('desktop.update.manualMessage', {version}), + detail: offer.detail, + buttons: [offer.button, t('desktop.update.later')], + defaultId: UpdatePromptChoice.PRIMARY, + cancelId: UpdatePromptChoice.SECONDARY, + }); + if (choice === UpdatePromptChoice.PRIMARY) { + await openExternalDeduped(offer.url); + } +} + async function offerDesktopUpdate(): Promise { const install = await ask({ type: 'info', diff --git a/fluxer_desktop/src/main/MainI18n.ts b/fluxer_desktop/src/main/MainI18n.ts index 717239cd6..ae0e3ff41 100644 --- a/fluxer_desktop/src/main/MainI18n.ts +++ b/fluxer_desktop/src/main/MainI18n.ts @@ -80,6 +80,9 @@ const FALLBACK_STRINGS: Readonly> = Object.freeze({ 'desktop.update.download': 'Download', 'desktop.update.manualMessage': '{appName} {version} is available.', 'desktop.update.manualDetail': 'Download the new version to update {appName}.', + 'desktop.update.managedDetail': 'Update {appName} with your package manager, or download the new version.', + 'desktop.update.flatpakDetail': 'Update {appName} from your software center.', + 'desktop.update.openSoftwareCenter': 'Open software center', 'desktop.update.upToDateMessage': '{appName} is up to date.', 'desktop.update.upToDateDetail': 'You have version {version}.', 'desktop.update.checkFailedMessage': "{appName} couldn't check for updates.", diff --git a/fluxer_desktop/src/main/ModuleManifest.test.mjs b/fluxer_desktop/src/main/ModuleManifest.test.mjs index 802cd68dd..1f60f82ad 100644 --- a/fluxer_desktop/src/main/ModuleManifest.test.mjs +++ b/fluxer_desktop/src/main/ModuleManifest.test.mjs @@ -82,15 +82,34 @@ describe('parseModuleUpdateManifest', () => { ); }); - test('refuses linux_security_minimum.required_modules naming an Object.prototype property', () => { + test('linux_security_minimum is ignored, even one shipped shells would enforce', () => { const bytes = manifestBytes({ platform: 'linux', - linux_security_minimum: {version: SHELL_VERSION, required_modules: ['constructor']}, + shell: {latest_version: '2026.900.1', minimum_version: '0.0.0'}, + linux_security_minimum: {version: '2026.900.1', required_modules: ['constructor']}, }); - assert.throws(() => parseModuleUpdateManifest(bytes, {...EXPECTED_TARGET, platform: 'linux'}), { - name: 'ModuleManifestMalformedError', - message: /linux_security_minimum\.required_modules names an undeclared module: constructor/u, + const manifest = parseModuleUpdateManifest(bytes, {...EXPECTED_TARGET, platform: 'linux'}); + + assert.equal(Object.hasOwn(manifest, 'linuxSecurityMinimum'), false); + assert.equal(manifest.shell.minimumVersion.source, '0.0.0'); + }); + + test('a manifest carrying shell_compatibility parses', () => { + const bytes = manifestBytes({ + shell_compatibility: { + minimum_shell_version: '0.0.0', + source_sha: 'b'.repeat(40), + previous_source_sha: 'c'.repeat(40), + floor_source_sha: null, + decision: 'compatible', + contract_base_sha: 'c'.repeat(40), + contract_changes: ['fluxer_desktop/src/preload/index.ts'], + }, }); + + const manifest = parseModuleUpdateManifest(bytes, EXPECTED_TARGET); + + assert.equal(manifest.modules.fluxer_renderer.sha256, RENDERER_SHA); }); }); diff --git a/fluxer_desktop/src/main/ModuleManifest.ts b/fluxer_desktop/src/main/ModuleManifest.ts index 0efe316e4..70087ce1f 100644 --- a/fluxer_desktop/src/main/ModuleManifest.ts +++ b/fluxer_desktop/src/main/ModuleManifest.ts @@ -2,12 +2,7 @@ import {isRecord} from '@fluxer/record_utils/src/RecordGuards'; // SPDX-License-Identifier: AGPL-3.0-or-later import type {Buffer} from 'node:buffer'; -import { - compareModuleVersions, - type ModuleVersion, - ModuleVersionMalformedError, - parseModuleVersion, -} from '@electron/main/ModuleVersion'; +import {type ModuleVersion, ModuleVersionMalformedError, parseModuleVersion} from '@electron/main/ModuleVersion'; import {isDesktopModuleName} from '@fluxer/desktop_ipc/src/ModuleContract'; const MODULE_MANIFEST_VERSION = 1; @@ -36,11 +31,6 @@ export interface DesktopModuleShellRequirement { readonly minimumVersion: ModuleVersion; } -export interface DesktopLinuxSecurityMinimum { - readonly version: ModuleVersion; - readonly requiredModules: ReadonlyArray; -} - export interface DesktopModuleUpdateManifest { readonly manifestVersion: number; readonly releaseChannel: string; @@ -52,7 +42,6 @@ export interface DesktopModuleUpdateManifest { readonly shell: DesktopModuleShellRequirement; readonly modules: Readonly>; readonly requiredModules: ReadonlyArray; - readonly linuxSecurityMinimum: DesktopLinuxSecurityMinimum | null; } class ModuleManifestMalformedError extends Error { @@ -228,51 +217,6 @@ export function parseModuleUpdateManifest( } const buildVersion = requireBuildVersion(parsed); const shell = parseShellRequirement(parsed['shell']); - const rawLinuxSecurityMinimum = parsed['linux_security_minimum']; - let linuxSecurityMinimum: DesktopLinuxSecurityMinimum | null = null; - if (rawLinuxSecurityMinimum != null) { - if (!isRecord(rawLinuxSecurityMinimum)) { - throw new ModuleManifestMalformedError('manifest linux_security_minimum must be an object or null'); - } - const rawSecurityModules = rawLinuxSecurityMinimum['required_modules']; - if (!Array.isArray(rawSecurityModules)) { - throw new ModuleManifestMalformedError('manifest linux_security_minimum.required_modules must be an array'); - } - const securityModules: Array = []; - const seenSecurityModules = new Set(); - for (const moduleName of rawSecurityModules) { - if (typeof moduleName !== 'string' || !Object.hasOwn(modules, moduleName)) { - throw new ModuleManifestMalformedError( - `linux_security_minimum.required_modules names an undeclared module: ${String(moduleName)}`, - ); - } - if (seenSecurityModules.has(moduleName)) { - throw new ModuleManifestMalformedError( - `linux_security_minimum.required_modules contains a duplicate module: ${moduleName}`, - ); - } - seenSecurityModules.add(moduleName); - securityModules.push(moduleName); - } - securityModules.sort(); - linuxSecurityMinimum = Object.freeze({ - version: requireVersion(rawLinuxSecurityMinimum, 'version', 'manifest linux_security_minimum.version'), - requiredModules: Object.freeze(securityModules), - }); - } - if (platform !== 'linux' && linuxSecurityMinimum != null) { - throw new ModuleManifestMalformedError('manifest linux security minimum is only valid for Linux'); - } - if (linuxSecurityMinimum != null) { - if (compareModuleVersions(buildVersion, linuxSecurityMinimum.version) < 0) { - throw new ModuleManifestMalformedError('manifest build_version cannot be below linux_security_minimum.version'); - } - if (compareModuleVersions(shell.minimumVersion, linuxSecurityMinimum.version) < 0) { - throw new ModuleManifestMalformedError( - 'manifest shell.minimum_version cannot be below linux_security_minimum.version', - ); - } - } return Object.freeze({ manifestVersion, releaseChannel, @@ -284,14 +228,5 @@ export function parseModuleUpdateManifest( shell, modules: Object.freeze(modules), requiredModules: Object.freeze(requiredModules), - linuxSecurityMinimum, }); } - -export function blockingModuleNames(manifest: DesktopModuleUpdateManifest): ReadonlySet { - const names = new Set(manifest.requiredModules); - for (const moduleName of manifest.linuxSecurityMinimum?.requiredModules ?? []) { - names.add(moduleName); - } - return names; -} diff --git a/fluxer_desktop/src/main/ModulePackageInstaller.test.mjs b/fluxer_desktop/src/main/ModulePackageInstaller.test.mjs index 516322843..e56f9f8d3 100644 --- a/fluxer_desktop/src/main/ModulePackageInstaller.test.mjs +++ b/fluxer_desktop/src/main/ModulePackageInstaller.test.mjs @@ -85,6 +85,7 @@ function createInstaller({respond, resumeFrom}) { installModule: async ({download}) => { downloads.push(await collect(await download(resumeFrom))); }, + recordShellRanges: async () => {}, }, packageOrigin: PACKAGE_ORIGIN, fetch: async (url, init) => { diff --git a/fluxer_desktop/src/main/ModulePackageInstaller.ts b/fluxer_desktop/src/main/ModulePackageInstaller.ts index b23fbd2fd..08be80eff 100644 --- a/fluxer_desktop/src/main/ModulePackageInstaller.ts +++ b/fluxer_desktop/src/main/ModulePackageInstaller.ts @@ -201,6 +201,14 @@ export class ModulePackageInstaller { sha256: item.entry.sha256, download: (resumeFrom) => this.openPackageStream(item, current, total, resumeFrom), }); + await this.store.recordShellRanges([ + { + module: item.module, + sha256: item.entry.sha256, + minimum: item.entry.minimumShellVersion?.source ?? null, + maximum: item.entry.maximumShellVersion?.source ?? null, + }, + ]); this.onProgress({ phase: ModulePackageInstallPhase.INSTALLING, current, diff --git a/fluxer_desktop/src/main/ModuleStore.test.mjs b/fluxer_desktop/src/main/ModuleStore.test.mjs index c0d8c0546..ee3bc967c 100644 --- a/fluxer_desktop/src/main/ModuleStore.test.mjs +++ b/fluxer_desktop/src/main/ModuleStore.test.mjs @@ -1037,6 +1037,112 @@ describe('ModuleStore rejected module sets', () => { }); }); +describe('ModuleStore shell ranges beside older shells', () => { + test('state written by this shell stays readable by shells that require linux_security_minimum', async () => { + const userDataPath = createUserData(); + const store = await openStore(userDataPath); + await install(store, RENDERER_V1); + await store.commit({fluxer_renderer: RENDERER_V1.sha256}); + + const written = readState(userDataPath); + + assert.equal(Object.hasOwn(written, 'linux_security_minimum'), true); + assert.equal(written.linux_security_minimum, null); + assert.equal(written.state_version, MODULE_STATE_VERSION); + }); + + test('state that persisted a security minimum still opens', async () => { + const userDataPath = createUserData(); + const store = await openStore(userDataPath); + await install(store, RENDERER_V1); + await store.commit({fluxer_renderer: RENDERER_V1.sha256}); + const legacy = readState(userDataPath); + legacy.linux_security_minimum = {version: '2026.900.1', required_modules: ['fluxer_renderer']}; + delete legacy.shell_ranges; + writeFileSync(path.join(store.root, MODULE_STATE_FILE_NAME), JSON.stringify(legacy)); + + const reopened = await openStore(userDataPath); + + assert.equal(reopened.recoveredUnreadableState, null); + assert.deepEqual(reopened.getCommitted(), {fluxer_renderer: RENDERER_V1.sha256}); + assert.deepEqual(reopened.getState().shell_ranges, {}); + await reopened.commit({fluxer_renderer: RENDERER_V1.sha256}); + assert.equal(readState(userDataPath).linux_security_minimum, null); + }); + + test('shell ranges survive a reopen and garbage collection prunes them', async () => { + const userDataPath = createUserData(); + const earlierRun = await openStore(userDataPath); + await install(earlierRun, RENDERER_V1); + await earlierRun.commit({fluxer_renderer: RENDERER_V1.sha256}); + const orphan = await install(earlierRun, GRAMMARS_V1); + await earlierRun.recordShellRanges([ + {module: 'fluxer_renderer', sha256: RENDERER_V1.sha256, minimum: SHELL_VERSION, maximum: null}, + {module: 'fluxer_grammars', sha256: GRAMMARS_V1.sha256, minimum: null, maximum: '2026.900.1'}, + {module: 'fluxer_renderer', sha256: 'd'.repeat(64), minimum: '2026.900.1', maximum: null}, + ]); + const store = await openStore(userDataPath); + + assert.deepEqual(store.getShellRange('fluxer_renderer', RENDERER_V1.sha256), { + minimum_shell_version: SHELL_VERSION, + maximum_shell_version: null, + }); + assert.deepEqual(store.getShellRange('fluxer_grammars', GRAMMARS_V1.sha256), { + minimum_shell_version: null, + maximum_shell_version: '2026.900.1', + }); + + await markLaunched(store); + await store.collectGarbage(); + + assert.equal(existsSync(orphan.directory), false); + assert.deepEqual(Object.keys(readState(userDataPath).shell_ranges), [`fluxer_renderer/${RENDERER_V1.sha256}`]); + assert.equal(store.getShellRange('fluxer_grammars', GRAMMARS_V1.sha256), null); + }); + + test('a recorded range that no longer parses is dropped instead of discarding the state', async () => { + const userDataPath = createUserData(); + const store = await openStore(userDataPath); + const state = readState(userDataPath); + state.shell_ranges = { + [`fluxer_renderer/${'d'.repeat(64)}`]: {minimum_shell_version: 'not a version', maximum_shell_version: null}, + [`fluxer_renderer/${'e'.repeat(64)}`]: {minimum_shell_version: SHELL_VERSION, maximum_shell_version: null}, + }; + writeFileSync(path.join(store.root, MODULE_STATE_FILE_NAME), JSON.stringify(state)); + + const reopened = await openStore(userDataPath); + + assert.equal(reopened.recoveredUnreadableState, null); + assert.deepEqual(Object.keys(reopened.getState().shell_ranges), [`fluxer_renderer/${'e'.repeat(64)}`]); + }); + + test('retireIncompatible drops non-runnable pairs from committed, previous and floor', async () => { + const userDataPath = createUserData(); + const store = await openStore(userDataPath); + await install(store, RENDERER_V1); + await install(store, RENDERER_V2); + await install(store, GRAMMARS_V1); + await store.commit({fluxer_renderer: RENDERER_V1.sha256}); + await store.commit({fluxer_renderer: RENDERER_V2.sha256, fluxer_grammars: GRAMMARS_V1.sha256}); + await store.recordManifestFetch({ + etag: null, + fetchedAt: MANIFEST_FETCHED_AT, + manifest: manifestObservation(1, 'a'.repeat(64)), + floor: {fluxer_renderer: RENDERER_V2.sha256, fluxer_grammars: GRAMMARS_V1.sha256}, + }); + + const retired = await store.retireIncompatible( + (moduleName, digest) => !(moduleName === 'fluxer_renderer' && digest === RENDERER_V2.sha256), + ); + + assert.deepEqual(retired, ['fluxer_renderer']); + assert.deepEqual(store.getCommitted(), {fluxer_grammars: GRAMMARS_V1.sha256}); + assert.deepEqual(store.getState().previous, {fluxer_renderer: RENDERER_V1.sha256}); + assert.deepEqual(store.getState().floor, {fluxer_grammars: GRAMMARS_V1.sha256}); + assert.deepEqual(await store.retireIncompatible(() => true), []); + }); +}); + describe('ModuleStore on Windows sharing violations', () => { test('a rename that an antivirus scanner briefly locks is retried instead of failing the install', async () => { const userDataPath = createUserData(); diff --git a/fluxer_desktop/src/main/ModuleStore.ts b/fluxer_desktop/src/main/ModuleStore.ts index cc7a93f34..64bc43749 100644 --- a/fluxer_desktop/src/main/ModuleStore.ts +++ b/fluxer_desktop/src/main/ModuleStore.ts @@ -24,7 +24,7 @@ import { parseDesktopModuleManifest, verifyAndExtract, } from '@electron/main/ModulePackage'; -import {compareModuleVersions, parseModuleVersion} from '@electron/main/ModuleVersion'; +import {parseModuleVersion} from '@electron/main/ModuleVersion'; import {isDesktopModuleName} from '@fluxer/desktop_ipc/src/ModuleContract'; const MODULE_STORE_DIRECTORY_NAME = 'modules'; @@ -131,9 +131,16 @@ export class ModuleStoreGarbageCollectionOrderError extends ModuleStoreError { } } -export interface LinuxModuleSecurityMinimum { - readonly version: string; - readonly requiredModules: ReadonlyArray; +export interface ModuleShellRange { + readonly minimum_shell_version: string | null; + readonly maximum_shell_version: string | null; +} + +export interface ModuleShellRangeRecord { + readonly module: string; + readonly sha256: string; + readonly minimum: string | null; + readonly maximum: string | null; } export interface ModuleManifestFeedIdentity { @@ -161,7 +168,7 @@ interface ModuleStoreState { readonly previous: Readonly>; readonly rejected: Readonly>; readonly floor: Readonly>; - readonly linux_security_minimum: LinuxModuleSecurityMinimum | null; + readonly shell_ranges: Readonly>; readonly manifest_high_water: ReadonlyArray; readonly last_manifest_etag: string | null; readonly last_manifest_fetch: string | null; @@ -379,6 +386,15 @@ async function isRegularFile(target: string): Promise { } } +async function isDirectory(target: string): Promise { + try { + const stats = await fs.stat(target); + return stats.isDirectory(); + } catch { + return false; + } +} + function isMissingInstallationError(error: unknown): boolean { return ( typeof error === 'object' && @@ -885,63 +901,66 @@ function retainRejected( return retained; } -function parseLinuxSecurityMinimum(value: unknown): LinuxModuleSecurityMinimum | null | undefined { - if (value === undefined || value === null) { - return null; - } - if (typeof value !== 'object' || Array.isArray(value)) { - return undefined; - } - const record = value as Record; - const version = record['version']; - const rawRequiredModules = record['required_modules']; - if (typeof version !== 'string' || !Array.isArray(rawRequiredModules)) { - return undefined; - } - try { - parseModuleVersion(version, 'persisted Linux security minimum'); - } catch { - return undefined; - } - const requiredModules: Array = []; - const seen = new Set(); - for (const moduleName of rawRequiredModules) { - if (!isDesktopModuleName(moduleName) || seen.has(moduleName)) { - return undefined; - } - seen.add(moduleName); - requiredModules.push(moduleName); - } - return {version, requiredModules: Object.freeze(requiredModules.sort())}; +function shellRangeKey(moduleName: string, sha256: string): string { + return `${moduleName}/${sha256}`; } -function advanceLinuxSecurityMinimum( - current: LinuxModuleSecurityMinimum | null, - candidate: LinuxModuleSecurityMinimum, -): LinuxModuleSecurityMinimum { - const version = parseModuleVersion(candidate.version, 'Linux security minimum'); - const requiredModules = Array.from(new Set(candidate.requiredModules)).sort(); - if (requiredModules.length !== candidate.requiredModules.length) { - throw new ModuleStoreError('Linux security minimum contains duplicate required modules'); +function isShellRangeVersion(value: unknown): value is string | null { + if (value === null) { + return true; } - for (const moduleName of requiredModules) { - assertModuleName(moduleName); + if (typeof value !== 'string') { + return false; } - if (current != null) { - const comparison = compareModuleVersions( - version, - parseModuleVersion(current.version, 'persisted Linux security minimum'), - ); - if (comparison < 0) { - throw new ModuleStoreError( - `refusing to lower Linux security minimum from ${current.version} to ${candidate.version}`, - ); + try { + parseModuleVersion(value, 'persisted module shell range'); + return true; + } catch { + return false; + } +} + +function isShellRangeKey(key: string): boolean { + const separator = key.indexOf('/'); + return separator > 0 && isDesktopModuleName(key.slice(0, separator)) && SHA256_PATTERN.test(key.slice(separator + 1)); +} + +function sortShellRanges( + value: Readonly>, +): Readonly> { + const sorted: Record = {}; + for (const key of Object.keys(value).sort()) { + sorted[key] = Object.freeze({...value[key]}); + } + return Object.freeze(sorted); +} + +function parseShellRanges(value: unknown): Readonly> { + if (typeof value !== 'object' || value === null || Array.isArray(value)) { + return Object.freeze({}); + } + const parsed: Record = {}; + for (const [key, rawRange] of Object.entries(value as Record)) { + if (!isShellRangeKey(key) || typeof rawRange !== 'object' || rawRange === null || Array.isArray(rawRange)) { + continue; } - if (comparison === 0 && requiredModules.join('\0') !== current.requiredModules.join('\0')) { - throw new ModuleStoreError(`Linux security minimum ${candidate.version} changed its required modules`); + const range = rawRange as Record; + const minimum = range['minimum_shell_version']; + const maximum = range['maximum_shell_version']; + if (!isShellRangeVersion(minimum) || !isShellRangeVersion(maximum)) { + continue; } + parsed[key] = {minimum_shell_version: minimum, maximum_shell_version: maximum}; } - return {version: candidate.version, requiredModules: Object.freeze(requiredModules)}; + return sortShellRanges(parsed); +} + +function sameShellRange(left: ModuleShellRange | undefined, right: ModuleShellRange): boolean { + return ( + left != null && + left.minimum_shell_version === right.minimum_shell_version && + left.maximum_shell_version === right.maximum_shell_version + ); } function parseOptionalString(value: unknown): string | null | undefined { @@ -990,19 +1009,8 @@ function parseModuleStoreState(bytes: Buffer, statePath: string): ModuleStoreSta const previous = parseModuleMap(record['previous']); const rejected = record['rejected'] === undefined ? {} : parseModuleMap(record['rejected']); const floor = parseModuleMap(record['floor']); - const rawLinuxSecurityMinimum = record['linux_security_minimum']; - if (rawLinuxSecurityMinimum === undefined) { - throw new ModuleStoreStateCorruptError(statePath, 'linux_security_minimum is required'); - } - const linuxSecurityMinimum = parseLinuxSecurityMinimum(rawLinuxSecurityMinimum); - if ( - committed === null || - previous === null || - rejected === null || - floor === null || - linuxSecurityMinimum === undefined - ) { - throw new ModuleStoreStateCorruptError(statePath, 'module maps or Linux security minimum are invalid'); + if (committed === null || previous === null || rejected === null || floor === null) { + throw new ModuleStoreStateCorruptError(statePath, 'module maps are invalid'); } const manifestHighWater = parseManifestHighWater(record['manifest_high_water']); if (manifestHighWater === null) { @@ -1025,7 +1033,7 @@ function parseModuleStoreState(bytes: Buffer, statePath: string): ModuleStoreSta previous, rejected, floor, - linux_security_minimum: linuxSecurityMinimum, + shell_ranges: parseShellRanges(record['shell_ranges']), manifest_high_water: manifestHighWater, last_manifest_etag: lastManifestEtag, last_manifest_fetch: lastManifestFetch, @@ -1042,7 +1050,7 @@ function createInitialModuleStoreState(shellVersion: string, releaseChannel: str previous: {}, rejected: {}, floor: {}, - linux_security_minimum: null, + shell_ranges: {}, manifest_high_water: [], last_manifest_etag: null, last_manifest_fetch: null, @@ -1060,13 +1068,8 @@ function serializeModuleStoreState(state: ModuleStoreState): string { previous: sortModuleMap(state.previous), rejected: sortModuleMap(state.rejected), floor: sortModuleMap(state.floor), - linux_security_minimum: - state.linux_security_minimum == null - ? null - : { - version: state.linux_security_minimum.version, - required_modules: state.linux_security_minimum.requiredModules, - }, + linux_security_minimum: null, + shell_ranges: sortShellRanges(state.shell_ranges), manifest_high_water: state.manifest_high_water.map((entry) => ({ release_channel: entry.releaseChannel, platform: entry.platform, @@ -1510,14 +1513,12 @@ export class ModuleStore { fetchedAt, manifest, floor, - linuxSecurityMinimum, advertised, }: { readonly etag: string | null; readonly fetchedAt: string; readonly manifest: ModuleManifestFeedObservation; readonly floor?: Readonly>; - readonly linuxSecurityMinimum?: LinuxModuleSecurityMinimum; readonly advertised?: Readonly>; }): Promise { return await this.withStateLock(async () => { @@ -1528,15 +1529,81 @@ export class ModuleStore { last_manifest_etag: etag, last_manifest_fetch: fetchedAt, floor: floor == null ? this.state.floor : sortModuleMap(floor), - linux_security_minimum: - linuxSecurityMinimum == null - ? this.state.linux_security_minimum - : advanceLinuxSecurityMinimum(this.state.linux_security_minimum, linuxSecurityMinimum), manifest_high_water: manifestHighWater, }); }); } + public getShellRange(moduleName: string, sha256: string): ModuleShellRange | null { + return this.state.shell_ranges[shellRangeKey(moduleName, sha256)] ?? null; + } + + public async recordShellRanges(ranges: ReadonlyArray): Promise { + for (const range of ranges) { + assertModuleName(range.module); + assertSha256(range.sha256); + } + await this.withStateLock(async () => { + const shellRanges: Record = {...this.state.shell_ranges}; + let changed = false; + for (const range of ranges) { + const key = shellRangeKey(range.module, range.sha256); + const next = {minimum_shell_version: range.minimum, maximum_shell_version: range.maximum}; + if (sameShellRange(shellRanges[key], next)) { + continue; + } + shellRanges[key] = next; + changed = true; + } + if (changed) { + await this.writeState({...this.state, shell_ranges: shellRanges}); + } + }); + } + + public async retireIncompatible( + isRunnable: (moduleName: string, sha256: string) => boolean, + ): Promise> { + return await this.withStateLock(async () => { + const retired = new Set(); + const keepRunnable = (map: Readonly>): Record => { + const kept: Record = {}; + for (const [moduleName, sha256] of Object.entries(map)) { + if (isRunnable(moduleName, sha256)) { + kept[moduleName] = sha256; + } else { + retired.add(moduleName); + } + } + return kept; + }; + const committed = keepRunnable(this.state.committed); + const previous = keepRunnable(this.state.previous); + const floor = keepRunnable(this.state.floor); + if (retired.size === 0) { + return []; + } + await this.writeState({...this.state, committed, previous, floor}); + return [...retired].sort(); + }); + } + + private async pruneShellRanges(): Promise { + await this.withStateLock(async () => { + const shellRanges: Record = {}; + for (const [key, range] of Object.entries(this.state.shell_ranges)) { + const separator = key.indexOf('/'); + const directory = this.getModuleDirectory(key.slice(0, separator), key.slice(separator + 1)); + if (await isDirectory(directory)) { + shellRanges[key] = range; + } + } + if (Object.keys(shellRanges).length !== Object.keys(this.state.shell_ranges).length) { + await this.writeState({...this.state, shell_ranges: shellRanges}); + } + }); + } + public async beginBootAttempt({ bundled = [], }: { @@ -1666,7 +1733,9 @@ export class ModuleStore { if (!this.launchSucceeded) { throw new ModuleStoreGarbageCollectionOrderError(); } - return await this.withStoreTreeLock(() => this.collectGarbageExclusively(now)); + const result = await this.withStoreTreeLock(() => this.collectGarbageExclusively(now)); + await this.pruneShellRanges(); + return result; } private async collectGarbageExclusively(now: number): Promise { @@ -1791,13 +1860,7 @@ export class ModuleStore { previous: sortModuleMap(next.previous), rejected: sortModuleMap(next.rejected), floor: sortModuleMap(next.floor), - linux_security_minimum: - next.linux_security_minimum == null - ? null - : { - version: next.linux_security_minimum.version, - requiredModules: Object.freeze([...next.linux_security_minimum.requiredModules]), - }, + shell_ranges: sortShellRanges(next.shell_ranges), manifest_high_water: sortManifestHighWater(next.manifest_high_water), }; for (const [moduleName, sha256] of Object.entries(this.state.committed)) { diff --git a/fluxer_desktop/src/main/ModuleUpdatePlanner.test.mjs b/fluxer_desktop/src/main/ModuleUpdatePlanner.test.mjs index b3b23cda9..014153edb 100644 --- a/fluxer_desktop/src/main/ModuleUpdatePlanner.test.mjs +++ b/fluxer_desktop/src/main/ModuleUpdatePlanner.test.mjs @@ -63,6 +63,7 @@ function createStore({ bootAttempt = 0, missing = [], installedManifests = {}, + shellRanges = {}, } = {}) { const absent = new Set(missing); return { @@ -77,6 +78,7 @@ function createStore({ isRejected: (moduleName, sha256) => rejected[moduleName] === sha256, isInstalled: async (moduleName, sha256) => !absent.has(`${moduleName}:${sha256}`), getInstalledManifest: async (moduleName, sha256) => installedManifests[`${moduleName}:${sha256}`] ?? null, + getShellRange: (moduleName, sha256) => shellRanges[`${moduleName}/${sha256}`] ?? null, }; } @@ -237,13 +239,109 @@ describe('ModuleUpdatePlanner.plan', () => { assert.deepEqual(plan.items, []); assert.deepEqual(plan.base, {fluxer_renderer: RENDERER_SHA}); }); + + test('plan keeps the committed renderer and assets when the feed renderer needs a newer shell', async () => { + const dictionarySha = 'd'.repeat(64); + const store = createStore({committed: {fluxer_renderer: RENDERER_SHA, fluxer_overlay: OVERLAY_SHA}}); + const planner = createPlanner(store); + const plan = await planner.plan( + manifest({ + modules: { + fluxer_renderer: {sha256: 'c'.repeat(64), minimumShellVersion: '2026.900.1'}, + fluxer_overlay: {sha256: 'e'.repeat(64)}, + fluxer_dict_en: {sha256: dictionarySha}, + }, + required: ['fluxer_renderer', 'fluxer_dict_en'], + }), + ); + + assert.deepEqual( + plan.items.map((item) => [item.module, item.entry.sha256, item.requirement]), + [['fluxer_dict_en', dictionarySha, 'required']], + ); + assert.deepEqual(plan.base, {fluxer_renderer: RENDERER_SHA, fluxer_overlay: OVERLAY_SHA}); + }); +}); + +describe('ModuleUpdatePlanner.compatibleEntry', () => { + test('a shell below the manifest shell floor gets no entry even when the entry has no floor', () => { + const planner = createPlanner(createStore()); + const feed = manifest({shellMinimum: '2026.900.1', modules: {fluxer_renderer: {sha256: RENDERER_SHA}}}); + + assert.equal(planner.compatibleEntry(feed, 'fluxer_renderer'), null); + }); + + test('an entry inside its shell range is returned, one outside or missing is not', () => { + const planner = createPlanner(createStore()); + const feed = manifest({ + shellMinimum: '0.0.0', + modules: { + fluxer_renderer: {sha256: RENDERER_SHA, minimumShellVersion: SHELL_VERSION}, + fluxer_overlay: {sha256: OVERLAY_SHA, minimumShellVersion: '2026.900.1'}, + }, + }); + + assert.equal(planner.compatibleEntry(feed, 'fluxer_renderer')?.sha256, RENDERER_SHA); + assert.equal(planner.compatibleEntry(feed, 'fluxer_overlay'), null); + assert.equal(planner.compatibleEntry(feed, 'toString'), null); + }); +}); + +describe('ModuleUpdatePlanner.isRunnable', () => { + const cachedFeed = () => + manifest({ + shellMinimum: '0.0.0', + modules: {fluxer_renderer: {sha256: RENDERER_SHA, minimumShellVersion: '2026.900.1'}}, + }); + + test('a module with no record and no cached entry is runnable', () => { + const planner = createPlanner(createStore()); + + assert.equal(planner.isRunnable('fluxer_renderer', RENDERER_SHA, null), true); + }); + + test('a recorded range this shell is outside of is not runnable', () => { + const planner = createPlanner( + createStore({ + shellRanges: { + [`fluxer_renderer/${RENDERER_SHA}`]: {minimum_shell_version: '2026.900.1', maximum_shell_version: null}, + [`fluxer_overlay/${OVERLAY_SHA}`]: {minimum_shell_version: null, maximum_shell_version: '2026.823.0'}, + }, + }), + ); + + assert.equal(planner.isRunnable('fluxer_renderer', RENDERER_SHA, null), false); + assert.equal(planner.isRunnable('fluxer_overlay', OVERLAY_SHA, null), false); + }); + + test('a recorded range that ends on this shell is runnable', () => { + const planner = createPlanner( + createStore({ + shellRanges: { + [`fluxer_renderer/${RENDERER_SHA}`]: { + minimum_shell_version: SHELL_VERSION, + maximum_shell_version: SHELL_VERSION, + }, + }, + }), + ); + + assert.equal(planner.isRunnable('fluxer_renderer', RENDERER_SHA, cachedFeed()), true); + }); + + test('the cached manifest stands in for a missing record only for the same sha', () => { + const planner = createPlanner(createStore()); + + assert.equal(planner.isRunnable('fluxer_renderer', RENDERER_SHA, cachedFeed()), false); + assert.equal(planner.isRunnable('fluxer_renderer', 'f'.repeat(64), cachedFeed()), true); + }); }); describe('ModuleUpdatePlanner.evaluateUnreachableLaunch', () => { test('a store that never reached the manifest feed blocks with nothing-installed', async () => { const planner = createPlanner(createStore({committed: {fluxer_renderer: RENDERER_SHA}, lastManifestFetch: null})); - assert.deepEqual(await planner.evaluateUnreachableLaunch(true, false), { + assert.deepEqual(await planner.evaluateUnreachableLaunch(true), { kind: ModuleUnreachableLaunchDecision.BLOCK, reason: ModuleUpdaterBlockReason.NOTHING_INSTALLED, }); @@ -252,7 +350,7 @@ describe('ModuleUpdatePlanner.evaluateUnreachableLaunch', () => { test('an empty committed set with no offline renderer blocks with nothing-installed', async () => { const planner = createPlanner(createStore({committed: {}})); - assert.deepEqual(await planner.evaluateUnreachableLaunch(true, false), { + assert.deepEqual(await planner.evaluateUnreachableLaunch(true), { kind: ModuleUnreachableLaunchDecision.BLOCK, reason: ModuleUpdaterBlockReason.NOTHING_INSTALLED, }); @@ -261,25 +359,11 @@ describe('ModuleUpdatePlanner.evaluateUnreachableLaunch', () => { test('an offline renderer carries an empty committed set past the nothing-installed block', async () => { const planner = createPlanner(createStore({committed: {}}), {hasOfflineRenderer: true}); - assert.deepEqual(await planner.evaluateUnreachableLaunch(true, false), { + assert.deepEqual(await planner.evaluateUnreachableLaunch(true), { kind: ModuleUnreachableLaunchDecision.LAUNCH, }); }); - test('boot attempts at the rollback threshold block with rollback-exhausted', async () => { - const planner = createPlanner( - createStore({ - committed: {fluxer_renderer: RENDERER_SHA}, - bootAttempt: MODULE_BOOT_ATTEMPT_ROLLBACK_THRESHOLD, - }), - ); - - assert.deepEqual(await planner.evaluateUnreachableLaunch(true, false), { - kind: ModuleUnreachableLaunchDecision.BLOCK, - reason: ModuleUpdaterBlockReason.ROLLBACK_EXHAUSTED, - }); - }); - test('one boot attempt below the threshold still launches', async () => { const planner = createPlanner( createStore({ @@ -288,20 +372,11 @@ describe('ModuleUpdatePlanner.evaluateUnreachableLaunch', () => { }), ); - assert.deepEqual(await planner.evaluateUnreachableLaunch(true, false), { + assert.deepEqual(await planner.evaluateUnreachableLaunch(true), { kind: ModuleUnreachableLaunchDecision.LAUNCH, }); }); - test('a pending security update blocks with unreachable-below-floor', async () => { - const planner = createPlanner(createStore({committed: {fluxer_renderer: RENDERER_SHA}})); - - assert.deepEqual(await planner.evaluateUnreachableLaunch(false, true), { - kind: ModuleUnreachableLaunchDecision.BLOCK, - reason: ModuleUpdaterBlockReason.UNREACHABLE_BELOW_FLOOR, - }); - }); - test('a committed set behind the floor blocks only while startup updates are enforced', async () => { const store = createStore({ committed: {fluxer_renderer: RENDERER_SHA}, @@ -309,11 +384,11 @@ describe('ModuleUpdatePlanner.evaluateUnreachableLaunch', () => { }); const planner = createPlanner(store); - assert.deepEqual(await planner.evaluateUnreachableLaunch(true, false), { + assert.deepEqual(await planner.evaluateUnreachableLaunch(true), { kind: ModuleUnreachableLaunchDecision.BLOCK, reason: ModuleUpdaterBlockReason.UNREACHABLE_BELOW_FLOOR, }); - assert.deepEqual(await planner.evaluateUnreachableLaunch(false, false), { + assert.deepEqual(await planner.evaluateUnreachableLaunch(false), { kind: ModuleUnreachableLaunchDecision.LAUNCH, }); }); @@ -326,7 +401,7 @@ describe('ModuleUpdatePlanner.evaluateUnreachableLaunch', () => { }), ); - assert.deepEqual(await planner.evaluateUnreachableLaunch(false, false), { + assert.deepEqual(await planner.evaluateUnreachableLaunch(false), { kind: ModuleUnreachableLaunchDecision.BLOCK, reason: ModuleUpdaterBlockReason.UNREACHABLE_BELOW_FLOOR, }); @@ -340,7 +415,7 @@ describe('ModuleUpdatePlanner.evaluateUnreachableLaunch', () => { }), ); - assert.deepEqual(await planner.evaluateUnreachableLaunch(true, false), { + assert.deepEqual(await planner.evaluateUnreachableLaunch(true), { kind: ModuleUnreachableLaunchDecision.LAUNCH, }); }); @@ -439,20 +514,8 @@ describe('ModuleUpdatePlanner with a renderer bundled in the shell', () => { test('a fresh install that never reached the feed launches the bundled renderer', async () => { const planner = bundledPlanner(createStore({lastManifestFetch: null})); - assert.deepEqual(await planner.evaluateUnreachableLaunch(true, false), { + assert.deepEqual(await planner.evaluateUnreachableLaunch(true), { kind: ModuleUnreachableLaunchDecision.LAUNCH, }); }); - - test('the bundle meets a Linux security minimum it is at least as new as', async () => { - const planner = bundledPlanner(createStore()); - - assert.equal( - await planner.securityUpdateRequired({ - version: parseModuleVersion(SHELL_VERSION, 'minimum'), - requiredModules: ['fluxer_renderer'], - }), - false, - ); - }); }); diff --git a/fluxer_desktop/src/main/ModuleUpdatePlanner.ts b/fluxer_desktop/src/main/ModuleUpdatePlanner.ts index a5d5e1fe5..748508a39 100644 --- a/fluxer_desktop/src/main/ModuleUpdatePlanner.ts +++ b/fluxer_desktop/src/main/ModuleUpdatePlanner.ts @@ -1,12 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import { - blockingModuleNames, - type DesktopLinuxSecurityMinimum, - type DesktopModuleManifestEntry, - type DesktopModuleUpdateManifest, -} from '@electron/main/ModuleManifest'; -import {MODULE_BOOT_ATTEMPT_ROLLBACK_THRESHOLD, type ModuleStore} from '@electron/main/ModuleStore'; +import type {DesktopModuleManifestEntry, DesktopModuleUpdateManifest} from '@electron/main/ModuleManifest'; +import type {ModuleShellRange, ModuleStore} from '@electron/main/ModuleStore'; import {compareModuleVersions, type ModuleVersion, parseModuleVersion} from '@electron/main/ModuleVersion'; import {DESKTOP_RENDERER_MODULE_NAME} from '@fluxer/desktop_ipc/src/ModuleContract'; @@ -15,7 +10,6 @@ export const ModuleUpdaterBlockReason = Object.freeze({ UNREACHABLE_BELOW_FLOOR: 'unreachable-below-floor', BELOW_FLOOR: 'below-floor', NOTHING_INSTALLED: 'nothing-installed', - ROLLBACK_EXHAUSTED: 'rollback-exhausted', } as const); export type ModuleUpdaterBlockReason = (typeof ModuleUpdaterBlockReason)[keyof typeof ModuleUpdaterBlockReason]; @@ -178,20 +172,35 @@ export class ModuleUpdatePlanner { } public isShellCompatible(entry: DesktopModuleManifestEntry): boolean { - if (entry.minimumShellVersion != null && compareModuleVersions(this.shellVersion, entry.minimumShellVersion) < 0) { + return this.shellInRange(entry.minimumShellVersion, entry.maximumShellVersion); + } + + private shellInRange(minimum: ModuleVersion | null, maximum: ModuleVersion | null): boolean { + if (minimum != null && compareModuleVersions(this.shellVersion, minimum) < 0) { return false; } - if (entry.maximumShellVersion != null && compareModuleVersions(this.shellVersion, entry.maximumShellVersion) > 0) { + if (maximum != null && compareModuleVersions(this.shellVersion, maximum) > 0) { return false; } return true; } + private shellInRecordedRange(range: ModuleShellRange): boolean { + return this.shellInRange( + range.minimum_shell_version == null + ? null + : parseModuleVersion(range.minimum_shell_version, 'recorded module minimum shell version'), + range.maximum_shell_version == null + ? null + : parseModuleVersion(range.maximum_shell_version, 'recorded module maximum shell version'), + ); + } + public shellUpdateRequired(manifest: DesktopModuleUpdateManifest): boolean { if (compareModuleVersions(this.shellVersion, manifest.shell.minimumVersion) < 0) { return true; } - for (const moduleName of blockingModuleNames(manifest)) { + for (const moduleName of manifest.requiredModules) { const entry = manifest.modules[moduleName]; if (entry != null && !this.isShellCompatible(entry)) { return true; @@ -200,32 +209,47 @@ export class ModuleUpdatePlanner { return false; } - public async securityUpdateRequired(minimum: DesktopLinuxSecurityMinimum): Promise { - if (compareModuleVersions(this.shellVersion, minimum.version) < 0) { - return true; + public compatibleEntry(manifest: DesktopModuleUpdateManifest, moduleName: string): DesktopModuleManifestEntry | null { + if (!Object.hasOwn(manifest.modules, moduleName)) { + return null; } - const committed = this.store.getCommitted(); - for (const moduleName of minimum.requiredModules) { - if (moduleName === DESKTOP_RENDERER_MODULE_NAME && this.bundleCoversRendererAt(minimum.version)) { - continue; - } - const sha256 = committed[moduleName]; - if (sha256 == null) { - return true; - } - const installed = await this.store.getInstalledManifest(moduleName, sha256); - if (installed == null) { - return true; - } - const installedVersion = parseModuleVersion( - installed.build_version, - `installed module ${moduleName} build version`, - ); - if (compareModuleVersions(installedVersion, minimum.version) < 0) { - return true; - } + const entry = manifest.modules[moduleName]; + if (compareModuleVersions(this.shellVersion, manifest.shell.minimumVersion) < 0 || !this.isShellCompatible(entry)) { + return null; } - return false; + return entry; + } + + public isRendererAssetModule(moduleName: string): boolean { + return moduleName !== DESKTOP_RENDERER_MODULE_NAME && !moduleName.startsWith('fluxer_dict_'); + } + + public rendererFollowsFeed(manifest: DesktopModuleUpdateManifest): boolean { + return ( + manifest.modules[DESKTOP_RENDERER_MODULE_NAME] == null || + this.bundleCoversModule(DESKTOP_RENDERER_MODULE_NAME, manifest) || + this.compatibleEntry(manifest, DESKTOP_RENDERER_MODULE_NAME) != null + ); + } + + public skipsFeedEntry(manifest: DesktopModuleUpdateManifest, moduleName: string): boolean { + return ( + this.compatibleEntry(manifest, moduleName) == null || + this.bundleCoversModule(moduleName, manifest) || + (this.isRendererAssetModule(moduleName) && !this.rendererFollowsFeed(manifest)) + ); + } + + public isRunnable(moduleName: string, sha256: string, cached: DesktopModuleUpdateManifest | null): boolean { + const recorded = this.store.getShellRange(moduleName, sha256); + if (recorded != null) { + return this.shellInRecordedRange(recorded); + } + const entry = cached?.modules[moduleName]; + if (entry != null && entry.sha256 === sha256) { + return this.isShellCompatible(entry); + } + return true; } public async isInstalled(moduleName: string, sha256: string): Promise { @@ -234,8 +258,7 @@ export class ModuleUpdatePlanner { public async plan(manifest: DesktopModuleUpdateManifest): Promise { const committed = this.store.getCommitted(); - const blocking = blockingModuleNames(manifest); - const wanted = new Set([...Object.keys(committed), ...blocking]); + const wanted = new Set([...Object.keys(committed), ...manifest.requiredModules]); const items: Array = []; const base: Record = {}; for (const moduleName of Array.from(wanted).sort()) { @@ -243,7 +266,7 @@ export class ModuleUpdatePlanner { if (entry == null) { continue; } - if (!this.isShellCompatible(entry) || this.bundleCoversModule(moduleName, manifest)) { + if (this.skipsFeedEntry(manifest, moduleName)) { await this.retainCommitted(base, moduleName); continue; } @@ -259,7 +282,7 @@ export class ModuleUpdatePlanner { module: moduleName, entry, requirement: - blocking.has(moduleName) && !this.bundledModules().includes(moduleName) + manifest.requiredModules.includes(moduleName) && !this.bundledModules().includes(moduleName) ? ModulePlanItemRequirement.REQUIRED : ModulePlanItemRequirement.OPTIONAL, }); @@ -307,10 +330,7 @@ export class ModuleUpdatePlanner { return true; } - public async evaluateUnreachableLaunch( - startupUpdateEnforced: boolean, - securityUpdateRequired: boolean, - ): Promise { + public async evaluateUnreachableLaunch(startupUpdateEnforced: boolean): Promise { const state = this.store.getState(); if ( (state.last_manifest_fetch == null && !this.hasBundledRenderer()) || @@ -318,15 +338,6 @@ export class ModuleUpdatePlanner { ) { return {kind: ModuleUnreachableLaunchDecision.BLOCK, reason: ModuleUpdaterBlockReason.NOTHING_INSTALLED}; } - if (state.boot_attempt >= MODULE_BOOT_ATTEMPT_ROLLBACK_THRESHOLD) { - return {kind: ModuleUnreachableLaunchDecision.BLOCK, reason: ModuleUpdaterBlockReason.ROLLBACK_EXHAUSTED}; - } - if (securityUpdateRequired) { - return { - kind: ModuleUnreachableLaunchDecision.BLOCK, - reason: ModuleUpdaterBlockReason.UNREACHABLE_BELOW_FLOOR, - }; - } if ((await this.modulesBelowFloor(startupUpdateEnforced)).length > 0) { return { kind: ModuleUnreachableLaunchDecision.BLOCK, diff --git a/fluxer_desktop/src/main/ModuleUpdater.test.mjs b/fluxer_desktop/src/main/ModuleUpdater.test.mjs index d87fbfdc3..b17b7a5f4 100644 --- a/fluxer_desktop/src/main/ModuleUpdater.test.mjs +++ b/fluxer_desktop/src/main/ModuleUpdater.test.mjs @@ -72,6 +72,8 @@ function manifestBytes( bounds = {}, metadataVersion = 1, platform = PLATFORM, + buildVersion = SHELL_VERSION, + extra = {}, } = {}, ) { return Buffer.from( @@ -80,7 +82,7 @@ function manifestBytes( release_channel: RELEASE_CHANNEL, platform, arch: ARCH, - build_version: SHELL_VERSION, + build_version: buildVersion, pub_date: '2026-08-23T00:00:00.000Z', metadata_version: metadataVersion, shell: {latest_version: shellLatest, minimum_version: shellMinimum}, @@ -96,15 +98,16 @@ function manifestBytes( ]), ), required_modules: required ?? Object.keys(modules), + ...extra, }), 'utf8', ); } -async function openStore(userDataPath) { +async function openStore(userDataPath, shellVersion = SHELL_VERSION) { return await ModuleStore.open({ root: getModuleStoreRoot(userDataPath), - shellVersion: SHELL_VERSION, + shellVersion, releaseChannel: RELEASE_CHANNEL, }); } @@ -116,7 +119,7 @@ function createUpdater(store, modules, options = {}) { const reports = []; const updater = new ModuleUpdater({ store, - shellVersion: SHELL_VERSION, + shellVersion: options.shellVersion ?? SHELL_VERSION, releaseChannel: RELEASE_CHANNEL, platform: options.platform ?? PLATFORM, arch: ARCH, @@ -343,7 +346,6 @@ describe('ModuleUpdater.run', () => { assert.equal(outcome.status, 'blocked-shell-update'); assert.equal(outcome.latestVersion, '2026.824.0'); assert.equal(outcome.minimumVersion, '2026.824.0'); - assert.equal(outcome.requiredSecurityUpdate, false); assert.deepEqual(requests, [`${PACKAGE_ORIGIN}/desktop/${RELEASE_CHANNEL}/${PLATFORM}/${ARCH}/modules.json`]); assert.deepEqual(store.getCommitted(), {fluxer_renderer: RENDERER_SHA}); }); @@ -811,6 +813,7 @@ describe('ModuleUpdater checks without downloading and installs only when asked' forceStartupUpdate: true, selfUpdateShellFirst: async (latestVersion) => { steps.push(`shell ${latestVersion}`); + return {reason: 'install-failed'}; }, }, ); @@ -833,6 +836,7 @@ describe('ModuleUpdater checks without downloading and installs only when asked' forceStartupUpdate: true, selfUpdateShellFirst: async (latestVersion) => { steps.push(`shell ${latestVersion}`); + return {reason: 'install-failed'}; }, }, ); @@ -865,7 +869,7 @@ describe('ModuleUpdater checks without downloading and installs only when asked' assert.ok(requests.some((url) => String(url).endsWith('/modules.json'))); }); - test('a Linux boot forced only by a shell change launches the installed set at once when the download fails', async () => { + test('a forced Linux boot with a transient package error launches the committed set', async () => { const {store} = await storeOnRenderer(); const {updater, sleeps} = createUpdater( store, @@ -1145,41 +1149,423 @@ describe('ModuleUpdater with a renderer bundled in the shell', () => { assert.equal((await updater.selectServedModules(outcome.committed)).renderer.source, 'bundled'); assert.deepEqual(store.getCommitted(), {}); }); +}); - test('a Linux launch never waits on a shell download, a required security update still does', async () => { - const attempts = []; - const selfUpdateShellFirst = async (latestVersion) => { - attempts.push(latestVersion); +describe('ModuleUpdater keeps every shell on modules it can run', () => { + const REPORTER_SHELL = '2026.1008.32323'; + const LIVE_SHELL = '2026.1009.45608'; + const DICTIONARY_SHA = sha256('dictionary-one'); + const NEXT_DICTIONARY_SHA = sha256('dictionary-two'); + const FONT_SHA = sha256('font-one'); + const AHEAD = {fluxer_renderer: {minimum_shell_version: LIVE_SHELL}}; + + function feedAhead(overrides = {}) { + return { + shellVersion: REPORTER_SHELL, + shellLatest: LIVE_SHELL, + shellMinimum: '0.0.0', + buildVersion: LIVE_SHELL, + bounds: AHEAD, + ...overrides, }; - const store = await openStore(createUserData()); + } + + async function storeFor(shellVersion = REPORTER_SHELL) { + const userDataPath = createUserData(); + const store = await openStore(userDataPath, shellVersion); + return {store, storeRoot: getModuleStoreRoot(userDataPath)}; + } + + test('a Linux shell below the renderer floor launches its bundled renderer instead of asking for a shell update', async () => { + const {store, storeRoot} = await storeFor(); + installModuleOnDisk(storeRoot, 'fluxer_dict_en', DICTIONARY_SHA, [['en.dic', 'words']], LIVE_SHELL); + seedCachedPackage(store, DICTIONARY_SHA); + const statuses = []; + const {updater, requests} = createUpdater( + store, + {fluxer_renderer: NEXT_RENDERER_SHA, fluxer_dict_en: DICTIONARY_SHA}, + feedAhead({ + platform: 'linux', + bundledRendererVersion: REPORTER_SHELL, + forceStartupUpdate: true, + onState: (state) => statuses.push(state.status), + }), + ); + + const outcome = await updater.run(); + + assert.equal(outcome.status, 'launching'); + assert.deepEqual(outcome.committed, {fluxer_dict_en: DICTIONARY_SHA}); + assert.equal( + requests.some((url) => String(url).includes(NEXT_RENDERER_SHA)), + false, + ); + assert.equal(statuses.includes('blocked-shell-update'), false); + assert.equal((await updater.selectServedModules(outcome.committed)).renderer.source, 'bundled'); + }); + + test('a Flatpak shell Flathub has not updated yet keeps its renderer and still takes compatible modules', async () => { + const flathubShell = '2026.1012.154028'; + const liveShell = '2026.1013.15209'; + const {store, storeRoot} = await storeFor(flathubShell); + installModuleOnDisk(storeRoot, 'fluxer_renderer', RENDERER_SHA, [['index.js', 'renderer']], flathubShell); + installModuleOnDisk(storeRoot, 'fluxer_dict_en', DICTIONARY_SHA, [['en.dic', 'words']], flathubShell); + installModuleOnDisk(storeRoot, 'fluxer_dict_en', NEXT_DICTIONARY_SHA, [['en.dic', 'more words']], liveShell); + seedCachedPackage(store, NEXT_DICTIONARY_SHA); + await store.commit({fluxer_renderer: RENDERER_SHA, fluxer_dict_en: DICTIONARY_SHA}); + const statuses = []; + const {updater, requests} = createUpdater( + store, + {fluxer_renderer: NEXT_RENDERER_SHA, fluxer_dict_en: NEXT_DICTIONARY_SHA}, + feedAhead({ + shellVersion: flathubShell, + shellLatest: liveShell, + buildVersion: liveShell, + bounds: {fluxer_renderer: {minimum_shell_version: liveShell}}, + platform: 'linux', + forceStartupUpdate: true, + required: ['fluxer_renderer'], + onState: (state) => statuses.push(state.status), + }), + ); + + const outcome = await updater.run(); + + assert.equal(outcome.status, 'launching'); + assert.deepEqual(outcome.committed, {fluxer_dict_en: NEXT_DICTIONARY_SHA, fluxer_renderer: RENDERER_SHA}); + assert.equal( + requests.some((url) => String(url).includes(NEXT_RENDERER_SHA)), + false, + ); + assert.equal(statuses.includes('blocked-shell-update'), false); + assert.deepEqual(await updater.checkForUpdate(), { + shellLatestVersion: liveShell, + shellNewer: true, + modulesChanged: false, + }); + }); + + test('a Linux shell behind the floor keeps launching its committed compatible renderer', async () => { + const {store, storeRoot} = await storeFor(); + installModuleOnDisk(storeRoot, 'fluxer_renderer', RENDERER_SHA, [['index.js', 'renderer']], REPORTER_SHELL); + await store.commit({fluxer_renderer: RENDERER_SHA}); + const {updater, requests} = createUpdater( + store, + {fluxer_renderer: NEXT_RENDERER_SHA}, + feedAhead({platform: 'linux', forceStartupUpdate: true}), + ); + + const outcome = await updater.run(); + + assert.equal(outcome.status, 'launching'); + assert.deepEqual(outcome.committed, {fluxer_renderer: RENDERER_SHA}); + assert.equal( + requests.some((url) => String(url).endsWith('package.br')), + false, + ); + }); + + test('a renderer asset module is not replaced while the served renderer is older than the feed', async () => { + const {store, storeRoot} = await storeFor(); + installModuleOnDisk(storeRoot, 'fluxer_renderer', RENDERER_SHA, [['index.js', 'renderer']], REPORTER_SHELL); + installModuleOnDisk(storeRoot, 'fluxer_overlay', OVERLAY_SHA, [['overlay.js', 'overlay']], REPORTER_SHELL); + await store.commit({fluxer_renderer: RENDERER_SHA, fluxer_overlay: OVERLAY_SHA}); + const {updater, requests} = createUpdater( + store, + {fluxer_renderer: NEXT_RENDERER_SHA, fluxer_overlay: NEXT_OVERLAY_SHA}, + feedAhead({platform: 'linux', forceStartupUpdate: true, required: ['fluxer_renderer']}), + ); + + const outcome = await updater.run(); + + assert.equal(outcome.status, 'launching'); + assert.deepEqual(outcome.committed, {fluxer_overlay: OVERLAY_SHA, fluxer_renderer: RENDERER_SHA}); + assert.equal( + requests.some((url) => String(url).endsWith('package.br')), + false, + ); + assert.deepEqual(await updater.checkForUpdate(), { + shellLatestVersion: LIVE_SHELL, + shellNewer: true, + modulesChanged: false, + }); + }); + + test('an on-demand renderer asset is unavailable while the served renderer is older than the feed', async () => { + const {store} = await storeFor(); + const {updater, requests} = createUpdater( + store, + {fluxer_renderer: NEXT_RENDERER_SHA, fluxer_fonts_jp: FONT_SHA}, + feedAhead({platform: 'linux', bundledRendererVersion: REPORTER_SHELL, required: ['fluxer_renderer']}), + ); + + assert.deepEqual(await updater.ensureModule('fluxer_fonts_jp'), {module: 'fluxer_fonts_jp', status: 'unavailable'}); + assert.equal( + requests.some((url) => String(url).endsWith('package.br')), + false, + ); + }); + + test('a committed renderer asset stays installed while the served renderer is older than the feed', async () => { + const {store, storeRoot} = await storeFor(); + installModuleOnDisk(storeRoot, 'fluxer_renderer', RENDERER_SHA, [['index.js', 'renderer']], REPORTER_SHELL); + installModuleOnDisk(storeRoot, 'fluxer_fonts_jp', FONT_SHA, [['font.woff2', 'font']], REPORTER_SHELL); + await store.commit({fluxer_renderer: RENDERER_SHA, fluxer_fonts_jp: FONT_SHA}); + const {updater} = createUpdater( + store, + {fluxer_renderer: NEXT_RENDERER_SHA, fluxer_fonts_jp: sha256('font-two')}, + feedAhead({platform: 'linux', required: ['fluxer_renderer']}), + ); + + assert.deepEqual(await updater.ensureModule('fluxer_fonts_jp'), { + module: 'fluxer_fonts_jp', + status: 'already-installed', + }); + }); + + test('an on-demand dictionary installs while the renderer needs a newer shell', async () => { + const {store, storeRoot} = await storeFor(); + installModuleOnDisk(storeRoot, 'fluxer_dict_en', DICTIONARY_SHA, [['en.dic', 'words']], LIVE_SHELL); + seedCachedPackage(store, DICTIONARY_SHA); + const {updater} = createUpdater( + store, + {fluxer_renderer: NEXT_RENDERER_SHA, fluxer_dict_en: DICTIONARY_SHA}, + feedAhead({platform: 'linux', bundledRendererVersion: REPORTER_SHELL, required: ['fluxer_renderer']}), + ); + + assert.deepEqual(await updater.ensureModule('fluxer_dict_en'), {module: 'fluxer_dict_en', status: 'installed'}); + assert.deepEqual(store.getCommitted(), {fluxer_dict_en: DICTIONARY_SHA}); + assert.deepEqual(store.getShellRange('fluxer_dict_en', DICTIONARY_SHA), { + minimum_shell_version: null, + maximum_shell_version: null, + }); + }); + + test('an AppImage updates its shell first on a forced boot and launches the compatible set when that fails', async () => { + const {store, storeRoot} = await storeFor(); + installModuleOnDisk(storeRoot, 'fluxer_renderer', RENDERER_SHA, [['index.js', 'renderer']], REPORTER_SHELL); + await store.commit({fluxer_renderer: RENDERER_SHA}); + const attempts = []; + const {updater} = createUpdater( + store, + {fluxer_renderer: NEXT_RENDERER_SHA}, + feedAhead({ + platform: 'linux', + forceStartupUpdate: true, + selfUpdateShellFirst: async (latestVersion) => { + attempts.push(latestVersion); + return {reason: 'download-failed'}; + }, + }), + ); + + const outcome = await updater.run(); + + assert.deepEqual(attempts, [LIVE_SHELL]); + assert.equal(outcome.status, 'launching'); + assert.deepEqual(outcome.committed, {fluxer_renderer: RENDERER_SHA}); + }); + + test('a Windows shell below the floor launches the compatible set when the shell feed has no newer shell', async () => { + const {store, storeRoot} = await storeFor(); + installModuleOnDisk(storeRoot, 'fluxer_renderer', RENDERER_SHA, [['index.js', 'renderer']], REPORTER_SHELL); + await store.commit({fluxer_renderer: RENDERER_SHA}); + const {updater} = createUpdater( + store, + {fluxer_renderer: NEXT_RENDERER_SHA}, + feedAhead({platform: 'win32', selfUpdateShellFirst: async () => ({reason: 'no-update'})}), + ); + + const outcome = await updater.run(); + + assert.equal(outcome.status, 'launching'); + assert.deepEqual(outcome.committed, {fluxer_renderer: RENDERER_SHA}); + assert.deepEqual(store.getState().floor, {}); + }); + + test('a Windows shell below the floor installs a compatible module and launches when the shell feed has no newer shell', async () => { + const {store, storeRoot} = await storeFor(); + installModuleOnDisk(storeRoot, 'fluxer_renderer', RENDERER_SHA, [['index.js', 'renderer']], REPORTER_SHELL); + installModuleOnDisk(storeRoot, 'fluxer_dict_en', DICTIONARY_SHA, [['en.dic', 'words']], LIVE_SHELL); + seedCachedPackage(store, DICTIONARY_SHA); + await store.commit({fluxer_renderer: RENDERER_SHA}); + const attempts = []; + const {updater} = createUpdater( + store, + {fluxer_renderer: NEXT_RENDERER_SHA, fluxer_dict_en: DICTIONARY_SHA}, + feedAhead({ + platform: 'win32', + selfUpdateShellFirst: async (latestVersion) => { + attempts.push(latestVersion); + return {reason: 'no-update'}; + }, + }), + ); + + const outcome = await updater.run(); + + assert.deepEqual(attempts, [LIVE_SHELL]); + assert.equal(outcome.status, 'launching'); + assert.deepEqual(outcome.committed, {fluxer_dict_en: DICTIONARY_SHA, fluxer_renderer: RENDERER_SHA}); + }); + + test('a Windows shell below the floor whose self update fails is blocked', async () => { + const {store, storeRoot} = await storeFor(); + installModuleOnDisk(storeRoot, 'fluxer_renderer', RENDERER_SHA, [['index.js', 'renderer']], REPORTER_SHELL); + await store.commit({fluxer_renderer: RENDERER_SHA}); + const {updater} = createUpdater( + store, + {fluxer_renderer: NEXT_RENDERER_SHA}, + feedAhead({platform: 'win32', selfUpdateShellFirst: async () => ({reason: 'download-failed'})}), + ); + + const outcome = await updater.run(); + + assert.equal(outcome.status, 'blocked-shell-update'); + assert.equal(outcome.latestVersion, LIVE_SHELL); + assert.deepEqual(store.getCommitted(), {fluxer_renderer: RENDERER_SHA}); + }); + + test('a Windows floor keeps the renderer it already enforced when the feed raises it past this shell', async () => { + const {store, storeRoot} = await storeFor(); + installModuleOnDisk(storeRoot, 'fluxer_renderer', RENDERER_SHA, [['index.js', 'renderer']], REPORTER_SHELL); + seedCachedPackage(store, RENDERER_SHA); + const first = createUpdater( + store, + {fluxer_renderer: RENDERER_SHA}, + feedAhead({ + platform: 'win32', + shellLatest: REPORTER_SHELL, + buildVersion: REPORTER_SHELL, + bounds: {}, + required: ['fluxer_renderer'], + }), + ); + assert.equal((await first.updater.run()).status, 'launching'); + assert.deepEqual(store.getState().floor, {fluxer_renderer: RENDERER_SHA}); + const raised = createUpdater( + store, + {fluxer_renderer: NEXT_RENDERER_SHA}, + feedAhead({ + platform: 'win32', + metadataVersion: 2, + required: ['fluxer_renderer'], + selfUpdateShellFirst: async () => ({reason: 'download-failed'}), + }), + ); + + assert.equal((await raised.updater.run()).status, 'blocked-shell-update'); + assert.deepEqual(store.getState().floor, {fluxer_renderer: RENDERER_SHA}); + }); + + test('a renderer installed for a newer shell is retired after a shell downgrade and the bundle serves', async () => { + const {store, storeRoot} = await storeFor(); + installModuleOnDisk(storeRoot, 'fluxer_renderer', NEXT_RENDERER_SHA, [['index.js', 'renderer']], LIVE_SHELL); + await store.commit({fluxer_renderer: NEXT_RENDERER_SHA}); + await store.recordShellRanges([ + {module: 'fluxer_renderer', sha256: NEXT_RENDERER_SHA, minimum: LIVE_SHELL, maximum: null}, + ]); + const {updater, reports} = createUpdater( + store, + {}, + {shellVersion: REPORTER_SHELL, platform: 'linux', bundledRendererVersion: REPORTER_SHELL}, + ); + + const outcome = await updater.run(); + + assert.equal(outcome.status, 'launching'); + assert.deepEqual(outcome.committed, {}); + assert.deepEqual( + reports.filter((report) => report.type === 'shell-incompatible-retired').map((report) => report.module), + ['fluxer_renderer'], + ); + assert.equal((await updater.selectServedModules(outcome.committed)).renderer.source, 'bundled'); + }); + + test('a renderer with no recorded range is retired when the cached manifest says this shell cannot run it', async () => { + const {store, storeRoot} = await storeFor(); + installModuleOnDisk(storeRoot, 'fluxer_renderer', NEXT_RENDERER_SHA, [['index.js', 'renderer']], LIVE_SHELL); + await store.commit({fluxer_renderer: NEXT_RENDERER_SHA}); + writeFileSync( + path.join(store.root, 'manifest.json'), + manifestBytes({fluxer_renderer: NEXT_RENDERER_SHA}, feedAhead({platform: 'linux'})), + ); + const {updater, reports} = createUpdater( + store, + {}, + {shellVersion: REPORTER_SHELL, platform: 'linux', bundledRendererVersion: REPORTER_SHELL}, + ); + + const outcome = await updater.run(); + + assert.deepEqual(outcome.committed, {}); + assert.equal( + reports.some((report) => report.type === 'shell-incompatible-retired'), + true, + ); + }); + + test('a crash-loop rollback never restores a renderer this shell cannot run', async () => { + const {store, storeRoot} = await storeFor(); + installModuleOnDisk(storeRoot, 'fluxer_renderer', NEXT_RENDERER_SHA, [['index.js', 'ahead']], LIVE_SHELL); + installModuleOnDisk(storeRoot, 'fluxer_renderer', RENDERER_SHA, [['index.js', 'current']], LIVE_SHELL); + await store.commit({fluxer_renderer: NEXT_RENDERER_SHA}); + await store.commit({fluxer_renderer: RENDERER_SHA}); + await store.recordShellRanges([ + {module: 'fluxer_renderer', sha256: NEXT_RENDERER_SHA, minimum: LIVE_SHELL, maximum: null}, + ]); + await store.recordLaunchAttempt(); + await store.recordLaunchAttempt(); const {updater} = createUpdater( store, {}, - { - bundledRendererVersion: SHELL_VERSION, + {shellVersion: REPORTER_SHELL, platform: 'linux', bundledRendererVersion: REPORTER_SHELL}, + ); + + const outcome = await updater.run(); + + assert.equal(outcome.status, 'launching'); + assert.equal(outcome.committed.fluxer_renderer, undefined); + assert.equal(store.getState().previous.fluxer_renderer, undefined); + assert.equal(store.isRejected('fluxer_renderer', RENDERER_SHA), true); + }); + + test('a check offers compatible modules while the renderer needs a newer shell', async () => { + const {store, storeRoot} = await storeFor(); + installModuleOnDisk(storeRoot, 'fluxer_dict_en', DICTIONARY_SHA, [['en.dic', 'words']], REPORTER_SHELL); + await store.commit({fluxer_dict_en: DICTIONARY_SHA}); + const {updater} = createUpdater( + store, + {fluxer_renderer: NEXT_RENDERER_SHA, fluxer_dict_en: NEXT_DICTIONARY_SHA}, + feedAhead({platform: 'linux', bundledRendererVersion: REPORTER_SHELL}), + ); + + assert.deepEqual(await updater.checkForUpdate(), { + shellLatestVersion: LIVE_SHELL, + shellNewer: true, + modulesChanged: true, + }); + }); + + test('a Linux launch never blocks on linux_security_minimum in the manifest', async () => { + const {store, storeRoot} = await storeFor(); + installModuleOnDisk(storeRoot, 'fluxer_renderer', RENDERER_SHA, [['index.js', 'renderer']], REPORTER_SHELL); + await store.commit({fluxer_renderer: RENDERER_SHA}); + const {updater} = createUpdater( + store, + {fluxer_renderer: NEXT_RENDERER_SHA}, + feedAhead({ platform: 'linux', forceStartupUpdate: true, - shellLatest: '2026.900.1', - shellMinimum: '0.0.0', - selfUpdateShellFirst, - }, + shellMinimum: LIVE_SHELL, + extra: {linux_security_minimum: {version: LIVE_SHELL, required_modules: ['fluxer_renderer']}}, + }), ); - assert.equal((await updater.run()).status, 'launching'); - assert.deepEqual(attempts, []); + const outcome = await updater.run(); - const mac = createUpdater( - await openStore(createUserData()), - {}, - { - bundledRendererVersion: SHELL_VERSION, - forceStartupUpdate: true, - shellLatest: '2026.900.1', - shellMinimum: '0.0.0', - selfUpdateShellFirst, - }, - ); - await mac.updater.run(); - assert.deepEqual(attempts, ['2026.900.1']); + assert.equal(outcome.status, 'launching'); + assert.deepEqual(outcome.committed, {fluxer_renderer: RENDERER_SHA}); + assert.equal(Object.hasOwn(updater.getLastState(), 'requiredSecurityUpdate'), false); }); }); diff --git a/fluxer_desktop/src/main/ModuleUpdater.ts b/fluxer_desktop/src/main/ModuleUpdater.ts index 5cedc516a..40b614da5 100644 --- a/fluxer_desktop/src/main/ModuleUpdater.ts +++ b/fluxer_desktop/src/main/ModuleUpdater.ts @@ -6,12 +6,7 @@ import {errorMessage} from '@electron/common/ErrorMessage'; import {jitteredExponentialDelay} from '@electron/common/ExponentialBackoff'; import type {DesktopUpdateProbe} from '@electron/main/DesktopUpdateRun'; import {ModuleDownloadRate} from '@electron/main/ModuleDownloadRate'; -import { - blockingModuleNames, - type DesktopLinuxSecurityMinimum, - type DesktopModuleUpdateManifest, - resolveDesktopModuleArchitecture, -} from '@electron/main/ModuleManifest'; +import {type DesktopModuleUpdateManifest, resolveDesktopModuleArchitecture} from '@electron/main/ModuleManifest'; import { type ModuleManifestDocument, ModuleManifestFetchError, @@ -29,7 +24,6 @@ import { ModulePackageInstallPhase, } from '@electron/main/ModulePackageInstaller'; import { - type LinuxModuleSecurityMinimum, MODULE_BOOT_ATTEMPT_ROLLBACK_THRESHOLD, type ModuleLaunchAttempt, ModuleManifestEquivocationError, @@ -86,6 +80,7 @@ const ModuleUpdaterReportType = Object.freeze({ ROLLBACK: 'rollback', ROLLBACK_FLOOR_BYPASS: 'rollback-floor-bypass', BUNDLED_RENDERER_PREFERRED: 'bundled-renderer-preferred', + SHELL_INCOMPATIBLE_RETIRED: 'shell-incompatible-retired', BLOCKED: 'blocked', } as const); @@ -102,6 +97,7 @@ const MODULE_UPDATER_REPORT_SAMPLE_RATES: Readonly; readonly message: string; - readonly requiredSecurityUpdate: boolean; readonly updateServerUnreachable: boolean; } | { readonly status: typeof ModuleUpdaterStatus.BLOCKED_SHELL_UPDATE; readonly latestVersion: string; readonly minimumVersion: string; - readonly requiredSecurityUpdate: boolean; }; class ModuleUpdaterStorageFullError extends Error { @@ -256,14 +249,6 @@ interface ModulePlanResult { readonly blockedError: unknown; } -const ModuleStartupUpdatePolicy = Object.freeze({ - REQUIRED: 'required', - OPTIONAL: 'optional', - REQUIRED_SECURITY: 'required-security', -} as const); - -type ModuleStartupUpdatePolicy = (typeof ModuleStartupUpdatePolicy)[keyof typeof ModuleStartupUpdatePolicy]; - const ModuleManifestObservationMode = Object.freeze({ READ_ONLY: 'read-only', RECORD_FETCH: 'record-fetch', @@ -282,7 +267,7 @@ interface ModuleUpdaterOptions { readonly bundledRendererVersion?: string | null; readonly preferUnversionedBundle?: boolean; readonly forceStartupUpdate?: boolean; - readonly selfUpdateShellFirst?: (latestVersion: string, requiredSecurityUpdate: boolean) => Promise; + readonly selfUpdateShellFirst?: (latestVersion: string) => Promise<{readonly reason: string}>; readonly onState?: (state: ModuleUpdaterSplashState) => void; readonly report?: (report: ModuleUpdaterReport) => void; readonly fetch: typeof globalThis.fetch; @@ -301,10 +286,8 @@ export class ModuleUpdater { private readonly shellVersion: ModuleVersion; private readonly hasOfflineRenderer: boolean; private readonly forceStartupUpdate: boolean; - private readonly selfUpdateShellFirst: - | ((latestVersion: string, requiredSecurityUpdate: boolean) => Promise) - | null; - private readonly shellFirstAttempted = new Set(); + private readonly selfUpdateShellFirst: ((latestVersion: string) => Promise<{readonly reason: string}>) | null; + private readonly shellFirstAttempts = new Map(); private readonly manifestFeed: ModuleManifestFeedIdentity; private readonly manifestRepository: ModuleManifestRepository; private readonly planner: ModuleUpdatePlanner; @@ -321,7 +304,7 @@ export class ModuleUpdater { private lastError: string | null = null; private lastErrorAt: number | null = null; private ensureBlockedUntil = 0; - private startupUpdatePolicy: ModuleStartupUpdatePolicy; + private readonly startupUpdateEnforced: boolean; private manifestObservationTail: Promise = Promise.resolve(); private recordedManifestSha256: string | null = null; private lastAccepted: {readonly document: ModuleManifestDocument; readonly metadataVersion: number} | null = null; @@ -381,11 +364,9 @@ export class ModuleUpdater { }, report: (report) => this.handlePackageInstallerReport(report), }); - this.startupUpdatePolicy = - this.platform === 'linux' ? ModuleStartupUpdatePolicy.OPTIONAL : ModuleStartupUpdatePolicy.REQUIRED; + this.startupUpdateEnforced = this.platform !== 'linux'; this.lastState = { status: ModuleUpdaterStatus.CHECKING, - requiredSecurityUpdate: false, current: null, total: null, progress: null, @@ -423,10 +404,10 @@ export class ModuleUpdater { let attempt = await this.store.beginBootAttempt({bundled: this.planner.bundledModules()}); const retiredRenderer = await this.retireRendererCoveredByBundle(); const retiredMissing = await this.retireUninstalledModules(); - if (retiredRenderer || retiredMissing) { + const retiredIncompatible = await this.retireIncompatibleModules(); + if (retiredRenderer || retiredMissing || retiredIncompatible) { attempt = {...attempt, committed: this.store.getCommitted()}; } - await this.restoreCachedUpdatePolicy(); if (attempt.rolledBack) { this.report({ type: ModuleUpdaterReportType.ROLLBACK, @@ -450,25 +431,22 @@ export class ModuleUpdater { const document = await this.manifestRepository.fetchLatest(); fetched = true; const manifest = await this.acceptManifest(document); - const startupUpdateRequired = await this.startupUpdateRequired(); - if (!startupUpdateRequired) { + if (!(await this.startupUpdateRequired())) { return await this.launchIfAtFloor(); } - if (this.planner.shellUpdateRequired(manifest) && (await this.onlyForcedByShellChange())) { - return await this.launchIfAtFloor(); - } - if (this.planner.shellUpdateRequired(manifest)) { + const shellFirst = await this.updateShellFirst(manifest); + if ( + this.startupUpdateEnforced && + this.planner.shellUpdateRequired(manifest) && + shellFirst?.reason !== 'no-update' + ) { this.emit(ModuleUpdaterStatus.BLOCKED_SHELL_UPDATE); return { status: ModuleUpdaterStatus.BLOCKED_SHELL_UPDATE, latestVersion: manifest.shell.latestVersion.source, minimumVersion: manifest.shell.minimumVersion.source, - requiredSecurityUpdate: this.isSecurityUpdateRequired(), }; } - if (await this.updateShellFirst(manifest)) { - this.emit(ModuleUpdaterStatus.CHECKING); - } const plan = await this.planner.plan(manifest); if (plan.items.length === 0) { await this.commitIfChanged(plan.base); @@ -487,7 +465,6 @@ export class ModuleUpdater { this.emit(ModuleUpdaterStatus.VERIFYING, {current: plan.items.length, total: plan.items.length}); } await this.commitIfChanged(executed.desired); - await this.applyUpdatePolicy(manifest); if (executed.installed.length === 0) { return await this.launchIfAtFloor(); } @@ -505,12 +482,9 @@ export class ModuleUpdater { const exhausted = backoff.reachedCeiling || error instanceof ModuleManifestEquivocationError || - (await this.onlyForcedByShellChange()); + (await this.forcedButLaunchable()); if (exhausted || !fetched) { - const decision = await this.planner.evaluateUnreachableLaunch( - this.isStartupUpdateEnforced(), - this.isSecurityUpdateRequired(), - ); + const decision = await this.planner.evaluateUnreachableLaunch(this.startupUpdateEnforced); if (decision.kind === ModuleUnreachableLaunchDecision.LAUNCH) { const launchAttempt = await this.store.recordLaunchAttempt(); this.emit(ModuleUpdaterStatus.UNREACHABLE_LAUNCH); @@ -558,20 +532,17 @@ export class ModuleUpdater { this.reportFailure(error); return {module: moduleName, status: DesktopModuleEnsureStatus.UNAVAILABLE}; } - if (this.planner.shellUpdateRequired(manifest)) { - return {module: moduleName, status: DesktopModuleEnsureStatus.UNAVAILABLE}; - } - if (!Object.hasOwn(manifest.modules, moduleName)) { - return {module: moduleName, status: DesktopModuleEnsureStatus.UNAVAILABLE}; - } - const entry = manifest.modules[moduleName]; - if (!this.planner.isShellCompatible(entry)) { - return {module: moduleName, status: DesktopModuleEnsureStatus.UNAVAILABLE}; - } const committed = this.store.getCommitted()[moduleName]; if (committed != null && (await this.planner.isInstalled(moduleName, committed))) { return {module: moduleName, status: DesktopModuleEnsureStatus.ALREADY_INSTALLED}; } + const entry = this.planner.compatibleEntry(manifest, moduleName); + if ( + entry == null || + (this.planner.isRendererAssetModule(moduleName) && !this.planner.rendererFollowsFeed(manifest)) + ) { + return {module: moduleName, status: DesktopModuleEnsureStatus.UNAVAILABLE}; + } if (this.store.isRejected(moduleName, entry.sha256)) { return {module: moduleName, status: DesktopModuleEnsureStatus.UNAVAILABLE}; } @@ -625,23 +596,50 @@ export class ModuleUpdater { return retired; } + private async retireIncompatibleModules(): Promise { + const cached = await this.readCachedManifest(); + const retired = await this.store.retireIncompatible((moduleName, sha256) => + this.planner.isRunnable(moduleName, sha256, cached), + ); + for (const moduleName of retired) { + this.report({ + type: ModuleUpdaterReportType.SHELL_INCOMPATIBLE_RETIRED, + module: moduleName, + message: `retired module ${moduleName} because shell ${this.shellVersion.source} cannot run it`, + }); + } + return retired.length > 0; + } + + private async readCachedManifest(): Promise { + try { + const document = await this.manifestRepository.readCached(); + return document == null ? null : this.manifestRepository.parse(document); + } catch { + return null; + } + } + private async resolveManifestForEnsure(): Promise { return this.acceptManifest(await this.manifestRepository.resolveMemoizedDocument()); } - private async updateShellFirst(manifest: DesktopModuleUpdateManifest): Promise { + private async updateShellFirst(manifest: DesktopModuleUpdateManifest): Promise<{readonly reason: string} | null> { const latestVersion = manifest.shell.latestVersion.source; if ( this.selfUpdateShellFirst == null || - compareModuleVersions(manifest.shell.latestVersion, this.shellVersion) <= 0 || - this.shellFirstAttempted.has(latestVersion) || - (this.platform === 'linux' && !this.isSecurityUpdateRequired()) + compareModuleVersions(manifest.shell.latestVersion, this.shellVersion) <= 0 ) { - return false; + return null; } - this.shellFirstAttempted.add(latestVersion); - await this.selfUpdateShellFirst(latestVersion, this.isSecurityUpdateRequired()); - return true; + if (this.shellFirstAttempts.has(latestVersion)) { + return this.shellFirstAttempts.get(latestVersion) ?? null; + } + this.shellFirstAttempts.set(latestVersion, null); + const result = await this.selfUpdateShellFirst(latestVersion); + this.shellFirstAttempts.set(latestVersion, result); + this.emit(ModuleUpdaterStatus.CHECKING); + return result; } public async checkForUpdate(): Promise { @@ -658,9 +656,6 @@ export class ModuleUpdater { : manifest.shell.latestVersion; const shellLatestVersion = shellLatest.source; const shellNewer = compareModuleVersions(shellLatest, this.shellVersion) > 0; - if (this.planner.shellUpdateRequired(manifest)) { - return {shellLatestVersion, shellNewer, modulesChanged: false}; - } return { shellLatestVersion, shellNewer, @@ -672,9 +667,6 @@ export class ModuleUpdater { this.emit(ModuleUpdaterStatus.CHECKING); this.downloadRate.reset(); const manifest = await this.fetchFreshManifest(ModuleManifestObservationMode.RECORD_FETCH); - if (this.planner.shellUpdateRequired(manifest)) { - return null; - } const items = await this.collectPendingItems(manifest, this.planner); const {installed, unresolved} = await this.installItems(items); if (Object.keys(installed).length > 0) { @@ -711,11 +703,11 @@ export class ModuleUpdater { planner: ModuleUpdatePlanner, ): Promise> { const committed = this.store.getCommitted(); - const names = new Set([...Object.keys(committed), ...blockingModuleNames(manifest)]); + const names = new Set([...Object.keys(committed), ...manifest.requiredModules]); const wanted: Array = []; for (const moduleName of Array.from(names).sort()) { const entry = manifest.modules[moduleName]; - if (entry == null || !planner.isShellCompatible(entry) || planner.bundleCoversModule(moduleName, manifest)) { + if (entry == null || planner.skipsFeedEntry(manifest, moduleName)) { continue; } if (committed[moduleName] === entry.sha256 && (await planner.isInstalled(moduleName, entry.sha256))) { @@ -783,14 +775,7 @@ export class ModuleUpdater { } private async launchIfAtFloor(): Promise { - if (this.isSecurityUpdateRequired()) { - return this.blockUpdateRequired( - ModuleUpdaterBlockReason.BELOW_FLOOR, - [], - 'the installed shell or module set is below the required Linux security minimum', - ); - } - const below = await this.planner.modulesBelowFloor(this.isStartupUpdateEnforced()); + const below = await this.planner.modulesBelowFloor(this.startupUpdateEnforced); if (below.length > 0) { return this.blockUpdateRequired( ModuleUpdaterBlockReason.BELOW_FLOOR, @@ -820,9 +805,6 @@ export class ModuleUpdater { private async launchRevertedSetBypassingFloor( committed: Readonly>, ): Promise { - if (this.isSecurityUpdateRequired()) { - return null; - } const names = Object.keys(committed); if (names.length === 0) { return null; @@ -832,7 +814,7 @@ export class ModuleUpdater { return null; } } - const belowFloor = await this.planner.modulesBelowFloor(this.isStartupUpdateEnforced()); + const belowFloor = await this.planner.modulesBelowFloor(this.startupUpdateEnforced); if (belowFloor.length > 0) { this.report({ type: ModuleUpdaterReportType.ROLLBACK_FLOOR_BYPASS, @@ -867,87 +849,10 @@ export class ModuleUpdater { reason, modules, message, - requiredSecurityUpdate: this.isSecurityUpdateRequired(), updateServerUnreachable, }; } - private async applyUpdatePolicy(manifest: DesktopModuleUpdateManifest): Promise { - if (this.platform !== 'linux') { - this.startupUpdatePolicy = ModuleStartupUpdatePolicy.REQUIRED; - return; - } - await this.applyLinuxUpdatePolicy(manifest.linuxSecurityMinimum); - } - - private persistedLinuxSecurityMinimum(): DesktopLinuxSecurityMinimum | null { - const persisted = this.store.getState().linux_security_minimum; - if (persisted == null) { - return null; - } - return { - version: parseModuleVersion(persisted.version, 'persisted Linux security minimum'), - requiredModules: persisted.requiredModules, - }; - } - - private effectiveLinuxSecurityMinimum( - manifestMinimum: DesktopLinuxSecurityMinimum | null, - ): DesktopLinuxSecurityMinimum | null { - const persisted = this.persistedLinuxSecurityMinimum(); - if (manifestMinimum == null) { - return persisted; - } - if (persisted == null) { - return manifestMinimum; - } - const comparison = compareModuleVersions(manifestMinimum.version, persisted.version); - if (comparison < 0) { - throw new Error( - `manifest attempts to lower the Linux security minimum from ${persisted.version.source} to ${manifestMinimum.version.source}`, - ); - } - if (comparison === 0 && manifestMinimum.requiredModules.join('\0') !== persisted.requiredModules.join('\0')) { - throw new Error(`manifest changes the required modules at Linux security minimum ${persisted.version.source}`); - } - return manifestMinimum; - } - - private async applyLinuxUpdatePolicy(manifestMinimum: DesktopLinuxSecurityMinimum | null): Promise { - const minimum = this.effectiveLinuxSecurityMinimum(manifestMinimum); - if (minimum != null) { - this.startupUpdatePolicy = (await this.planner.securityUpdateRequired(minimum)) - ? ModuleStartupUpdatePolicy.REQUIRED_SECURITY - : ModuleStartupUpdatePolicy.OPTIONAL; - return; - } - this.startupUpdatePolicy = ModuleStartupUpdatePolicy.OPTIONAL; - } - - private isStartupUpdateEnforced(): boolean { - return this.startupUpdatePolicy !== ModuleStartupUpdatePolicy.OPTIONAL; - } - - private isSecurityUpdateRequired(): boolean { - return this.startupUpdatePolicy === ModuleStartupUpdatePolicy.REQUIRED_SECURITY; - } - - private async restoreCachedUpdatePolicy(): Promise { - if (this.platform !== 'linux') { - return; - } - await this.applyLinuxUpdatePolicy(null); - const document = await this.manifestRepository.readCached(); - if (document == null) { - return; - } - try { - await this.observeManifest(document, ModuleManifestObservationMode.READ_ONLY); - } catch (error) { - this.reportFailure(error); - } - } - private observeManifest( document: ModuleManifestDocument, mode: ModuleManifestObservationMode = ModuleManifestObservationMode.RECORD_FETCH, @@ -964,7 +869,7 @@ export class ModuleUpdater { manifestSha256: createHash('sha256').update(document.bytes).digest('hex'), }; this.store.requireManifestFresh(observation); - await this.applyUpdatePolicy(manifest); + await this.backfillShellRanges(manifest); if ( mode === ModuleManifestObservationMode.RECORD_FETCH && this.recordedManifestSha256 !== observation.manifestSha256 @@ -983,16 +888,29 @@ export class ModuleUpdater { return observed; } - private async onlyForcedByShellChange(): Promise { - return ( - this.forceStartupUpdate && !this.isStartupUpdateEnforced() && (await this.planner.canLaunchCommittedModules()) - ); + private async backfillShellRanges(manifest: DesktopModuleUpdateManifest): Promise { + const ranges = []; + for (const [moduleName, sha256] of Object.entries(this.store.getCommitted())) { + const entry = manifest.modules[moduleName]; + if (entry?.sha256 !== sha256) { + continue; + } + ranges.push({ + module: moduleName, + sha256, + minimum: entry.minimumShellVersion?.source ?? null, + maximum: entry.maximumShellVersion?.source ?? null, + }); + } + await this.store.recordShellRanges(ranges); + } + + private async forcedButLaunchable(): Promise { + return this.forceStartupUpdate && !this.startupUpdateEnforced && (await this.planner.canLaunchCommittedModules()); } private async startupUpdateRequired(): Promise { - return ( - this.forceStartupUpdate || this.isStartupUpdateEnforced() || !(await this.planner.canLaunchCommittedModules()) - ); + return this.forceStartupUpdate || this.startupUpdateEnforced || !(await this.planner.canLaunchCommittedModules()); } private async execute(plan: ModulePlan): Promise { @@ -1041,20 +959,16 @@ export class ModuleUpdater { for (const [moduleName, entry] of Object.entries(manifest.modules)) { advertised[moduleName] = entry.sha256; } - let linuxSecurityMinimum: LinuxModuleSecurityMinimum | undefined; - if (this.platform === 'linux') { - if (manifest.linuxSecurityMinimum != null) { - linuxSecurityMinimum = { - version: manifest.linuxSecurityMinimum.version.source, - requiredModules: manifest.linuxSecurityMinimum.requiredModules, - }; - } - } else { - for (const moduleName of blockingModuleNames(manifest)) { + if (this.startupUpdateEnforced) { + const enforced = this.store.getState().floor; + for (const moduleName of manifest.requiredModules) { if (this.planner.bundleCoversModule(moduleName, manifest)) { continue; } - floor[moduleName] = manifest.modules[moduleName].sha256; + const sha256 = this.planner.compatibleEntry(manifest, moduleName)?.sha256 ?? enforced[moduleName]; + if (sha256 != null) { + floor[moduleName] = sha256; + } } } await this.manifestRepository.persist(document); @@ -1063,7 +977,6 @@ export class ModuleUpdater { fetchedAt: new Date(this.now()).toISOString(), manifest: observation, floor, - linuxSecurityMinimum, advertised, }); } @@ -1143,7 +1056,6 @@ export class ModuleUpdater { ): void { this.lastState = { status, - requiredSecurityUpdate: this.isSecurityUpdateRequired(), current: patch.current ?? null, total: patch.total ?? null, progress: patch.progress ?? null, diff --git a/fluxer_desktop/src/main/ShellDownloadFormats.ts b/fluxer_desktop/src/main/ShellDownloadFormats.ts index ddbea4fa6..bc9e169f0 100644 --- a/fluxer_desktop/src/main/ShellDownloadFormats.ts +++ b/fluxer_desktop/src/main/ShellDownloadFormats.ts @@ -31,35 +31,3 @@ export function getUpdateBaseUrl(platform: NodeJS.Platform = process.platform): export const MANUAL_DESKTOP_FORMATS = ['setup', 'dmg', 'zip', 'appimage', 'deb', 'rpm', 'tar_gz'] as const; export type ManualDesktopFormat = (typeof MANUAL_DESKTOP_FORMATS)[number]; -export type LinuxManualDesktopFormat = Extract; - -export const LINUX_MANUAL_FORMAT_EXTENSIONS: Record = { - appimage: '.AppImage', - deb: '.deb', - rpm: '.rpm', - tar_gz: '.tar.gz', -}; - -export const LINUX_MANUAL_ARCH_TOKENS: Record> = { - appimage: {x64: 'x86_64', arm64: 'arm64'}, - deb: {x64: 'amd64', arm64: 'arm64'}, - rpm: {x64: 'x86_64', arm64: 'aarch64'}, - tar_gz: {x64: 'x64', arm64: 'arm64'}, -}; - -export const SPLASH_LINUX_FORMAT_ORDER: ReadonlyArray = ['deb', 'rpm', 'appimage', 'tar_gz']; - -export const SPLASH_LINUX_FORMAT_LABELS: Record = { - deb: 'Debian (deb)', - rpm: 'Fedora (rpm)', - appimage: 'Linux (AppImage)', - tar_gz: 'Linux (tar.gz)', -}; - -export function isLinuxManualDesktopFormat(format: ManualDesktopFormat): format is LinuxManualDesktopFormat { - return format === 'appimage' || format === 'deb' || format === 'rpm' || format === 'tar_gz'; -} - -export function buildManualLatestDownloadUrl(format: ManualDesktopFormat): string { - return `${getUpdateBaseUrl()}/latest/${format}`; -} diff --git a/fluxer_desktop/src/main/ShellUpdateCapability.test.mjs b/fluxer_desktop/src/main/ShellUpdateCapability.test.mjs index d03d5d766..9783b83f2 100644 --- a/fluxer_desktop/src/main/ShellUpdateCapability.test.mjs +++ b/fluxer_desktop/src/main/ShellUpdateCapability.test.mjs @@ -105,35 +105,17 @@ installElectronStub({ const {openExternalDeduped, shouldOpenExternalUrl} = await import('@electron/main/OpenExternal'); const {openSplashWindow} = await import('@electron/main/SplashWindow'); -const {buildManualLatestDownloadUrl, DESKTOP_DOWNLOAD_ARCH, getDesktopDownloadArch, getUpdateBaseUrl} = await import( - '@electron/main/ShellDownloadFormats' -); +const {getDesktopDownloadArch} = await import('@electron/main/ShellDownloadFormats'); const {BUILD_CHANNEL} = await import('@electron/common/BuildChannel'); -const {decideShellUpdatePlan, resolveShellUpdatePlan, ShellUpdateCapability} = await import( - '@electron/main/ShellUpdateCapability' -); +const {decideShellUpdatePlan, resolveShellUpdatePlan} = await import('@electron/main/ShellUpdateCapability'); const {DOWNLOAD_PAGE_URL} = await import('@electron/main/UpdaterDownloads'); -const {armBlockedShellUpdate, getBlockedShellUpdate, SPLASH_MANUAL_UPDATE_MESSAGE} = await import( - '@electron/main/ShellUpdateSplash' -); +const {armBlockedShellUpdate} = await import('@electron/main/ShellUpdateSplash'); -const SELF_UPDATE_PLAN = {capability: ShellUpdateCapability.SELF_UPDATE, updater: 'velopack'}; -const MANUAL_PLATFORM_PLAN = {capability: ShellUpdateCapability.MANUAL_DOWNLOAD, reason: 'platform'}; -const MANUAL_UNPACKAGED_PLAN = {capability: ShellUpdateCapability.MANUAL_DOWNLOAD, reason: 'unpackaged'}; -const MANAGED_PLAN = {capability: ShellUpdateCapability.MANAGED_PACKAGE}; const APPIMAGE_TARGET = { installedPath: '/home/user/Applications/Fluxer.AppImage', directory: '/home/user/Applications', }; -const EXPECTED_PICKER_OPTIONS = [ - {value: 'deb', label: 'Debian (deb)', buttonLabel: 'Download', kind: 'download'}, - {value: 'rpm', label: 'Fedora (rpm)', buttonLabel: 'Download', kind: 'download'}, - {value: 'appimage', label: 'Linux (AppImage)', buttonLabel: 'Download', kind: 'download'}, - {value: 'tar_gz', label: 'Linux (tar.gz)', buttonLabel: 'Download', kind: 'download'}, - {value: 'nope', label: "I'll figure it out", buttonLabel: 'Okay', kind: 'quit'}, -]; - function emitIpc(channel, event, payload) { for (const registration of ipcRegistrations) { if (registration.channel === channel) { @@ -148,24 +130,6 @@ function settle() { }); } -function withProperty(target, name, value, run) { - const original = Object.getOwnPropertyDescriptor(target, name); - Object.defineProperty(target, name, {value, configurable: true, writable: true}); - try { - return run(); - } finally { - if (original == null) { - delete target[name]; - } else { - Object.defineProperty(target, name, original); - } - } -} - -function withPlatform(platform, run) { - return withProperty(process, 'platform', platform, run); -} - function planFor(runtime) { return decideShellUpdatePlan({ channel: 'canary', @@ -178,10 +142,6 @@ function planFor(runtime) { }); } -function hasAffordance(blocked) { - return blocked.action != null || blocked.options != null; -} - describe('shell update capability', () => { test('every runtime signal picks one updater, first match wins', () => { assert.deepEqual(planFor({packaged: false, platform: 'win32'}), { @@ -197,10 +157,26 @@ describe('shell update capability', () => { updater: 'appimage', target: APPIMAGE_TARGET, }); - assert.deepEqual(planFor({platform: 'linux'}), {capability: 'manual-download', reason: 'platform'}); + assert.deepEqual(planFor({platform: 'linux'}), {capability: 'managed-package'}); assert.deepEqual(planFor({platform: 'freebsd'}), {capability: 'manual-download', reason: 'platform'}); }); + test('only a writable AppImage updates itself on Linux, every other install belongs to its package manager', () => { + assert.deepEqual(planFor({platform: 'linux', appImage: APPIMAGE_TARGET}).capability, 'self-update'); + for (const runtime of [ + {platform: 'linux'}, + {platform: 'linux', portable: true}, + {platform: 'linux', portable: true, appImage: APPIMAGE_TARGET}, + {platform: 'linux', appImage: null}, + {platform: 'linux', flatpak: true}, + ]) { + assert.deepEqual(planFor(runtime), {capability: 'managed-package'}, JSON.stringify(runtime)); + } + for (const platform of ['win32', 'darwin']) { + assert.deepEqual(planFor({platform, portable: true}), {capability: 'manual-download', reason: 'platform'}); + } + }); + test('a development shell never self-updates, because no feed publishes development builds', () => { for (const platform of ['darwin', 'win32', 'linux']) { assert.deepEqual(planFor({channel: 'development', platform, appImage: APPIMAGE_TARGET}), { @@ -217,8 +193,7 @@ describe('shell update capability', () => { test('a store runtime outranks a portable marker, so a sandboxed install is never offered a deb or rpm it cannot apply', () => { assert.deepEqual(planFor({portable: true, flatpak: true, platform: 'linux'}), {capability: 'managed-package'}); assert.deepEqual(planFor({portable: true, appImage: APPIMAGE_TARGET, platform: 'linux'}), { - capability: 'manual-download', - reason: 'platform', + capability: 'managed-package', }); assert.deepEqual(planFor({packaged: false, flatpak: true, platform: 'linux'}), { capability: 'manual-download', @@ -242,114 +217,11 @@ describe('shell update capability', () => { }); describe('blocked shell update states', () => { - test('every plan lands on an affordance that can resolve the block', () => { - for (const plan of [SELF_UPDATE_PLAN, MANUAL_PLATFORM_PLAN, MANUAL_UNPACKAGED_PLAN, MANAGED_PLAN]) { - for (const platform of ['linux', 'darwin', 'win32']) { - const blocked = withPlatform(platform, () => getBlockedShellUpdate(plan, '2026.823.1')); - assert.ok(hasAffordance(blocked), `${plan.capability} on ${platform} has no way out`); - assert.notEqual( - blocked.action?.kind, - 'retry', - `${plan.capability} cannot escape a relaunch: the same binary refetches the same manifest and blocks again`, - ); - for (const label of [blocked.action?.label, ...(blocked.options ?? []).map((option) => option.label)]) { - assert.ok(label == null || label.length > 0); - } - } - } - }); - - test('a shell that could have updated itself and did not falls back to the download page', () => { - const blocked = withPlatform('win32', () => getBlockedShellUpdate(SELF_UPDATE_PLAN, '2026.823.1')); - - assert.deepEqual(blocked, { - layout: 'splash', - status: 'blocked-shell-update', - action: {kind: 'download', label: 'Download update'}, - message: null, - versionLabel: null, - options: null, - downloadUrl: DOWNLOAD_PAGE_URL, - downloadUrls: new Map(), - }); - }); - - test('a packaged linux install gets the picker, with the version main already knows', () => { - const blocked = withPlatform('linux', () => getBlockedShellUpdate(MANUAL_PLATFORM_PLAN, '2026.823.1')); - - assert.equal(blocked.layout, 'manual-update'); - assert.equal(blocked.status, 'blocked-shell-update'); - assert.equal(blocked.action, null); - assert.equal(blocked.message, SPLASH_MANUAL_UPDATE_MESSAGE); - assert.equal(blocked.versionLabel, 'Version 2026.823.1 available'); - assert.deepEqual(blocked.options, EXPECTED_PICKER_OPTIONS); - assert.equal(blocked.downloadUrl, null); - }); - - test('an unknown latest version leaves the bottom line off rather than inventing one', () => { - const blocked = withPlatform('linux', () => getBlockedShellUpdate(MANUAL_PLATFORM_PLAN, null)); - - assert.equal(blocked.versionLabel, null); - assert.deepEqual(blocked.options, EXPECTED_PICKER_OPTIONS); - }); - - test('a dev run from source is never handed a distro artifact', () => { - const blocked = withPlatform('linux', () => getBlockedShellUpdate(MANUAL_UNPACKAGED_PLAN, '2026.823.1')); - - assert.equal(blocked.layout, 'splash'); - assert.equal(blocked.options, null); - assert.deepEqual(blocked.action, {kind: 'download', label: 'Download update'}); - assert.equal(blocked.downloadUrl, DOWNLOAD_PAGE_URL); - }); - - test('a portable win32 or darwin build gets the page, never the linux picker', () => { - for (const platform of ['win32', 'darwin']) { - const blocked = withPlatform(platform, () => getBlockedShellUpdate(MANUAL_PLATFORM_PLAN, '2026.823.1')); - assert.equal(blocked.layout, 'splash', platform); - assert.equal(blocked.options, null, platform); - assert.equal(blocked.downloadUrl, DOWNLOAD_PAGE_URL, platform); - } - }); - - test('a package manager owned install is offered no download url at all', () => { - const blocked = withPlatform('linux', () => getBlockedShellUpdate(MANAGED_PLAN, '2026.823.1')); - - assert.equal(blocked.downloadUrl, null); - assert.equal(blocked.downloadUrls.size, 0); - assert.equal(blocked.options, null); - assert.equal(blocked.status, 'blocked-shell-update-managed'); - assert.deepEqual(blocked.action, {kind: 'quit', label: 'Quit'}); - }); - test('the download page is a channel aware page, never an artifact', () => { assert.equal(shouldOpenExternalUrl(DOWNLOAD_PAGE_URL), true); assert.equal(new URL(DOWNLOAD_PAGE_URL).pathname, '/download'); }); - test('every picker entry resolves to the latest linux artifact route for the running arch', () => { - const blocked = withPlatform('linux', () => getBlockedShellUpdate(MANUAL_PLATFORM_PLAN, '2026.823.1')); - - assert.deepEqual( - [...blocked.downloadUrls.keys()], - EXPECTED_PICKER_OPTIONS.filter((option) => option.kind === 'download').map((option) => option.value), - ); - for (const [format, url] of blocked.downloadUrls) { - assert.equal(url, `${getUpdateBaseUrl('linux')}/latest/${format}`); - assert.equal(new URL(url).pathname.endsWith(`/linux/${DESKTOP_DOWNLOAD_ARCH}/latest/${format}`), true, url); - assert.equal(shouldOpenExternalUrl(url), true); - } - assert.equal(blocked.downloadUrls.has('nope'), false); - }); - - test('the artifact route reads the platform at call time, so a darwin runner still proves the linux shape', () => { - const linux = withPlatform('linux', () => buildManualLatestDownloadUrl('deb')); - const win32 = withPlatform('win32', () => buildManualLatestDownloadUrl('setup')); - - assert.equal(linux, `${getUpdateBaseUrl('linux')}/latest/deb`); - assert.ok(linux.includes('/linux/'), linux); - assert.ok(win32.includes('/win32/'), win32); - }); - test('the download arch collapses to the two we publish', () => { assert.equal(getDesktopDownloadArch('arm64'), 'arm64'); assert.equal(getDesktopDownloadArch('x64'), 'x64'); @@ -359,50 +231,16 @@ describe('blocked shell update states', () => { }); describe('arming the blocked shell update', () => { - test('a package manager owned install arms nothing that could open a url', async () => { - const window = openSplashWindow(); - events.length = 0; - - armBlockedShellUpdate(MANAGED_PLAN, '2026.823.1'); - - assert.deepEqual(window.webContents.sent.at(-1), { - channel: 'desktop-splash:state', - payload: { - layout: 'splash', - status: 'blocked-shell-update-managed', - requiredSecurityUpdate: false, - current: null, - total: null, - progress: null, - seconds: null, - receivedBytes: null, - totalBytes: null, - bytesPerSecond: null, - action: {kind: 'quit', label: 'Quit'}, - message: null, - versionLabel: null, - options: null, - }, - }); - - emitIpc('desktop-splash:open-download', {sender: window.webContents}); - await settle(); - - assert.deepEqual(events, []); - }); - test('the download action opens the allowlisted page and then quits', async () => { const window = openSplashWindow(); events.length = 0; - withPlatform('win32', () => armBlockedShellUpdate(SELF_UPDATE_PLAN, '2026.823.1')); + armBlockedShellUpdate(); assert.deepEqual(window.webContents.sent.at(-1), { channel: 'desktop-splash:state', payload: { - layout: 'splash', status: 'blocked-shell-update', - requiredSecurityUpdate: false, current: null, total: null, progress: null, @@ -412,8 +250,6 @@ describe('arming the blocked shell update', () => { bytesPerSecond: null, action: {kind: 'download', label: 'Download update'}, message: null, - versionLabel: null, - options: null, }, }); @@ -423,58 +259,13 @@ describe('arming the blocked shell update', () => { assert.deepEqual(events, [`open:${DOWNLOAD_PAGE_URL}`, 'quit']); }); - test('the picker sends the whole manual layout and opens the artifact the user chose', async () => { - const window = openSplashWindow(); - events.length = 0; - - const blocked = withPlatform('linux', () => armBlockedShellUpdate(MANUAL_PLATFORM_PLAN, '2026.823.1')); - - assert.deepEqual(window.webContents.sent.at(-1), { - channel: 'desktop-splash:state', - payload: { - layout: 'manual-update', - status: 'blocked-shell-update', - requiredSecurityUpdate: false, - current: null, - total: null, - progress: null, - seconds: null, - receivedBytes: null, - totalBytes: null, - bytesPerSecond: null, - action: null, - message: SPLASH_MANUAL_UPDATE_MESSAGE, - versionLabel: 'Version 2026.823.1 available', - options: EXPECTED_PICKER_OPTIONS, - }, - }); - - emitIpc('desktop-splash:open-download', {sender: window.webContents}, 'rpm'); - await settle(); - - assert.deepEqual(events, [`open:${blocked.downloadUrls.get('rpm')}`, 'quit']); - }); - - test('a token that was never armed is dropped instead of being turned into a url', async () => { - const window = openSplashWindow(); - withPlatform('linux', () => armBlockedShellUpdate(MANUAL_PLATFORM_PLAN, '2026.823.1')); - events.length = 0; - - for (const value of ['setup', 'dmg', 'nope', 'unknown_format']) { - emitIpc('desktop-splash:open-download', {sender: window.webContents}, value); - } - await settle(); - - assert.deepEqual(events, []); - }); - test('a download that cannot be opened still resolves the screen instead of stranding the user', async () => { const window = openSplashWindow(); - withPlatform('linux', () => armBlockedShellUpdate(MANUAL_PLATFORM_PLAN, '2026.823.1')); + armBlockedShellUpdate(); events.length = 0; openExternalFailure = new Error('no handler for https'); - emitIpc('desktop-splash:open-download', {sender: window.webContents}, 'appimage'); + emitIpc('desktop-splash:open-download', {sender: window.webContents}); await settle(); assert.deepEqual(events, ['quit']); diff --git a/fluxer_desktop/src/main/ShellUpdateCapability.ts b/fluxer_desktop/src/main/ShellUpdateCapability.ts index ce3165975..6d3e8a526 100644 --- a/fluxer_desktop/src/main/ShellUpdateCapability.ts +++ b/fluxer_desktop/src/main/ShellUpdateCapability.ts @@ -40,6 +40,12 @@ export function decideShellUpdatePlan(runtime: ShellUpdateRuntime): ShellUpdateP if (runtime.flatpak) { return {capability: ShellUpdateCapability.MANAGED_PACKAGE}; } + if (runtime.platform === 'linux') { + if (!runtime.portable && runtime.appImage != null) { + return {capability: ShellUpdateCapability.SELF_UPDATE, updater: 'appimage', target: runtime.appImage}; + } + return {capability: ShellUpdateCapability.MANAGED_PACKAGE}; + } if (runtime.portable) { return {capability: ShellUpdateCapability.MANUAL_DOWNLOAD, reason: 'platform'}; } @@ -49,9 +55,6 @@ export function decideShellUpdatePlan(runtime: ShellUpdateRuntime): ShellUpdateP if (runtime.platform === 'darwin') { return {capability: ShellUpdateCapability.SELF_UPDATE, updater: 'electron'}; } - if (runtime.platform === 'linux' && runtime.appImage != null) { - return {capability: ShellUpdateCapability.SELF_UPDATE, updater: 'appimage', target: runtime.appImage}; - } return {capability: ShellUpdateCapability.MANUAL_DOWNLOAD, reason: 'platform'}; } diff --git a/fluxer_desktop/src/main/ShellUpdateSplash.ts b/fluxer_desktop/src/main/ShellUpdateSplash.ts index d39c35a3f..6e49a413a 100644 --- a/fluxer_desktop/src/main/ShellUpdateSplash.ts +++ b/fluxer_desktop/src/main/ShellUpdateSplash.ts @@ -2,161 +2,32 @@ import {createChildLogger} from '@electron/common/Logger'; import {openExternalDeduped} from '@electron/main/OpenExternal'; -import { - buildManualLatestDownloadUrl, - SPLASH_LINUX_FORMAT_LABELS, - SPLASH_LINUX_FORMAT_ORDER, -} from '@electron/main/ShellDownloadFormats'; -import {ShellUpdateCapability, type ShellUpdatePlan} from '@electron/main/ShellUpdateCapability'; -import { - onSplashOpenDownload, - SplashAction, - type SplashActionDescriptor, - SplashActionKind, - type SplashDownloadOption, - SplashLayout, - SplashStatus, - setSplashState, -} from '@electron/main/SplashWindow'; +import {onSplashOpenDownload, SplashAction, SplashStatus, setSplashState} from '@electron/main/SplashWindow'; import {DOWNLOAD_PAGE_URL} from '@electron/main/UpdaterDownloads'; import {app} from 'electron'; -export const SPLASH_MANUAL_UPDATE_MESSAGE = 'A new version of Fluxer is ready to install.'; -const SPLASH_REQUIRED_SECURITY_UPDATE_MESSAGE = 'A required security update must be installed before Fluxer can start.'; -const SPLASH_DECLINE_OPTION_VALUE = 'nope'; -const SPLASH_DECLINE_OPTION_LABEL = "I'll figure it out"; -const SPLASH_DOWNLOAD_BUTTON_LABEL = 'Download'; -const SPLASH_DECLINE_BUTTON_LABEL = 'Okay'; - -interface BlockedShellUpdate { - readonly layout: SplashLayout; - readonly status: SplashStatus; - readonly action: SplashActionDescriptor | null; - readonly message: string | null; - readonly versionLabel: string | null; - readonly options: ReadonlyArray | null; - readonly downloadUrl: string | null; - readonly downloadUrls: ReadonlyMap; -} - const logger = createChildLogger('ShellUpdateSplash'); -let armedDownloadUrl: string | null = null; -const armedDownloadUrls = new Map(); let disarmDownloadListener: (() => void) | null = null; -function buildLinuxDownloadOptions(): Array { - const options: Array = SPLASH_LINUX_FORMAT_ORDER.map((format) => ({ - value: format, - label: SPLASH_LINUX_FORMAT_LABELS[format], - buttonLabel: SPLASH_DOWNLOAD_BUTTON_LABEL, - kind: SplashActionKind.DOWNLOAD, - })); - options.push({ - value: SPLASH_DECLINE_OPTION_VALUE, - label: SPLASH_DECLINE_OPTION_LABEL, - buttonLabel: SPLASH_DECLINE_BUTTON_LABEL, - kind: SplashActionKind.QUIT, - }); - return options; -} - -function buildLinuxDownloadUrls(): Map { - return new Map(SPLASH_LINUX_FORMAT_ORDER.map((format) => [format, buildManualLatestDownloadUrl(format)])); -} - -function usesManualUpdatePicker(plan: ShellUpdatePlan, platform: NodeJS.Platform): boolean { - return ( - plan.capability === ShellUpdateCapability.MANUAL_DOWNLOAD && plan.reason === 'platform' && platform === 'linux' - ); -} - -export function getBlockedShellUpdate( - plan: ShellUpdatePlan, - latestVersion: string | null, - requiredSecurityUpdate = false, -): BlockedShellUpdate { - if (plan.capability === ShellUpdateCapability.MANAGED_PACKAGE) { - return { - layout: SplashLayout.SPLASH, - status: requiredSecurityUpdate - ? SplashStatus.BLOCKED_SECURITY_UPDATE_MANAGED - : SplashStatus.BLOCKED_SHELL_UPDATE_MANAGED, - action: SplashAction.QUIT, - message: null, - versionLabel: null, - options: null, - downloadUrl: null, - downloadUrls: new Map(), - }; - } - if (usesManualUpdatePicker(plan, process.platform)) { - return { - layout: SplashLayout.MANUAL_UPDATE, - status: requiredSecurityUpdate - ? SplashStatus.BLOCKED_SECURITY_UPDATE_REQUIRED - : SplashStatus.BLOCKED_SHELL_UPDATE, - action: null, - message: requiredSecurityUpdate ? SPLASH_REQUIRED_SECURITY_UPDATE_MESSAGE : SPLASH_MANUAL_UPDATE_MESSAGE, - versionLabel: latestVersion == null ? null : `Version ${latestVersion} available`, - options: buildLinuxDownloadOptions(), - downloadUrl: null, - downloadUrls: buildLinuxDownloadUrls(), - }; - } - return { - layout: SplashLayout.SPLASH, - status: requiredSecurityUpdate ? SplashStatus.BLOCKED_SECURITY_UPDATE_REQUIRED : SplashStatus.BLOCKED_SHELL_UPDATE, - action: SplashAction.DOWNLOAD, - message: null, - versionLabel: null, - options: null, - downloadUrl: DOWNLOAD_PAGE_URL, - downloadUrls: new Map(), - }; -} - -function openArmedDownload(value: string | null): void { - const url = value == null ? armedDownloadUrl : (armedDownloadUrls.get(value) ?? null); - if (url == null) { - logger.warn('Ignoring a splash download request that matches no armed download', {value}); - return; - } - void openExternalDeduped(url).then( +function openDownloadPage(): void { + void openExternalDeduped(DOWNLOAD_PAGE_URL).then( () => { - logger.info('Opened the download for a required shell update, quitting', {url}); + logger.info('Opened the download for a required shell update, quitting', {url: DOWNLOAD_PAGE_URL}); app.quit(); }, (error: unknown) => { - logger.error('Failed to open the download for a required shell update, quitting anyway', {url, error}); + logger.error('Failed to open the download for a required shell update, quitting anyway', { + url: DOWNLOAD_PAGE_URL, + error, + }); app.quit(); }, ); } -export function armBlockedShellUpdate( - plan: ShellUpdatePlan, - latestVersion: string | null, - requiredSecurityUpdate = false, -): BlockedShellUpdate { - const blocked = getBlockedShellUpdate(plan, latestVersion, requiredSecurityUpdate); - armedDownloadUrl = blocked.downloadUrl; - armedDownloadUrls.clear(); - for (const [value, url] of blocked.downloadUrls) { - armedDownloadUrls.set(value, url); - } +export function armBlockedShellUpdate(): void { disarmDownloadListener?.(); - disarmDownloadListener = null; - if (armedDownloadUrl != null || armedDownloadUrls.size > 0) { - disarmDownloadListener = onSplashOpenDownload(openArmedDownload); - } - setSplashState({ - layout: blocked.layout, - status: blocked.status, - action: blocked.action, - message: blocked.message, - versionLabel: blocked.versionLabel, - options: blocked.options, - }); - return blocked; + disarmDownloadListener = onSplashOpenDownload(openDownloadPage); + setSplashState({status: SplashStatus.BLOCKED_SHELL_UPDATE, action: SplashAction.DOWNLOAD}); } diff --git a/fluxer_desktop/src/main/SplashWindow.test.mjs b/fluxer_desktop/src/main/SplashWindow.test.mjs index 889ad2ad7..385d16f92 100644 --- a/fluxer_desktop/src/main/SplashWindow.test.mjs +++ b/fluxer_desktop/src/main/SplashWindow.test.mjs @@ -10,16 +10,6 @@ import {installElectronStub} from './LocalAppTestSupport.test.mjs'; const MODULE_URL = new URL('./SplashWindow.ts', import.meta.url).href; const PRELOAD_PATH = fileURLToPath(new URL('../preload/splash.cjs', import.meta.url)); const PRELOAD_SOURCE = readFileSync(PRELOAD_PATH, 'utf8'); -const STYLESHEET_PATH = fileURLToPath(new URL('../splash/index.css', import.meta.url)); -const STYLESHEET_SOURCE = readFileSync(STYLESHEET_PATH, 'utf8'); - -const MANUAL_UPDATE_OPTIONS = Object.freeze([ - {value: 'deb', label: 'Debian (deb)', buttonLabel: 'Download', kind: 'download'}, - {value: 'rpm', label: 'Fedora (rpm)', buttonLabel: 'Download', kind: 'download'}, - {value: 'appimage', label: 'Linux (AppImage)', buttonLabel: 'Download', kind: 'download'}, - {value: 'tar_gz', label: 'Linux (tar.gz)', buttonLabel: 'Download', kind: 'download'}, - {value: 'nope', label: "I'll figure it out", buttonLabel: 'Okay', kind: 'quit'}, -]); const quitCalls = []; const ipcRegistrations = []; @@ -512,9 +502,7 @@ describe('splash state serialisation', () => { progress: 42.5, }), { - layout: 'splash', status: 'downloading-updates', - requiredSecurityUpdate: false, current: 3, total: 7, progress: 42.5, @@ -524,8 +512,6 @@ describe('splash state serialisation', () => { bytesPerSecond: null, action: null, message: null, - versionLabel: null, - options: null, }, ); }); @@ -542,9 +528,7 @@ describe('splash state serialisation', () => { seconds: -4, }), { - layout: 'splash', status: 'update-failure', - requiredSecurityUpdate: false, current: null, total: null, progress: 0, @@ -554,8 +538,6 @@ describe('splash state serialisation', () => { bytesPerSecond: null, action: null, message: null, - versionLabel: null, - options: null, }, ); }); @@ -572,17 +554,6 @@ describe('splash state serialisation', () => { assert.equal(serializeSplashState({status: 'not-a-splash-status'}).status, 'checking-for-updates'); }); - test('falls back to the splash layout on anything the renderer could not lay out', async () => { - const {serializeSplashState, SplashLayout, SplashStatus} = await loadSplashWindow('serialise-layout'); - const layoutFor = (layout) => serializeSplashState({status: SplashStatus.BLOCKED_SHELL_UPDATE, layout}).layout; - - assert.equal(layoutFor(SplashLayout.MANUAL_UPDATE), 'manual-update'); - assert.equal(layoutFor(SplashLayout.SPLASH), 'splash'); - assert.equal(layoutFor('dice-roll'), 'splash'); - assert.equal(layoutFor(undefined), 'splash'); - assert.equal(layoutFor(null), 'splash'); - }); - test('carries every shipped action through and drops one the renderer could not route', async () => { const {serializeSplashState, SplashAction, SplashStatus} = await loadSplashWindow('serialise-action'); const actionFor = (action) => serializeSplashState({status: SplashStatus.BLOCKED_SHELL_UPDATE, action}).action; @@ -597,49 +568,6 @@ describe('splash state serialisation', () => { assert.equal(actionFor({kind: 'retry', label: 'l'.repeat(200)}).label.length, 48); }); - test('carries the whole picker through and drops entries the renderer could not route', async () => { - const {serializeSplashState, SplashStatus} = await loadSplashWindow('serialise-options'); - const optionsFor = (options) => serializeSplashState({status: SplashStatus.BLOCKED_SHELL_UPDATE, options}).options; - - assert.deepEqual(optionsFor(MANUAL_UPDATE_OPTIONS), MANUAL_UPDATE_OPTIONS); - assert.equal(optionsFor(undefined), null); - assert.equal(optionsFor('deb'), null); - assert.equal(optionsFor([]), null); - assert.equal(optionsFor([{value: 'deb', label: 'Debian', buttonLabel: 'Download', kind: 'launch-anyway'}]), null); - assert.equal(optionsFor([{value: 'DEB', label: 'Debian', buttonLabel: 'Download', kind: 'download'}]), null); - assert.equal(optionsFor([{value: '../etc', label: 'Debian', buttonLabel: 'Download', kind: 'download'}]), null); - assert.equal(optionsFor([{value: 'deb', label: ' ', buttonLabel: 'Download', kind: 'download'}]), null); - assert.equal(optionsFor([{value: 'deb', label: 'Debian', buttonLabel: '', kind: 'download'}]), null); - assert.deepEqual( - optionsFor([ - {value: 'deb', label: 'Debian', buttonLabel: 'Download', kind: 'download'}, - {value: 'not valid', label: 'Nope', buttonLabel: 'Nope', kind: 'download'}, - ]), - [{value: 'deb', label: 'Debian', buttonLabel: 'Download', kind: 'download'}], - ); - assert.equal( - optionsFor( - Array.from({length: 12}, (_entry, index) => ({ - value: `format_${index}`, - label: `Format ${index}`, - buttonLabel: 'Download', - kind: 'download', - })), - ).length, - 8, - ); - assert.equal( - optionsFor([{value: 'deb', label: 'l'.repeat(200), buttonLabel: 'b'.repeat(200), kind: 'download'}])[0].label - .length, - 48, - ); - assert.equal( - optionsFor([{value: 'deb', label: 'l'.repeat(200), buttonLabel: 'b'.repeat(200), kind: 'download'}])[0] - .buttonLabel.length, - 24, - ); - }); - test('only a state that carries a way out counts as an affordance', async () => { const {hasSplashAffordance, serializeSplashState, SplashAction, SplashStatus} = await loadSplashWindow('affordance-predicate'); @@ -650,43 +578,6 @@ describe('splash state serialisation', () => { hasSplashAffordance(serializeSplashState({status: SplashStatus.BLOCKED_SHELL_UPDATE, action: SplashAction.QUIT})), true, ); - assert.equal( - hasSplashAffordance( - serializeSplashState({status: SplashStatus.BLOCKED_SHELL_UPDATE, options: MANUAL_UPDATE_OPTIONS}), - ), - true, - ); - }); - - test('the manual layout can never be serialised without a way out of it', async () => { - const {hasSplashAffordance, serializeSplashState, SplashLayout, SplashStatus} = - await loadSplashWindow('affordance-manual'); - const manual = (patch) => - serializeSplashState({layout: SplashLayout.MANUAL_UPDATE, status: SplashStatus.BLOCKED_SHELL_UPDATE, ...patch}); - - assert.deepEqual(manual({}).action, {kind: 'quit', label: 'Quit'}); - assert.deepEqual(manual({action: {kind: 'launch-anyway', label: 'Launch anyway'}, options: []}).action, { - kind: 'quit', - label: 'Quit', - }); - assert.equal(hasSplashAffordance(manual({})), true); - assert.equal(manual({options: MANUAL_UPDATE_OPTIONS}).action, null); - assert.deepEqual(manual({options: MANUAL_UPDATE_OPTIONS}).options, MANUAL_UPDATE_OPTIONS); - assert.equal(serializeSplashState({status: SplashStatus.BLOCKED_SHELL_UPDATE}).action, null); - }); - - test('only a token the main process could have minted survives the download payload check', async () => { - const {toSplashDownloadValue} = await loadSplashWindow('download-payload'); - - assert.equal(toSplashDownloadValue('deb'), 'deb'); - assert.equal(toSplashDownloadValue('tar_gz'), 'tar_gz'); - assert.equal(toSplashDownloadValue(undefined), null); - assert.equal(toSplashDownloadValue(null), null); - assert.equal(toSplashDownloadValue(42), null); - assert.equal(toSplashDownloadValue('../../etc/passwd'), null); - assert.equal(toSplashDownloadValue('https://evil.example/x'), null); - assert.equal(toSplashDownloadValue('a'.repeat(33)), null); - assert.equal(toSplashDownloadValue(''), null); }); }); @@ -791,9 +682,7 @@ describe('splash IPC', () => { { channel: 'desktop-splash:state', payload: { - layout: 'splash', status: 'downloading-updates', - requiredSecurityUpdate: false, current: 2, total: 5, progress: 40, @@ -803,8 +692,6 @@ describe('splash IPC', () => { bytesPerSecond: null, action: null, message: null, - versionLabel: null, - options: null, }, }, ]); @@ -827,9 +714,7 @@ describe('splash IPC', () => { { channel: 'desktop-splash:state', payload: { - layout: 'splash', status: 'verifying', - requiredSecurityUpdate: false, current: null, total: null, progress: null, @@ -839,8 +724,6 @@ describe('splash IPC', () => { bytesPerSecond: null, action: null, message: null, - versionLabel: null, - options: null, }, }, ]); @@ -888,23 +771,22 @@ describe('splash IPC', () => { assert.equal(onlineCount, 1); }); - test('hands the download listener the chosen option, and nothing at all for a bare click', async () => { + test('notifies download listeners only for the splash sender and hands them nothing from the payload', async () => { const {onSplashOpenDownload, openSplashWindow} = await loadSplashWindow('ipc-open-download'); const window = withPlatformResult('darwin', () => openSplashWindow()); - const values = []; - onSplashOpenDownload((value) => { - values.push(value); + const calls = []; + onSplashOpenDownload((...args) => { + calls.push(args); }); - emitIpc('desktop-splash:open-download', {sender: new FakeWebContents()}, 'deb'); + emitIpc('desktop-splash:open-download', {sender: new FakeWebContents()}); - assert.deepEqual(values, []); + assert.deepEqual(calls, []); emitIpc('desktop-splash:open-download', {sender: window.webContents}); - emitIpc('desktop-splash:open-download', {sender: window.webContents}, 'tar_gz'); emitIpc('desktop-splash:open-download', {sender: window.webContents}, 'https://evil.example'); - assert.deepEqual(values, [null, 'tar_gz', null]); + assert.deepEqual(calls, [[], []]); }); test('stops sending state once the splash is closed', async () => { @@ -1060,10 +942,6 @@ class StubNode { querySelector(selector) { return this.descendants().find((node) => node.matches(selector)) ?? null; } - - querySelectorAll(selector) { - return this.descendants().filter((node) => node.matches(selector)); - } } const {serializeSplashState: serializePreloadState} = await loadSplashWindow('preload-state-producer'); @@ -1143,13 +1021,7 @@ function createPreloadHarness(options = {}) { liveIntervals: () => intervals.filter((timer) => !timer.cleared), mount, query, - queryAll: (selector) => mount.querySelectorAll(selector), click: (selector) => query(selector).dispatch('click'), - changeSelect: (value) => { - const select = query('#dl-select-input'); - select.value = value; - select.dispatch('change'); - }, sendState: (payload) => ipcListeners.get('desktop-splash:state')({}, serializePreloadState(payload)), reveal: () => ipcListeners.get('desktop-splash:revealed')({}), sent, @@ -1174,14 +1046,6 @@ function createPreloadHarness(options = {}) { }; } -function styleRule(selector) { - const marker = `\n${selector} {`; - const start = STYLESHEET_SOURCE.indexOf(marker); - assert.notEqual(start, -1, `${selector} is missing from the splash stylesheet`); - const end = STYLESHEET_SOURCE.indexOf('\n}', start); - return STYLESHEET_SOURCE.slice(start + marker.length, end); -} - describe('splash preload rendering', () => { test('a held splash waits for its reveal before timing the diagnostics links', async () => { const diagnosticsTimers = (harness) => @@ -1418,21 +1282,6 @@ describe('splash preload rendering', () => { assert.deepEqual(harness.sent.at(-1), {channel: 'desktop-splash:open-download', payload: undefined}); }); - test('offers only a quit action on a package manager owned install', async () => { - const {serializeSplashState, SplashAction} = await loadSplashWindow('preload-managed-action'); - const harness = createPreloadHarness(); - await harness.start(); - - harness.sendState(serializeSplashState({status: 'blocked-shell-update-managed', action: SplashAction.QUIT})); - - assert.equal(harness.statusText(), 'Update Fluxer through your package manager'); - assert.equal(harness.query('.splash-action').textContent, 'Quit'); - - harness.click('.splash-action'); - - assert.deepEqual(harness.channels(), ['desktop-splash:ready', 'desktop-splash:quit']); - }); - test('routes a click to the current action kind without rebuilding the button', async () => { const {serializeSplashState, SplashAction} = await loadSplashWindow('preload-action-swap'); const harness = createPreloadHarness(); @@ -1471,112 +1320,12 @@ describe('splash preload rendering', () => { }); }); -describe('splash preload manual update layout', () => { - function manualState(patch = {}) { - return { - layout: 'manual-update', - status: 'blocked-shell-update', - current: null, - total: null, - progress: null, - seconds: null, - action: null, - message: 'A new version of Fluxer is ready to install.', - versionLabel: 'Version 2026.823.1 available', - options: MANUAL_UPDATE_OPTIONS, - ...patch, - }; - } - - test('renders the validated markup, every format and the opt-out', () => { - const harness = createPreloadHarness(); - harness.start(); - - harness.sendState(manualState()); - - assert.equal(harness.mount.replaceChildrenCalls, 2); - assert.equal(harness.query('.splash-inner-dl').children.length, 4); - assert.equal(harness.query('.splash-mark-dl').querySelector('img').getAttribute('src'), './symbol.svg'); - assert.equal(harness.query('.dl-update-message').textContent, 'A new version of Fluxer is ready to install.'); - assert.equal(harness.query('.dl-version-message').textContent, 'Version 2026.823.1 available'); - assert.deepEqual( - harness.queryAll('option').map((node) => [node.value, node.textContent]), - [ - ['deb', 'Debian (deb)'], - ['rpm', 'Fedora (rpm)'], - ['appimage', 'Linux (AppImage)'], - ['tar_gz', 'Linux (tar.gz)'], - ['nope', "I'll figure it out"], - ], - ); - assert.equal(harness.query('#dl-select-input').value, 'deb'); - assert.equal(harness.query('#dl-button').textContent, 'Download'); - assert.equal(harness.query('#dl-button').tagName, 'button'); - }); - - test('clicking with a real format sends that format on the download channel', async () => { - const harness = createPreloadHarness(); - await harness.start(); - harness.sendState(manualState()); - - harness.click('#dl-button'); - harness.changeSelect('tar_gz'); - harness.click('#dl-button'); - - assert.deepEqual(harness.sent.slice(1), [ - {channel: 'desktop-splash:open-download', payload: 'deb'}, - {channel: 'desktop-splash:open-download', payload: 'tar_gz'}, - ]); - }); - - test('clicking the opt-out quits and opens nothing', async () => { - const harness = createPreloadHarness(); - await harness.start(); - harness.sendState(manualState()); - - harness.changeSelect('nope'); - harness.click('#dl-button'); - - assert.deepEqual(harness.sent.slice(1), [{channel: 'desktop-splash:quit', payload: 'nope'}]); - }); - - test('a picker with nothing routable in it falls back to a quit control instead of a dead window', async () => { - const harness = createPreloadHarness(); - await harness.start(); - - harness.sendState(manualState({options: null})); - - assert.equal(harness.query('.dl-select-frame'), null); - assert.equal(harness.query('#dl-button').textContent, 'Quit'); - assert.equal(harness.query('.dl-update-message').textContent, 'A new version of Fluxer is ready to install.'); - - harness.click('#dl-button'); - - assert.deepEqual(harness.channels(), ['desktop-splash:ready', 'desktop-splash:quit']); - }); - - test('a manual state that carries an action instead of a picker renders that action', () => { - const harness = createPreloadHarness(); - harness.start(); - - harness.sendState(manualState({options: null, action: {kind: 'download', label: 'Download update'}})); - - assert.equal(harness.query('.dl-select-frame'), null); - assert.equal(harness.query('#dl-button').textContent, 'Download update'); - - harness.click('#dl-button'); - - assert.deepEqual(harness.sent.at(-1), {channel: 'desktop-splash:open-download', payload: undefined}); - }); -}); - describe('splash preload contract', () => { - test('knows every status and layout the main process can send', async () => { - const {SplashLayout, SplashStatus} = await loadSplashWindow('preload-status-parity'); + test('knows every status the main process can send', async () => { + const {SplashStatus} = await loadSplashWindow('preload-status-parity'); const {context} = createPreloadHarness(); assert.deepEqual(JSON.parse(JSON.stringify(vm.runInContext('SplashStatus', context))), SplashStatus); - assert.deepEqual(JSON.parse(JSON.stringify(vm.runInContext('SplashLayout', context))), SplashLayout); }); test('shows the bar only for a numeric progress on a status that carries one', () => { @@ -1594,23 +1343,6 @@ describe('splash preload contract', () => { assert.equal(shows({status: 'launching', progress: 50}), false); }); - test('clamps the manual update message so a long one can never push the controls out of the window', () => { - const message = styleRule('.dl-update-message'); - - assert.match(message, /-webkit-line-clamp: \d+;/); - assert.match(message, /overflow: hidden;/); - assert.match(message, /min-height: 0;/); - assert.match(styleRule('.splash-inner-dl'), /overflow: hidden;/); - }); - - test('keeps every manual control out of the shrink path and out of the drag region', () => { - for (const selector of ['.dl-select-frame', '.dl-action-frame']) { - assert.match(styleRule(selector), /flex: none;/, `${selector} must never shrink`); - assert.match(styleRule(selector), /-webkit-app-region: no-drag;/, `${selector} must stay clickable`); - } - assert.match(styleRule('.dl-version-message'), /flex: none;/); - }); - test('uses the same channel names as the main process', async () => { const module = await loadSplashWindow('preload-channels'); diff --git a/fluxer_desktop/src/main/SplashWindow.ts b/fluxer_desktop/src/main/SplashWindow.ts index 931590694..d37648e37 100644 --- a/fluxer_desktop/src/main/SplashWindow.ts +++ b/fluxer_desktop/src/main/SplashWindow.ts @@ -11,11 +11,6 @@ const SPLASH_WINDOW_HEIGHT_DEFAULT = 350; const SPLASH_BACKGROUND_COLOR = '#1a1a1a'; const SPLASH_ACTION_LABEL_MAX_LENGTH = 48; const SPLASH_MESSAGE_MAX_LENGTH = 160; -const SPLASH_VERSION_LABEL_MAX_LENGTH = 64; -const SPLASH_OPTION_LABEL_MAX_LENGTH = 48; -const SPLASH_OPTION_BUTTON_LABEL_MAX_LENGTH = 24; -const SPLASH_OPTION_MAX_COUNT = 8; -const SPLASH_OPTION_VALUE_PATTERN = /^[a-z0-9_]{1,32}$/; const SPLASH_PROGRESS_MAX = 100; export const SPLASH_READY_WATCHDOG_MS = 3000; @@ -31,13 +26,6 @@ export const DESKTOP_SPLASH_NETWORK_ONLINE_CHANNEL = 'desktop-splash:network-onl export const DESKTOP_SPLASH_OPEN_LOGS_CHANNEL = 'desktop-splash:open-logs'; export const DESKTOP_SPLASH_COPY_DIAGNOSTICS_CHANNEL = 'desktop-splash:copy-diagnostics'; -export const SplashLayout = Object.freeze({ - SPLASH: 'splash', - MANUAL_UPDATE: 'manual-update', -} as const); - -export type SplashLayout = (typeof SplashLayout)[keyof typeof SplashLayout]; - export const SplashStatus = Object.freeze({ CHECKING_FOR_UPDATES: 'checking-for-updates', DOWNLOADING_UPDATES: 'downloading-updates', @@ -49,10 +37,7 @@ export const SplashStatus = Object.freeze({ SHELL_UPDATE_RESTARTING: 'shell-update-restarting', BLOCKED_UPDATE_REQUIRED: 'blocked-update-required', BLOCKED_UPDATE_UNREACHABLE: 'blocked-update-unreachable', - BLOCKED_SECURITY_UPDATE_REQUIRED: 'blocked-security-update-required', - BLOCKED_SECURITY_UPDATE_MANAGED: 'blocked-security-update-managed', BLOCKED_SHELL_UPDATE: 'blocked-shell-update', - BLOCKED_SHELL_UPDATE_MANAGED: 'blocked-shell-update-managed', BLOCKED_UNSUPPORTED_BUILD: 'blocked-unsupported-build', LAUNCHING: 'launching', UNREACHABLE_LAUNCH: 'unreachable-launch', @@ -74,13 +59,6 @@ export interface SplashActionDescriptor { readonly label: string; } -export interface SplashDownloadOption { - readonly value: string; - readonly label: string; - readonly buttonLabel: string; - readonly kind: SplashActionKind; -} - export const SplashAction = Object.freeze({ RETRY: Object.freeze({kind: SplashActionKind.RETRY, label: 'Retry now'}), DOWNLOAD: Object.freeze({kind: SplashActionKind.DOWNLOAD, label: 'Download update'}), @@ -89,8 +67,6 @@ export const SplashAction = Object.freeze({ export interface SplashState { readonly status: SplashStatus; - readonly requiredSecurityUpdate?: boolean; - readonly layout?: SplashLayout | null; readonly current?: number | null; readonly total?: number | null; readonly progress?: number | null; @@ -100,14 +76,10 @@ export interface SplashState { readonly bytesPerSecond?: number | null; readonly action?: SplashActionDescriptor | null; readonly message?: string | null; - readonly versionLabel?: string | null; - readonly options?: ReadonlyArray | null; } export interface SerializedSplashState { - readonly layout: SplashLayout; readonly status: SplashStatus; - readonly requiredSecurityUpdate: boolean; readonly current: number | null; readonly total: number | null; readonly progress: number | null; @@ -117,22 +89,18 @@ export interface SerializedSplashState { readonly bytesPerSecond: number | null; readonly action: SplashActionDescriptor | null; readonly message: string | null; - readonly versionLabel: string | null; - readonly options: ReadonlyArray | null; } type SplashListener = () => void; -type SplashDownloadListener = (value: string | null) => void; const logger = createChildLogger('SplashWindow'); -const SPLASH_LAYOUTS = new Set(Object.values(SplashLayout)); const SPLASH_STATUSES = new Set(Object.values(SplashStatus)); const SPLASH_ACTION_KINDS = new Set(Object.values(SplashActionKind)); const readyListeners = new Set(); const retryListeners = new Set(); const quitListeners = new Set(); const networkOnlineListeners = new Set(); -const openDownloadListeners = new Set(); +const openDownloadListeners = new Set(); const openLogsListeners = new Set(); const copyDiagnosticsListeners = new Set(); @@ -172,41 +140,9 @@ function toSplashAction(action: SplashActionDescriptor | null | undefined): Spla return {kind: action.kind, label}; } -function toSplashOption(option: SplashDownloadOption): SplashDownloadOption | null { - if (option == null || typeof option !== 'object') return null; - if (typeof option.value !== 'string' || !SPLASH_OPTION_VALUE_PATTERN.test(option.value)) return null; - if (!SPLASH_ACTION_KINDS.has(option.kind)) return null; - const label = toSplashText(option.label, SPLASH_OPTION_LABEL_MAX_LENGTH); - if (label == null) return null; - const buttonLabel = toSplashText(option.buttonLabel, SPLASH_OPTION_BUTTON_LABEL_MAX_LENGTH); - if (buttonLabel == null) return null; - return {value: option.value, label, buttonLabel, kind: option.kind}; -} - -function toSplashOptions( - options: ReadonlyArray | null | undefined, -): ReadonlyArray | null { - if (!Array.isArray(options)) return null; - const accepted: Array = []; - for (const option of options.slice(0, SPLASH_OPTION_MAX_COUNT)) { - const normalized = toSplashOption(option); - if (normalized != null) { - accepted.push(normalized); - } - } - return accepted.length === 0 ? null : accepted; -} - -function withManualUpdateAffordance(state: SerializedSplashState): SerializedSplashState { - if (state.layout !== SplashLayout.MANUAL_UPDATE || hasSplashAffordance(state)) return state; - return {...state, action: SplashAction.QUIT}; -} - export function serializeSplashState(state: SplashState): SerializedSplashState { - return withManualUpdateAffordance({ - layout: SPLASH_LAYOUTS.has(state.layout as string) ? (state.layout as SplashLayout) : SplashLayout.SPLASH, + return { status: SPLASH_STATUSES.has(state.status) ? state.status : SplashStatus.CHECKING_FOR_UPDATES, - requiredSecurityUpdate: state.requiredSecurityUpdate === true, current: toSplashCount(state.current), total: toSplashCount(state.total), progress: toSplashProgress(state.progress), @@ -216,18 +152,11 @@ export function serializeSplashState(state: SplashState): SerializedSplashState bytesPerSecond: toSplashCount(state.bytesPerSecond), action: toSplashAction(state.action), message: toSplashText(state.message, SPLASH_MESSAGE_MAX_LENGTH), - versionLabel: toSplashText(state.versionLabel, SPLASH_VERSION_LABEL_MAX_LENGTH), - options: toSplashOptions(state.options), - }); + }; } export function hasSplashAffordance(state: SerializedSplashState): boolean { - return state.action != null || state.options != null; -} - -export function toSplashDownloadValue(value: unknown): string | null { - if (typeof value !== 'string') return null; - return SPLASH_OPTION_VALUE_PATTERN.test(value) ? value : null; + return state.action != null; } export function getSplashWindowHeight(platform: NodeJS.Platform): number { @@ -238,10 +167,7 @@ export function shouldQuitOnSplashClosed(platform: NodeJS.Platform, isLaunchLatc return platform !== 'darwin' && !isLaunchLatched; } -function addSplashListener( - listeners: Set, - listener: T, -): () => void { +function addSplashListener(listeners: Set, listener: SplashListener): () => void { listeners.add(listener); return () => { listeners.delete(listener); @@ -258,16 +184,6 @@ function emitSplashListeners(listeners: Set): void { } } -function emitSplashDownloadListeners(value: string | null): void { - for (const listener of Array.from(openDownloadListeners)) { - try { - listener(value); - } catch (error) { - logger.error('A splash listener threw', error); - } - } -} - function isSplashSender(event: IpcMainEvent): boolean { return splashWindow != null && !splashWindow.isDestroyed() && event.sender === splashWindow.webContents; } @@ -311,9 +227,9 @@ function registerSplashIpc(): void { if (!isSplashSender(event)) return; emitSplashListeners(networkOnlineListeners); }); - ipcMain.on(DESKTOP_SPLASH_OPEN_DOWNLOAD_CHANNEL, (event, value: unknown) => { + ipcMain.on(DESKTOP_SPLASH_OPEN_DOWNLOAD_CHANNEL, (event) => { if (!isSplashSender(event)) return; - emitSplashDownloadListeners(toSplashDownloadValue(value)); + emitSplashListeners(openDownloadListeners); }); ipcMain.on(DESKTOP_SPLASH_OPEN_LOGS_CHANNEL, (event) => { if (!isSplashSender(event)) return; @@ -527,7 +443,7 @@ export function onSplashNetworkOnline(listener: SplashListener): () => void { return addSplashListener(networkOnlineListeners, listener); } -export function onSplashOpenDownload(listener: SplashDownloadListener): () => void { +export function onSplashOpenDownload(listener: SplashListener): () => void { return addSplashListener(openDownloadListeners, listener); } diff --git a/fluxer_desktop/src/main/Updater.ts b/fluxer_desktop/src/main/Updater.ts index 4d3ab11de..f9c31431c 100644 --- a/fluxer_desktop/src/main/Updater.ts +++ b/fluxer_desktop/src/main/Updater.ts @@ -8,12 +8,10 @@ import {MANUAL_DESKTOP_FORMATS, type ManualDesktopFormat} from '@electron/main/S import {resolveShellUpdatePlan, ShellUpdateCapability} from '@electron/main/ShellUpdateCapability'; import { DOWNLOAD_PAGE_URL, - getManualDownloadOptions, getManualDownloadUrl, type ManualLatestFile, type ManualLatestInfo, UPDATE_BASE_URL, - type UpdaterDownloadOption, } from '@electron/main/UpdaterDownloads'; import {app, type BrowserWindow, ipcMain, net} from 'electron'; import log from 'electron-log'; @@ -29,7 +27,6 @@ type UpdaterEvent = context: UpdaterContext; version?: string | null; downloadUrl?: string; - downloadOptions?: Array; } | { type: 'not-available'; @@ -43,7 +40,7 @@ type UpdaterEvent = | { type: 'unsupported'; context: UpdaterContext; - reason: 'platform' | 'unpackaged' | 'managed-package'; + reason: 'platform' | 'unpackaged'; downloadUrl?: string; }; @@ -104,7 +101,9 @@ async function fetchManualLatest(options: {forceRefresh?: boolean} = {}): Promis export async function findNewerManualShell(): Promise { const plan = resolveShellUpdatePlan(); - if (plan.capability !== ShellUpdateCapability.MANUAL_DOWNLOAD || plan.reason !== 'platform') { + const managed = plan.capability === ShellUpdateCapability.MANAGED_PACKAGE; + const manual = plan.capability === ShellUpdateCapability.MANUAL_DOWNLOAD && plan.reason === 'platform'; + if (!managed && !manual) { return null; } const latest = await fetchManualLatest({forceRefresh: true}); @@ -116,13 +115,11 @@ function sendManualUpdateAvailable( context: UpdaterContext, latest: ManualLatestInfo, ): void { - const downloadOptions = getManualDownloadOptions(latest); send(getMainWindow(), { type: 'available', context, version: latest.version, downloadUrl: getManualDownloadUrl(latest), - ...(downloadOptions.length > 0 ? {downloadOptions} : {}), }); } @@ -157,10 +154,7 @@ function registerSelfUpdater(getMainWindow: () => BrowserWindow | null): void { }); } -function registerManualUpdater( - getMainWindow: () => BrowserWindow | null, - reason: 'platform' | 'unpackaged' | 'managed-package', -): void { +function registerManualUpdater(getMainWindow: () => BrowserWindow | null, unpackaged: boolean): void { ipcMain.handle('updater-check', async (_e, context: UpdaterContext) => { if (context === 'user') { try { @@ -169,13 +163,8 @@ function registerManualUpdater( log.warn('Desktop module update check failed', error); } } - if (reason !== 'platform') { - send(getMainWindow(), { - type: 'unsupported', - context, - reason, - ...(reason === 'unpackaged' ? {downloadUrl: DOWNLOAD_PAGE_URL} : {}), - }); + if (unpackaged) { + send(getMainWindow(), {type: 'unsupported', context, reason: 'unpackaged', downloadUrl: DOWNLOAD_PAGE_URL}); return; } await checkManualUpdate(context, getMainWindow); @@ -184,14 +173,12 @@ function registerManualUpdater( export function registerUpdater(getMainWindow: () => BrowserWindow | null) { const plan = resolveShellUpdatePlan(); - switch (plan.capability) { - case ShellUpdateCapability.SELF_UPDATE: - registerSelfUpdater(getMainWindow); - return; - case ShellUpdateCapability.MANAGED_PACKAGE: - registerManualUpdater(getMainWindow, 'managed-package'); - return; - default: - registerManualUpdater(getMainWindow, plan.reason); + if (plan.capability === ShellUpdateCapability.SELF_UPDATE) { + registerSelfUpdater(getMainWindow); + return; } + registerManualUpdater( + getMainWindow, + plan.capability === ShellUpdateCapability.MANUAL_DOWNLOAD && plan.reason === 'unpackaged', + ); } diff --git a/fluxer_desktop/src/main/UpdaterDownloads.ts b/fluxer_desktop/src/main/UpdaterDownloads.ts index f63685b04..a860e3c82 100644 --- a/fluxer_desktop/src/main/UpdaterDownloads.ts +++ b/fluxer_desktop/src/main/UpdaterDownloads.ts @@ -2,24 +2,7 @@ import {BUILD_CHANNEL} from '@electron/common/BuildChannel'; import {DOWNLOAD_PAGE_URLS} from '@electron/common/Constants'; -import {DESKTOP_ARTIFACT_PRODUCT_NAME} from '@electron/common/DesktopIdentity'; -import { - DESKTOP_DOWNLOAD_ARCH, - getUpdateBaseUrl, - isLinuxManualDesktopFormat, - LINUX_MANUAL_ARCH_TOKENS, - LINUX_MANUAL_FORMAT_EXTENSIONS, - type LinuxManualDesktopFormat, - type ManualDesktopFormat, -} from '@electron/main/ShellDownloadFormats'; - -export type UpdaterDownloadOption = { - format: ManualDesktopFormat; - label: string; - url: string; - suggestedName?: string; - sha256?: string | null; -}; +import {getUpdateBaseUrl, type ManualDesktopFormat} from '@electron/main/ShellDownloadFormats'; export const UPDATE_BASE_URL = getUpdateBaseUrl(); export const DOWNLOAD_PAGE_URL = DOWNLOAD_PAGE_URLS[BUILD_CHANNEL]; @@ -33,9 +16,6 @@ export type ManualLatestInfo = { }; function getManualDownloadFormatPreference(): Array { - if (process.platform === 'linux') { - return ['appimage', 'deb', 'rpm', 'tar_gz']; - } if (process.platform === 'darwin') { return ['dmg', 'zip']; } @@ -45,50 +25,7 @@ function getManualDownloadFormatPreference(): Array { return []; } -const LINUX_MANUAL_FORMAT_LABELS: Record = { - appimage: 'AppImage', - deb: 'DEB package', - rpm: 'RPM package', - tar_gz: 'tar.gz archive', -}; - -export function buildManualVersionDownloadUrl(version: string, format: ManualDesktopFormat): string { - return `${UPDATE_BASE_URL}/${version}/${format}`; -} - -function getArtifactProductName(): string { - return DESKTOP_ARTIFACT_PRODUCT_NAME; -} - -function getManualUpdateSuggestedName(format: LinuxManualDesktopFormat, version: string): string { - const archToken = LINUX_MANUAL_ARCH_TOKENS[format][DESKTOP_DOWNLOAD_ARCH]; - const extension = LINUX_MANUAL_FORMAT_EXTENSIONS[format]; - return `${getArtifactProductName()}-${version}-linux-${archToken}${extension}`; -} - -export function getManualDownloadOptions(info: ManualLatestInfo): Array { - if (process.platform !== 'linux') { - return []; - } - return getManualDownloadFormatPreference() - .filter(isLinuxManualDesktopFormat) - .map((format) => { - const file = info.files[format]; - return { - format, - label: LINUX_MANUAL_FORMAT_LABELS[format], - url: buildManualVersionDownloadUrl(info.version, format), - suggestedName: getManualUpdateSuggestedName(format, info.version), - sha256: file?.sha256 ?? null, - }; - }); -} - export function getManualDownloadUrl(info: ManualLatestInfo): string { - const [preferredOption] = getManualDownloadOptions(info); - if (preferredOption) { - return preferredOption.url; - } for (const format of getManualDownloadFormatPreference()) { const url = info.files[format]?.url; if (url) { diff --git a/fluxer_desktop/src/preload/splash.cjs b/fluxer_desktop/src/preload/splash.cjs index 7392d8648..a30e9b379 100644 --- a/fluxer_desktop/src/preload/splash.cjs +++ b/fluxer_desktop/src/preload/splash.cjs @@ -24,11 +24,6 @@ const DIAGNOSTICS_COPIED_MS = 2000; const BYTES_PER_MEGABYTE = 1000000; const BYTES_PER_KILOBYTE = 1000; -const SplashLayout = Object.freeze({ - SPLASH: 'splash', - MANUAL_UPDATE: 'manual-update', -}); - const SplashStatus = Object.freeze({ CHECKING_FOR_UPDATES: 'checking-for-updates', DOWNLOADING_UPDATES: 'downloading-updates', @@ -40,10 +35,7 @@ const SplashStatus = Object.freeze({ SHELL_UPDATE_RESTARTING: 'shell-update-restarting', BLOCKED_UPDATE_REQUIRED: 'blocked-update-required', BLOCKED_UPDATE_UNREACHABLE: 'blocked-update-unreachable', - BLOCKED_SECURITY_UPDATE_REQUIRED: 'blocked-security-update-required', - BLOCKED_SECURITY_UPDATE_MANAGED: 'blocked-security-update-managed', BLOCKED_SHELL_UPDATE: 'blocked-shell-update', - BLOCKED_SHELL_UPDATE_MANAGED: 'blocked-shell-update-managed', BLOCKED_UNSUPPORTED_BUILD: 'blocked-unsupported-build', LAUNCHING: 'launching', UNREACHABLE_LAUNCH: 'unreachable-launch', @@ -61,7 +53,6 @@ const DIAGNOSTICS_STATUSES = new Set([ SplashStatus.DOWNLOAD_STALLED, SplashStatus.BLOCKED_UPDATE_REQUIRED, SplashStatus.BLOCKED_UPDATE_UNREACHABLE, - SplashStatus.BLOCKED_SECURITY_UPDATE_REQUIRED, SplashStatus.BLOCKED_SHELL_UPDATE, SplashStatus.BLOCKED_UNSUPPORTED_BUILD, SplashStatus.UNREACHABLE_LAUNCH, @@ -73,9 +64,7 @@ const ACTION_CHANNELS = new Map([ ['quit', SPLASH_QUIT_CHANNEL], ]); const DEFAULT_STATE = Object.freeze({ - layout: SplashLayout.SPLASH, status: SplashStatus.CHECKING_FOR_UPDATES, - requiredSecurityUpdate: false, current: null, total: null, progress: null, @@ -85,15 +74,11 @@ const DEFAULT_STATE = Object.freeze({ bytesPerSecond: null, action: null, message: null, - versionLabel: null, - options: null, }); let splashState = null; -let renderedLayout = null; -let renderedManualSignature = null; +let splashBuilt = false; let countdownTimer = null; -let selectedOption = null; let splashStartedAt = Number.POSITIVE_INFINITY; let diagnosticsRevealTimer = null; let diagnosticsCopiedTimer = null; @@ -101,43 +86,25 @@ let diagnosticsCopiedTimer = null; function getStatusText(state) { switch (state.status) { case SplashStatus.DOWNLOADING_UPDATES: - return state.requiredSecurityUpdate - ? `Downloading required security update ${state.current ?? 0} of ${state.total ?? 0}…` - : `Downloading update ${state.current ?? 0} of ${state.total ?? 0}…`; + return `Downloading update ${state.current ?? 0} of ${state.total ?? 0}…`; case SplashStatus.INSTALLING_UPDATES: - return state.requiredSecurityUpdate - ? `Installing required security update ${state.current ?? 0} of ${state.total ?? 0}…` - : `Installing update ${state.current ?? 0} of ${state.total ?? 0}…`; + return `Installing update ${state.current ?? 0} of ${state.total ?? 0}…`; case SplashStatus.VERIFYING: - return state.requiredSecurityUpdate ? 'Verifying required security update…' : 'Verifying files…'; + return 'Verifying files…'; case SplashStatus.DOWNLOAD_STALLED: return `Download stalled. Retrying in ${state.seconds ?? 0} sec…`; case SplashStatus.UPDATE_FAILURE: - return state.requiredSecurityUpdate - ? `Required security update failed. Retrying in ${state.seconds ?? 0} sec…` - : `Update failed. Retrying in ${state.seconds ?? 0} sec…`; + return `Update failed. Retrying in ${state.seconds ?? 0} sec…`; case SplashStatus.SHELL_UPDATE_DOWNLOADING: - return state.requiredSecurityUpdate ? 'Downloading required security update…' : 'Updating Fluxer…'; + return 'Updating Fluxer…'; case SplashStatus.SHELL_UPDATE_RESTARTING: - return state.requiredSecurityUpdate - ? 'Restarting to finish the required security update…' - : 'Restarting to finish the update…'; + return 'Restarting to finish the update…'; case SplashStatus.BLOCKED_UPDATE_REQUIRED: - return state.requiredSecurityUpdate - ? 'A required security update is needed to continue' - : 'Update required to continue'; + return 'Update required to continue'; case SplashStatus.BLOCKED_UPDATE_UNREACHABLE: return "Can't reach the Fluxer update server"; - case SplashStatus.BLOCKED_SECURITY_UPDATE_REQUIRED: - return 'A required security update is needed to continue'; - case SplashStatus.BLOCKED_SECURITY_UPDATE_MANAGED: - return 'Install the required security update through your package manager'; case SplashStatus.BLOCKED_SHELL_UPDATE: - return state.requiredSecurityUpdate - ? 'A required security update is needed to continue' - : 'A new version of Fluxer is required'; - case SplashStatus.BLOCKED_SHELL_UPDATE_MANAGED: - return 'Update Fluxer through your package manager'; + return 'A new version of Fluxer is required'; case SplashStatus.BLOCKED_UNSUPPORTED_BUILD: return 'This installation is incomplete. Reinstall Fluxer'; case SplashStatus.LAUNCHING: @@ -147,7 +114,7 @@ function getStatusText(state) { case SplashStatus.WAITING_LOCAL_NETWORK: return 'Waiting for local network permission…'; default: - return state.requiredSecurityUpdate ? 'Checking for required security update…' : 'Checking for updates…'; + return 'Checking for updates…'; } } @@ -184,7 +151,6 @@ function getMetricsText(state) { } function shouldShowDiagnostics(state) { - if (state.layout !== SplashLayout.SPLASH) return false; if (DIAGNOSTICS_HIDDEN_STATUSES.has(state.status)) return false; if (DIAGNOSTICS_STATUSES.has(state.status)) return true; return Date.now() - splashStartedAt >= DIAGNOSTICS_REVEAL_MS; @@ -223,19 +189,15 @@ function createProgressPlaceholder() { return placeholder; } -function sendSplashAction(kind, value) { +function sendSplashAction(kind) { const channel = ACTION_CHANNELS.get(kind); if (channel == null) return; - if (value === undefined) { - ipcRenderer.send(channel); - return; - } - ipcRenderer.send(channel, value); + ipcRenderer.send(channel); } function onSplashActionClick() { if (splashState == null || splashState.action == null) return; - sendSplashAction(splashState.action.kind, undefined); + sendSplashAction(splashState.action.kind); } function buildSplashLayout(mount) { @@ -329,111 +291,12 @@ function reconcileAction(mount, state) { existing.textContent = state.action.label; } -function findSelectedOption() { - if (splashState == null || splashState.options == null) return null; - return splashState.options.find((option) => option.value === selectedOption) ?? null; -} - -function manualUpdateSignature(state) { - return JSON.stringify([state.action, state.options]); -} - -function patchManualUpdateText(mount, state) { - const message = mount.querySelector('.dl-update-message'); - if (message != null) { - message.textContent = state.message ?? ''; - } - const version = mount.querySelector('.dl-version-message'); - if (version != null) { - version.textContent = state.versionLabel ?? ''; - } -} - -function buildManualUpdateLayout(mount, state) { - const splash = createNode('div'); - splash.id = 'splash'; - const inner = createNode('div', 'splash-inner-dl'); - const markBox = createNode('div', 'splash-mark-dl'); - markBox.appendChild(createMark(null)); - const message = createNode('div', 'dl-update-message'); - message.textContent = state.message ?? ''; - const version = createNode('div', 'dl-version-message'); - version.textContent = state.versionLabel ?? ''; - inner.appendChild(markBox); - inner.appendChild(message); - inner.appendChild(buildManualUpdateControl(state)); - inner.appendChild(version); - splash.appendChild(inner); - mount.replaceChildren(splash); -} - -function buildManualUpdateControl(state) { - return state.options == null ? buildManualUpdateAction(state) : buildManualUpdatePicker(state); -} - -function buildManualUpdateAction(state) { - const frame = createNode('div', 'dl-action-frame'); - const button = createNode('button', 'dl-button'); - button.id = 'dl-button'; - button.textContent = state.action.label; - button.addEventListener('click', onSplashActionClick); - frame.appendChild(button); - return frame; -} - -function buildManualUpdatePicker(state) { - const frame = createNode('div', 'dl-select-frame'); - const selectBox = createNode('div', 'dl-select'); - const select = createNode('select'); - select.id = 'dl-select-input'; - for (const option of state.options) { - const node = createNode('option'); - node.value = option.value; - node.textContent = option.label; - select.appendChild(node); - } - selectBox.appendChild(select); - const button = createNode('button', 'dl-button'); - button.id = 'dl-button'; - if (findSelectedOption() == null) { - selectedOption = state.options[0].value; - } - const initial = findSelectedOption(); - select.value = selectedOption; - button.textContent = initial == null ? '' : initial.buttonLabel; - select.addEventListener('change', () => { - selectedOption = select.value; - const option = findSelectedOption(); - button.textContent = option == null ? '' : option.buttonLabel; - }); - button.addEventListener('click', () => { - const option = findSelectedOption(); - if (option == null) return; - sendSplashAction(option.kind, option.value); - }); - frame.appendChild(selectBox); - frame.appendChild(button); - return frame; -} - function render() { const mount = document.getElementById(SPLASH_MOUNT_ID); if (mount == null) return; const state = splashState ?? DEFAULT_STATE; - if (state.layout === SplashLayout.MANUAL_UPDATE) { - const signature = manualUpdateSignature(state); - if (renderedLayout !== SplashLayout.MANUAL_UPDATE || renderedManualSignature !== signature) { - renderedLayout = SplashLayout.MANUAL_UPDATE; - renderedManualSignature = signature; - buildManualUpdateLayout(mount, state); - return; - } - patchManualUpdateText(mount, state); - return; - } - if (renderedLayout !== SplashLayout.SPLASH) { - renderedLayout = SplashLayout.SPLASH; - renderedManualSignature = null; + if (!splashBuilt) { + splashBuilt = true; buildSplashLayout(mount); } const statusNode = mount.querySelector('.splash-status'); @@ -454,7 +317,7 @@ function render() { } function hasCountdown(state) { - return state.layout === SplashLayout.SPLASH && state.seconds != null && state.seconds > 0; + return state.seconds != null && state.seconds > 0; } function stopCountdown() { diff --git a/fluxer_desktop/src/splash/index.css b/fluxer_desktop/src/splash/index.css index 85b3c065c..0b280475c 100644 --- a/fluxer_desktop/src/splash/index.css +++ b/fluxer_desktop/src/splash/index.css @@ -219,155 +219,3 @@ html { .splash-diagnostics-link:hover { color: var(--white); } - -#splash:has(.splash-inner-dl) { - align-items: stretch; - padding-bottom: 0; -} - -.splash-inner-dl { - -webkit-app-region: drag; - display: flex; - flex-direction: column; - align-items: center; - width: 100%; - height: 100%; - padding: 30px 20px 12px; - text-align: center; - overflow: hidden; -} - -.splash-inner-dl .splash-mark-dl { - width: 92px; - height: 92px; - display: flex; - align-items: center; - justify-content: center; - flex: none; -} - -.splash-inner-dl .splash-mark-dl img { - width: 88px; - height: 88px; - display: block; -} - -.dl-update-message { - font-family: var(--primary-font); - font-size: 17px; - font-weight: 500; - line-height: 1.35; - color: var(--white); - margin: 26px 0 0; - max-width: 26ch; - text-wrap: balance; - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 4; - overflow: hidden; - flex: 0 1 auto; - min-height: 0; -} - -.dl-action-frame { - -webkit-app-region: no-drag; - font-family: var(--primary-font); - display: flex; - justify-content: center; - width: 100%; - margin-top: 22px; - flex: none; -} - -.dl-select-frame { - -webkit-app-region: no-drag; - font-family: var(--primary-font); - display: flex; - align-items: center; - gap: 10px; - width: 100%; - margin-top: 22px; - flex: none; -} - -.dl-select { - position: relative; - flex: 1 1 auto; - min-width: 0; - height: 44px; - color: var(--white); -} - -.dl-select select { - width: 100%; - height: 100%; - border: 1px solid rgba(255, 255, 255, 0.3); - border-radius: 4px; - background: var(--window-background); - color: var(--white); - padding: 0 32px 0 12px; - font-size: 14px; - font-family: inherit; - outline: none; - appearance: none; - cursor: pointer; - text-overflow: ellipsis; -} - -.dl-select select:focus-visible { - border-color: var(--brand); - box-shadow: 0 0 0 2px rgba(70, 65, 217, 0.45); -} - -.dl-select::after { - content: ''; - position: absolute; - right: 13px; - top: 50%; - width: 0; - height: 0; - margin-top: -2px; - transform: translateY(-50%); - border-left: 5px solid transparent; - border-right: 5px solid transparent; - border-top: 6px solid var(--muted); - pointer-events: none; -} - -.dl-button { - flex: none; - padding: 0; - width: 92px; - height: 44px; - background-color: var(--brand); - color: var(--white); - font-size: 14px; - font-weight: 600; - border: none; - border-radius: 4px; - display: flex; - justify-content: center; - align-items: center; - cursor: pointer; - font-family: inherit; -} - -.dl-button:hover { - background-color: var(--brand-bright); -} - -.dl-button:focus-visible { - outline: 2px solid var(--brand-bright); - outline-offset: 2px; -} - -.dl-version-message { - font-family: var(--primary-font); - font-size: 11px; - letter-spacing: 0.04em; - color: var(--muted); - text-transform: uppercase; - margin: auto 0 0; - padding-top: 18px; - flex: none; -} diff --git a/tools/ci/src/desktop.rs b/tools/ci/src/desktop.rs index 4e76c7045..0d8bcb046 100644 --- a/tools/ci/src/desktop.rs +++ b/tools/ci/src/desktop.rs @@ -9,13 +9,15 @@ use crate::common::{ }; use crate::functions::{sha256_file, write_json_pretty}; use crate::release::{ - DESKTOP_RELEASE_DESCRIPTOR_SCHEMA_VERSION, DesktopReleaseAsset, DesktopReleaseDescriptor, - DesktopReleaseKind, RELEASE_REPOSITORY, desktop_module_manifest_release_asset_name, + DESKTOP_RELEASE_DESCRIPTOR_SCHEMA_VERSION, DesktopPublishedRelease, DesktopReleaseAsset, + DesktopReleaseDescriptor, DesktopReleaseKind, desktop_module_manifest_release_asset_name, desktop_module_manifest_storage_key, desktop_module_package_release_asset_name, desktop_module_package_storage_key, desktop_release_asset_name, desktop_release_coordinates, - desktop_release_descriptor_filename, desktop_release_product, desktop_release_shipped_formats, - desktop_release_update_payload_suffix, desktop_release_updater_feeds, - validate_desktop_release_descriptor, validate_desktop_release_module_files, + desktop_release_descriptor_filename, desktop_release_kind, desktop_release_product, + desktop_release_shipped_formats, desktop_release_update_payload_suffix, + desktop_release_updater_feeds, desktop_releases, download_desktop_release_asset, + resolve_commit_sha, validate_desktop_release_descriptor, validate_desktop_release_module_files, + validate_full_sha, }; use anyhow::{Context, Result, anyhow, bail, ensure}; use chrono::{DateTime, Utc}; @@ -156,7 +158,7 @@ enum DesktopStep { BuildModuleManifest, PrepareReleaseAssets, BuildSummary, - CheckShellDrift, + CheckShellContract, BuildModulesOnlyManifest, PrepareModulesReleaseAssets, } @@ -281,7 +283,7 @@ pub async fn run(args: BuildDesktopArgs) -> Result<()> { DesktopStep::BuildModuleManifest => build_module_manifest_step(), DesktopStep::PrepareReleaseAssets => prepare_release_assets_step(), DesktopStep::BuildSummary => build_summary_step(), - DesktopStep::CheckShellDrift => check_shell_drift_step().await, + DesktopStep::CheckShellContract => check_shell_contract_step().await, DesktopStep::BuildModulesOnlyManifest => build_modules_only_manifest_step(), DesktopStep::PrepareModulesReleaseAssets => prepare_modules_release_assets_step(), } @@ -5203,6 +5205,8 @@ pub(crate) struct DesktopChannelManifest { pub(crate) shell: DesktopChannelManifestShell, pub(crate) modules: BTreeMap, pub(crate) required_modules: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(crate) shell_compatibility: Option, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -5329,18 +5333,23 @@ fn stage_desktop_module_packages( fn desktop_channel_manifest_entries( channel: &str, packed: &[DesktopPackedModule], + compatibility: &DesktopShellCompatibility, + live: Option<&Value>, ) -> BTreeMap { packed .iter() .map(|module| { + let (minimum_shell_version, maximum_shell_version) = + inherited_shell_range(live, &module.module, &module.sha256) + .unwrap_or_else(|| (compatibility.minimum_shell_version.clone(), None)); ( module.module.clone(), DesktopChannelManifestEntry { sha256: module.sha256.clone(), bytes: module.bytes, url: desktop_module_package_url(channel, &module.module, &module.sha256), - minimum_shell_version: DESKTOP_MODULE_MINIMUM_SHELL_VERSION.to_string(), - maximum_shell_version: None, + minimum_shell_version, + maximum_shell_version, }, ) }) @@ -5451,6 +5460,7 @@ fn write_desktop_channel_manifests( pub_date: &str, packed: &[DesktopPackedModule], targets: &[(String, String)], + coordinates: &BTreeMap, ) -> Result<()> { ensure!( !targets.is_empty(), @@ -5459,8 +5469,11 @@ fn write_desktop_channel_manifests( ); let metadata_version = desktop_module_metadata_version(pub_date)?; stage_desktop_module_packages(payload_root, channel, packed)?; - let modules = desktop_channel_manifest_entries(channel, packed); for (platform, arch) in targets { + let key = desktop_coordinate_key(platform, arch); + let coordinate = coordinates + .get(&key) + .with_context(|| format!("The shell contract holds no {key} decision"))?; let manifest = DesktopChannelManifest { manifest_version: DESKTOP_CHANNEL_MANIFEST_VERSION, release_channel: channel.to_string(), @@ -5473,11 +5486,17 @@ fn write_desktop_channel_manifests( latest_version: build_version.to_string(), minimum_version: DESKTOP_MODULE_MINIMUM_SHELL_VERSION.to_string(), }, - modules: modules.clone(), + modules: desktop_channel_manifest_entries( + channel, + packed, + &coordinate.compatibility, + coordinate.live.as_ref(), + ), required_modules: DESKTOP_REQUIRED_MODULES .iter() .map(|module| (*module).to_string()) .collect(), + shell_compatibility: Some(coordinate.compatibility.clone()), }; let manifest_path = desktop_channel_manifest_path(payload_root, channel, platform, arch); write_desktop_channel_manifest(&manifest_path, &manifest)?; @@ -5501,6 +5520,29 @@ fn build_module_manifest_step() -> Result<()> { let pub_date = require_env("PUB_DATE")?; let packed = read_packed_desktop_modules(&desktop_modules_dir(), &channel, &build_version)?; let targets = desktop_module_manifest_targets(&payload_root, &channel)?; + let coordinates = match read_shell_contract(&channel)? { + Some(contract) => contract.coordinates, + None => { + ensure!( + env_bool("DESKTOP_MODULE_ONLY"), + "Missing {}, run the check_shell_contract step first", + shell_contract_path().display() + ); + let source_sha = env_string("SOURCE_SHA").unwrap_or_else(|| "development".to_string()); + targets + .iter() + .map(|(platform, arch)| { + ( + desktop_coordinate_key(platform, arch), + DesktopShellContractCoordinate { + live: None, + compatibility: baseline_shell_compatibility(&source_sha), + }, + ) + }) + .collect() + } + }; write_desktop_channel_manifests( &payload_root, &channel, @@ -5508,6 +5550,7 @@ fn build_module_manifest_step() -> Result<()> { &pub_date, &packed, &targets, + &coordinates, )?; for module in &packed { println!( @@ -5523,16 +5566,94 @@ fn build_module_manifest_step() -> Result<()> { Ok(()) } -const DESKTOP_ALLOW_SHELL_DRIFT_ENV: &str = "ALLOW_SHELL_DRIFT"; -const DESKTOP_LIVE_SHELL_FILE: &str = "live-shell.json"; +const DESKTOP_SHELL_CONTRACT_ENV: &str = "SHELL_CONTRACT"; +const DESKTOP_SHELL_CONTRACT_INPUT: &str = "shell_contract"; +const DESKTOP_MODULES_ONLY_RELEASE_ENV: &str = "DESKTOP_MODULES_ONLY_RELEASE"; +const DESKTOP_SHELL_CONTRACT_FILE: &str = "shell-contract.json"; const DESKTOP_LIVE_MANIFEST_TIMEOUT: Duration = Duration::from_secs(30); +const DESKTOP_FIRST_MODULE_SHELL_COMMIT: &str = "7c5fa2180a60e4d39bb06c3c384557fd9a3eabc4"; +const DESKTOP_CONTRACT_ROOTS: &[&str] = &[ + "fluxer_desktop/src/preload/index.ts", + "fluxer_app/src/types/electron.d.ts", +]; +const DESKTOP_CONTRACT_MAIN_DIR: &str = "fluxer_desktop/src/main/"; +const DESKTOP_CONTRACT_MAIN_BOUNDARY_PREFIX: &str = "LocalApp"; +const DESKTOP_CONTRACT_IPC_CALLS: &[&str] = &[ + "ipcMain.handle(", + "ipcMain.handleOnce(", + "ipcMain.on(", + "ipcMain.once(", + ".send(", +]; +const DESKTOP_CONTRACT_PROBE_SUFFIXES: &[&str] = &[ + "", + ".ts", + ".tsx", + ".d.ts", + ".mts", + ".cts", + ".mjs", + ".cjs", + ".js", + "/index.ts", +]; +const DESKTOP_CONTRACT_ELECTRON_ALIAS: &str = "@electron/"; +const DESKTOP_CONTRACT_ELECTRON_ROOT: &str = "fluxer_desktop/src/"; +const DESKTOP_CONTRACT_PACKAGE_ALIAS: &str = "@fluxer/"; +const DESKTOP_CONTRACT_EXPORT_CONDITIONS: &[&str] = &["types", "import", "default"]; +const DESKTOP_CONTRACT_GENERATED_MODULES: &[&str] = &["@electron/common/BuildChannel"]; + +#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub(crate) enum DesktopShellContractChoice { + Unchanged, + Compatible, + Raise, + Baseline, +} + +impl DesktopShellContractChoice { + fn parse_input(value: &str) -> Result { + match value { + "unchanged" => Ok(Self::Unchanged), + "compatible" => Ok(Self::Compatible), + "raise" => Ok(Self::Raise), + other => bail!( + "{DESKTOP_SHELL_CONTRACT_INPUT} must be unchanged, compatible or raise, got {other:?}" + ), + } + } +} #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] -struct DesktopLiveShell { +pub(crate) struct DesktopShellCompatibility { + pub(crate) minimum_shell_version: String, + pub(crate) source_sha: String, + pub(crate) previous_source_sha: String, + pub(crate) floor_source_sha: Option, + pub(crate) decision: DesktopShellContractChoice, + pub(crate) contract_base_sha: String, + pub(crate) contract_changes: Vec, +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +struct DesktopShellContractCoordinate { + live: Option, + compatibility: DesktopShellCompatibility, +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +struct DesktopShellContract { channel: String, - shell_version: String, - shell_sha: String, - manifests: BTreeMap, + source_sha: String, + choice: DesktopShellContractChoice, + coordinates: BTreeMap, +} + +#[derive(Debug, Clone, PartialEq)] +enum DesktopLiveFeed { + Missing, + Served(Value), } fn desktop_coordinate_key(platform: &str, arch: &str) -> String { @@ -5546,33 +5667,117 @@ fn desktop_live_manifest_url(channel: &str, platform: &str, arch: &str) -> Strin ) } -async fn fetch_live_channel_manifests(channel: &str) -> Result> { +async fn fetch_live_channel_feeds(channel: &str) -> Result> { let client = reqwest::Client::builder() .timeout(DESKTOP_LIVE_MANIFEST_TIMEOUT) .build() .context("Failed to build the HTTP client")?; - let mut manifests = BTreeMap::new(); + let mut feeds = BTreeMap::new(); for (platform, arch) in desktop_release_coordinates() { let url = desktop_live_manifest_url(channel, platform, arch); - let manifest: Value = client + let response = client .get(&url) .send() .await - .with_context(|| format!("Failed to fetch {url}"))? - .error_for_status() - .with_context(|| format!("{url} did not serve a module manifest"))? - .json() - .await - .with_context(|| format!("{url} is not JSON"))?; - ensure!( - manifest["release_channel"] == channel - && manifest["platform"] == platform - && manifest["arch"] == arch, - "{url} describes another channel or coordinate" - ); - manifests.insert(desktop_coordinate_key(platform, arch), manifest); + .with_context(|| format!("Failed to fetch {url}"))?; + let feed = if response.status() == reqwest::StatusCode::NOT_FOUND { + DesktopLiveFeed::Missing + } else { + DesktopLiveFeed::Served( + response + .error_for_status() + .with_context(|| format!("{url} did not serve a module manifest"))? + .json() + .await + .with_context(|| format!("{url} is not JSON"))?, + ) + }; + feeds.insert(desktop_coordinate_key(platform, arch), feed); } - Ok(manifests) + Ok(feeds) +} + +fn newest_release_with_manifest<'a>( + releases: &'a [DesktopPublishedRelease], + channel: &str, + platform: &str, + arch: &str, +) -> Result> { + for release in releases.iter().rev() { + let asset = + desktop_module_manifest_release_asset_name(channel, &release.version, platform, arch)?; + if release.assets.contains(&asset) { + return Ok(Some((release, asset))); + } + } + Ok(None) +} + +fn published_channel_manifest( + releases: &[DesktopPublishedRelease], + channel: &str, + platform: &str, + arch: &str, +) -> Result> { + let Some((release, asset)) = newest_release_with_manifest(releases, channel, platform, arch)? + else { + return Ok(None); + }; + let manifest: Value = + serde_json::from_slice(&download_desktop_release_asset(&release.tag, &asset)?) + .with_context(|| format!("{asset} from {} is not JSON", release.tag))?; + ensure!( + manifest["release_channel"] == channel + && manifest["platform"] == platform + && manifest["arch"] == arch + && manifest["build_version"] == release.version.as_str(), + "{asset} from {} describes another channel, coordinate or build", + release.tag + ); + Ok(Some(manifest)) +} + +fn check_live_feed( + releases: &[DesktopPublishedRelease], + channel: &str, + platform: &str, + arch: &str, + feed: DesktopLiveFeed, +) -> Result> { + let url = desktop_live_manifest_url(channel, platform, arch); + let newest = newest_release_with_manifest(releases, channel, platform, arch)? + .map(|(release, _)| release); + let manifest = match feed { + DesktopLiveFeed::Missing => { + if let Some(release) = newest { + bail!( + "{url} is missing although {} published it, the package mirror has not caught up with {}, re-run once it has", + release.tag, + release.tag + ); + } + return Ok(None); + } + DesktopLiveFeed::Served(manifest) => manifest, + }; + ensure!( + manifest["release_channel"] == channel + && manifest["platform"] == platform + && manifest["arch"] == arch, + "{url} describes another channel or coordinate" + ); + let release = newest.with_context(|| { + format!("{url} serves a module manifest that no published {channel} release carries") + })?; + let build_version = manifest["build_version"] + .as_str() + .with_context(|| format!("{url} names no build version"))?; + ensure!( + build_version == release.version, + "{url} serves {build_version}, the package mirror has not caught up with {}, re-run once it has", + release.tag + ); + Ok(Some(manifest)) } fn live_shell_version(manifest: &Value) -> Option<&str> { @@ -5603,41 +5808,6 @@ fn common_live_shell_version(manifests: &BTreeMap) -> Result Result> { - let package_path = repo_root.join("fluxer_desktop").join("package.json"); - let package: Value = serde_json::from_slice( - &fs::read(&package_path) - .with_context(|| format!("Failed to read {}", package_path.display()))?, - ) - .with_context(|| format!("Failed to parse {}", package_path.display()))?; - let workspace_dependencies = package["dependencies"] - .as_object() - .into_iter() - .flatten() - .filter(|(_, spec)| { - spec.as_str() - .is_some_and(|spec| spec.starts_with("workspace:")) - }) - .map(|(name, _)| name.clone()) - .collect::>(); - let mut roots = vec!["fluxer_desktop/".to_string()]; - for package_dir in sorted_child_directories(&repo_root.join("packages"))? { - let manifest_path = package_dir.join("package.json"); - if !manifest_path.is_file() { - continue; - } - let manifest: Value = serde_json::from_slice(&fs::read(&manifest_path)?) - .with_context(|| format!("Failed to parse {}", manifest_path.display()))?; - if manifest["name"] - .as_str() - .is_some_and(|name| workspace_dependencies.contains(name)) - { - roots.push(format!("packages/{}/", file_name_string(&package_dir)?)); - } - } - Ok(roots) -} - fn is_shell_irrelevant_path(path: &str) -> bool { path.contains(".test.") || path.contains("/__tests__/") @@ -5645,67 +5815,834 @@ fn is_shell_irrelevant_path(path: &str) -> bool { || path.ends_with(".md") } -fn desktop_shell_drift(changed: &[String], roots: &[String]) -> Vec { - changed - .iter() - .filter(|path| roots.iter().any(|root| path.starts_with(root.as_str()))) - .filter(|path| !is_shell_irrelevant_path(path)) - .cloned() - .collect() +fn is_import_identifier_byte(byte: u8) -> bool { + byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'$' | b'.') } -async fn check_shell_drift_step() -> Result<()> { - let channel = require_env("CHANNEL")?; - let source_sha = require_env("SOURCE_SHA")?; - let manifests = fetch_live_channel_manifests(&channel).await?; - let shell_version = common_live_shell_version(&manifests)?; - let tag = format!("fluxer-desktop-{channel}@{shell_version}"); - let shell_sha = output_text(CommandSpec::new("gh").args([ - "api", - &format!("repos/{RELEASE_REPOSITORY}/commits/{tag}"), - "--jq", - ".sha", - ])) - .with_context(|| format!("Failed to resolve the live shell release {tag}"))? - .trim() - .to_string(); +fn contract_import_specifiers(source: &str) -> Vec { + let bytes = source.as_bytes(); + let mut specifiers = Vec::new(); + let mut index = 0; + while index < bytes.len() { + let keyword = ["from", "import", "require"] + .into_iter() + .find(|keyword| bytes[index..].starts_with(keyword.as_bytes())); + let Some(keyword) = keyword else { + index += 1; + continue; + }; + let end = index + keyword.len(); + let bounded = (index == 0 || !is_import_identifier_byte(bytes[index - 1])) + && bytes + .get(end) + .is_none_or(|byte| !is_import_identifier_byte(*byte)); + if !bounded { + index = end; + continue; + } + let mut cursor = end; + while bytes.get(cursor).is_some_and(u8::is_ascii_whitespace) { + cursor += 1; + } + if keyword != "from" && bytes.get(cursor) == Some(&b'(') { + cursor += 1; + while bytes.get(cursor).is_some_and(u8::is_ascii_whitespace) { + cursor += 1; + } + } + if let Some("e) = bytes.get(cursor) + && matches!(quote, b'\'' | b'"' | b'`') + && let Some(length) = bytes[cursor + 1..].iter().position(|byte| *byte == quote) + { + specifiers.push(source[cursor + 1..cursor + 1 + length].to_string()); + cursor += length + 2; + } + index = cursor.max(end); + } + specifiers +} + +fn normalize_repo_path(path: &str) -> Option { + let mut parts: Vec<&str> = Vec::new(); + for part in path.split('/') { + match part { + "" | "." => {} + ".." => { + parts.pop()?; + } + other => parts.push(other), + } + } + Some(parts.join("/")) +} + +fn probe_contract_path( + read: &dyn Fn(&str) -> Result>, + base: &str, +) -> Result> { + for suffix in DESKTOP_CONTRACT_PROBE_SUFFIXES { + let candidate = format!("{base}{suffix}"); + if read(&candidate)?.is_some() { + return Ok(Some(candidate)); + } + } + Ok(None) +} + +fn package_export_target(manifest: &Value, subpath: &str) -> Option { + let key = if subpath.is_empty() { + ".".to_string() + } else { + format!("./{subpath}") + }; + let target = &manifest["exports"][&key]; + target.as_str().map(str::to_string).or_else(|| { + DESKTOP_CONTRACT_EXPORT_CONDITIONS + .iter() + .find_map(|condition| target[*condition].as_str().map(str::to_string)) + }) +} + +fn resolve_contract_specifier( + read: &dyn Fn(&str) -> Result>, + importer: &str, + specifier: &str, +) -> Result> { + if DESKTOP_CONTRACT_GENERATED_MODULES.contains(&specifier) { + return Ok(None); + } + let candidates = if let Some(rest) = specifier.strip_prefix(DESKTOP_CONTRACT_ELECTRON_ALIAS) { + vec![format!("{DESKTOP_CONTRACT_ELECTRON_ROOT}{rest}")] + } else if let Some(rest) = specifier.strip_prefix(DESKTOP_CONTRACT_PACKAGE_ALIAS) { + let (package, subpath) = rest.split_once('/').unwrap_or((rest, "")); + let package_root = format!("packages/{package}"); + let exported = read(&format!("{package_root}/package.json"))? + .map(|manifest| { + serde_json::from_str::(&manifest) + .with_context(|| format!("Failed to parse {package_root}/package.json")) + }) + .transpose()? + .and_then(|manifest| package_export_target(&manifest, subpath)); + match exported { + Some(target) => vec![format!( + "{package_root}/{}", + target.trim_start_matches("./") + )], + None if subpath.is_empty() => { + vec![package_root.clone(), format!("{package_root}/src/index")] + } + None => vec![format!("{package_root}/{subpath}")], + } + } else if specifier.starts_with("./") || specifier.starts_with("../") { + let directory = importer + .rsplit_once('/') + .map_or("", |(directory, _)| directory); + vec![format!("{directory}/{specifier}")] + } else { + return Ok(None); + }; + for candidate in candidates { + let Some(normalized) = normalize_repo_path(&candidate) else { + continue; + }; + if let Some(resolved) = probe_contract_path(read, &normalized)? { + return Ok(Some(resolved)); + } + } + bail!("{importer} imports {specifier}, which does not resolve to a file") +} + +fn is_contract_main_file(path: &str) -> bool { + path.strip_prefix(DESKTOP_CONTRACT_MAIN_DIR) + .is_some_and(|name| !name.contains('/') && name.ends_with(".ts")) + && !is_shell_irrelevant_path(path) +} + +fn desktop_shell_contract_surface( + read: &dyn Fn(&str) -> Result>, + main_files: &[String], +) -> Result> { + let mut surface = BTreeSet::new(); + let mut pending = DESKTOP_CONTRACT_ROOTS + .iter() + .map(|root| (*root).to_string()) + .collect::>(); + while let Some(path) = pending.pop() { + if is_shell_irrelevant_path(&path) || surface.contains(&path) { + continue; + } + let source = read(&path)? + .with_context(|| format!("The shell contract file {path} does not exist"))?; + surface.insert(path.clone()); + for specifier in contract_import_specifiers(&source) { + if let Some(resolved) = resolve_contract_specifier(read, &path, &specifier)? { + pending.push(resolved); + } + } + } + for path in main_files.iter().filter(|path| is_contract_main_file(path)) { + if path[DESKTOP_CONTRACT_MAIN_DIR.len()..] + .starts_with(DESKTOP_CONTRACT_MAIN_BOUNDARY_PREFIX) + { + surface.insert(path.clone()); + } + } + Ok(surface) +} + +fn call_arguments_end(source: &str, open: usize) -> usize { + let bytes = source.as_bytes(); + let mut depth = 0usize; + let mut index = open; + while index < bytes.len() { + match bytes[index] { + b'(' => depth += 1, + b')' => { + depth -= 1; + if depth == 0 { + return index + 1; + } + } + quote @ (b'\'' | b'"' | b'`') => { + index += 1; + while index < bytes.len() && bytes[index] != quote { + if bytes[index] == b'\\' { + index += 1; + } + index += 1; + } + } + b'/' if bytes.get(index + 1) == Some(&b'/') => { + while index < bytes.len() && bytes[index] != b'\n' { + index += 1; + } + } + b'/' if bytes.get(index + 1) == Some(&b'*') => { + index += 2; + while index + 1 < bytes.len() && !(bytes[index] == b'*' && bytes[index + 1] == b'/') + { + index += 1; + } + index += 1; + } + _ => {} + } + index += 1; + } + bytes.len() +} + +fn desktop_ipc_call_sites(source: &str) -> Vec { + let mut sites = DESKTOP_CONTRACT_IPC_CALLS + .iter() + .flat_map(|call| { + source.match_indices(call).map(|(start, call)| { + let end = call_arguments_end(source, start + call.len() - 1); + source[start..end] + .split_whitespace() + .collect::>() + .join(" ") + }) + }) + .collect::>(); + sites.sort(); + sites +} + +fn git_output(args: &[&str]) -> Result> { + let output = CommandSpec::new("git") + .args(args) + .to_command() + .output() + .with_context(|| format!("Failed to run git {}", args.join(" ")))?; ensure!( - shell_sha.len() == 40 && shell_sha.bytes().all(|byte| byte.is_ascii_hexdigit()), - "{tag} resolved to an invalid commit {shell_sha:?}" + output.status.success(), + "git {} failed: {}", + args.join(" "), + String::from_utf8_lossy(&output.stderr).trim() ); - let repo_root = env::current_dir().context("Failed to resolve the repository root")?; - let roots = desktop_shell_source_roots(&repo_root)?; - let changed = - output_text(CommandSpec::new("git").args(["diff", "--name-only", &shell_sha, &source_sha])) - .with_context(|| format!("Failed to diff {shell_sha} against {source_sha}"))? + Ok(output.stdout) +} + +fn git_text(args: &[&str]) -> Result { + String::from_utf8(git_output(args)?) + .map(|text| text.trim().to_string()) + .with_context(|| format!("git {} printed invalid UTF-8", args.join(" "))) +} + +fn git_is_ancestor(ancestor: &str, descendant: &str) -> Result { + let status = CommandSpec::new("git") + .args(["merge-base", "--is-ancestor", ancestor, descendant]) + .to_command() + .status() + .context("Failed to run git merge-base")?; + match status.code() { + Some(0) => Ok(true), + Some(1) => Ok(false), + _ => bail!("git merge-base --is-ancestor {ancestor} {descendant} failed"), + } +} + +fn changes_shell_contract( + path: &str, + base_surface: &BTreeSet, + head_surface: &BTreeSet, + base_read: &dyn Fn(&str) -> Result>, + head_read: &dyn Fn(&str) -> Result>, +) -> Result { + if base_surface.contains(path) || head_surface.contains(path) { + return Ok(true); + } + if !is_contract_main_file(path) { + return Ok(false); + } + let sites = |read: &dyn Fn(&str) -> Result>| -> Result> { + Ok(read(path)? + .as_deref() + .map(desktop_ipc_call_sites) + .unwrap_or_default()) + }; + Ok(sites(base_read)? != sites(head_read)?) +} + +fn read_worktree_file(root: &Path, path: &str) -> Result> { + let full = root.join(path); + if !full.is_file() { + return Ok(None); + } + fs::read_to_string(&full) + .map(Some) + .with_context(|| format!("Failed to read {}", full.display())) +} + +struct DesktopContractWorkspace { + root: PathBuf, + head: String, + head_surface: BTreeSet, + changes: BTreeMap>, +} + +impl DesktopContractWorkspace { + fn open(source_sha: &str) -> Result { + let head = git_text(&["rev-parse", "HEAD"])?; + ensure!( + head == source_sha, + "The checkout is at {head}, not at SOURCE_SHA {source_sha}" + ); + let root = env::current_dir().context("Failed to resolve the repository root")?; + let main_dir = root.join(DESKTOP_CONTRACT_MAIN_DIR); + let main_files = fs::read_dir(&main_dir) + .with_context(|| format!("Failed to read {}", main_dir.display()))? + .map(|entry| { + Ok(format!( + "{DESKTOP_CONTRACT_MAIN_DIR}{}", + file_name_string(&entry?.path())? + )) + }) + .collect::>>()?; + let read = |path: &str| read_worktree_file(&root, path); + let head_surface = desktop_shell_contract_surface(&read, &main_files)?; + Ok(Self { + root, + head, + head_surface, + changes: BTreeMap::new(), + }) + } + + fn shell_contract_changes(&mut self, base: &str) -> Result> { + if let Some(changes) = self.changes.get(base) { + return Ok(changes.clone()); + } + let files = String::from_utf8(git_output(&["ls-tree", "-r", "--name-only", base])?) + .context("git ls-tree printed invalid UTF-8")? .lines() .map(str::to_string) + .collect::>(); + let read = |path: &str| -> Result> { + if !files.contains(path) { + return Ok(None); + } + let bytes = git_output(&["show", &format!("{base}:{path}")])?; + String::from_utf8(bytes) + .map(Some) + .with_context(|| format!("{path} at {base} is not UTF-8")) + }; + let main_files = files + .iter() + .filter(|path| path.starts_with(DESKTOP_CONTRACT_MAIN_DIR)) + .cloned() .collect::>(); - let drift = desktop_shell_drift(&changed, &roots); - if !drift.is_empty() { - let listed = drift.join("\n "); - ensure!( - env_bool(DESKTOP_ALLOW_SHELL_DRIFT_ENV), - "The live {channel} shell {shell_version} was built from {shell_sha}, and these shell sources changed since then:\n {listed}\nShip a full desktop release, or set {DESKTOP_ALLOW_SHELL_DRIFT_ENV}=true if the renderer does not depend on these changes" - ); - println!( - "{DESKTOP_ALLOW_SHELL_DRIFT_ENV} is set, publishing modules although these shell sources changed since {shell_sha}:\n {listed}" + let base_surface = desktop_shell_contract_surface(&read, &main_files) + .with_context(|| format!("Failed to compute the shell contract at {base}"))?; + let head_read = |path: &str| read_worktree_file(&self.root, path); + let mut changes = Vec::new(); + for path in git_text(&["diff", "--name-only", base, &self.head])?.lines() { + if changes_shell_contract(path, &base_surface, &self.head_surface, &read, &head_read)? { + changes.push(path.to_string()); + } + } + self.changes.insert(base.to_string(), changes.clone()); + Ok(changes) + } +} + +struct DesktopShellHistory { + channel: String, + releases: Vec, + shas: BTreeMap, + module_era: BTreeMap, + full: BTreeMap, +} + +impl DesktopShellHistory { + fn new(channel: &str, releases: Vec) -> Self { + Self { + channel: channel.to_string(), + releases, + shas: BTreeMap::new(), + module_era: BTreeMap::new(), + full: BTreeMap::new(), + } + } + + fn sha(&mut self, index: usize) -> Result { + if let Some(sha) = self.shas.get(&index) { + return Ok(sha.clone()); + } + let tag = &self.releases[index].tag; + let sha = resolve_commit_sha(tag) + .with_context(|| format!("Failed to resolve the published release {tag}"))?; + self.shas.insert(index, sha.clone()); + Ok(sha) + } + + fn is_module_era(&mut self, index: usize) -> Result { + if let Some(module_era) = self.module_era.get(&index) { + return Ok(*module_era); + } + let module_era = git_is_ancestor(DESKTOP_FIRST_MODULE_SHELL_COMMIT, &self.sha(index)?)?; + self.module_era.insert(index, module_era); + Ok(module_era) + } + + fn is_full(&mut self, index: usize) -> Result { + if let Some(full) = self.full.get(&index) { + return Ok(*full); + } + let full = desktop_release_kind(&self.channel, &self.releases[index].version)?.is_full(); + self.full.insert(index, full); + Ok(full) + } + + fn index_of(&self, version: &str) -> Option { + self.releases + .iter() + .position(|release| release.version == version) + } + + fn shell(&mut self, index: usize) -> Result<(String, String)> { + Ok((self.releases[index].version.clone(), self.sha(index)?)) + } + + fn oldest_module_era_full_shell(&mut self) -> Result> { + let (mut low, mut high) = (0, self.releases.len()); + while low < high { + let middle = low + (high - low) / 2; + if self.is_module_era(middle)? { + high = middle; + } else { + low = middle + 1; + } + } + for index in low..self.releases.len() { + if self.is_full(index)? { + return self.shell(index).map(Some); + } + } + Ok(None) + } + + fn older_full_shell(&mut self, below: usize) -> Result> { + let mut index = below; + while index > 0 { + index -= 1; + if !self.is_module_era(index)? { + return Ok(None); + } + if self.is_full(index)? { + return Ok(Some(index)); + } + } + Ok(None) + } +} + +fn oldest_shell_carrying_contract( + start: (String, String), + mut next_older: impl FnMut() -> Result>, + mut carries: impl FnMut(&str) -> Result, +) -> Result<(String, String)> { + let mut floor = start; + while let Some(candidate) = next_older()? { + if !carries(&candidate.1)? { + break; + } + floor = candidate; + } + Ok(floor) +} + +fn walk_contract_floor( + history: &mut DesktopShellHistory, + workspace: &mut DesktopContractWorkspace, + start: (String, String), + start_index: usize, +) -> Result<(String, String)> { + let mut cursor = start_index; + oldest_shell_carrying_contract( + start, + || match history.older_full_shell(cursor)? { + Some(index) => { + cursor = index; + history.shell(index).map(Some) + } + None => Ok(None), + }, + |sha| Ok(workspace.shell_contract_changes(sha)?.is_empty()), + ) +} + +fn raised_shell_floor( + history: &mut DesktopShellHistory, + workspace: &mut DesktopContractWorkspace, + live: &BTreeMap>, + version: &str, + source_sha: &str, + modules_only: bool, +) -> Result<(String, String)> { + if !modules_only { + let start_index = history.releases.len(); + return walk_contract_floor( + history, + workspace, + (version.to_string(), source_sha.to_string()), + start_index, ); } - let live = DesktopLiveShell { - channel: channel.clone(), - shell_version: shell_version.clone(), - shell_sha: shell_sha.clone(), - manifests, + let manifests = live + .iter() + .filter_map(|(key, manifest)| Some((key.clone(), manifest.clone()?))) + .collect::>(); + let live_version = common_live_shell_version(&manifests)?; + let index = history.index_of(&live_version).with_context(|| { + format!("The live shell {live_version} is not a published release of this channel") + })?; + let live_shell = history.shell(index)?; + ensure_live_shell_carries_contract( + &live_shell, + &workspace.shell_contract_changes(&live_shell.1)?, + )?; + walk_contract_floor(history, workspace, live_shell, index) +} + +fn ensure_live_shell_carries_contract( + live_shell: &(String, String), + missing: &[String], +) -> Result<()> { + ensure!( + missing.is_empty(), + "No published shell carries these contract changes, ship a full desktop release. The live shell {} from {} lacks:\n {}", + live_shell.0, + live_shell.1, + missing.join("\n ") + ); + Ok(()) +} + +fn shell_floor_instant(version: &str) -> Result>> { + if version == DESKTOP_MODULE_MINIMUM_SHELL_VERSION { + return Ok(None); + } + crate::common::parse_version_instant(version) + .map(Some) + .with_context(|| format!("{version:?} is not a shell version")) +} + +fn is_full_commit_sha(value: &str) -> bool { + value.len() == 40 && value.bytes().all(|byte| byte.is_ascii_hexdigit()) +} + +fn baseline_shell_compatibility(source_sha: &str) -> DesktopShellCompatibility { + DesktopShellCompatibility { + minimum_shell_version: DESKTOP_MODULE_MINIMUM_SHELL_VERSION.to_string(), + source_sha: source_sha.to_string(), + previous_source_sha: source_sha.to_string(), + floor_source_sha: None, + decision: DesktopShellContractChoice::Baseline, + contract_base_sha: source_sha.to_string(), + contract_changes: Vec::new(), + } +} + +fn previous_shell_compatibility( + live: Option<&Value>, + baseline_sha: impl FnOnce() -> Result, +) -> Result { + match live + .map(|manifest| &manifest["shell_compatibility"]) + .filter(|record| !record.is_null()) + { + Some(record) => parse_shell_compatibility(record), + None => Ok(baseline_shell_compatibility(&baseline_sha()?)), + } +} + +fn parse_shell_compatibility(value: &Value) -> Result { + let record: DesktopShellCompatibility = + serde_json::from_value(value.clone()).context("The record does not parse")?; + shell_floor_instant(&record.minimum_shell_version)?; + for sha in [ + &record.source_sha, + &record.previous_source_sha, + &record.contract_base_sha, + ] + .into_iter() + .chain(record.floor_source_sha.as_ref()) + { + ensure!(is_full_commit_sha(sha), "{sha:?} is not a full commit SHA"); + } + Ok(record) +} + +fn shell_contract_base(previous: &DesktopShellCompatibility, baseline_sha: &str) -> String { + previous + .floor_source_sha + .clone() + .unwrap_or_else(|| baseline_sha.to_string()) +} + +fn decide_shell_compatibility( + previous: &DesktopShellCompatibility, + contract_base_sha: &str, + changes: &[String], + choice: DesktopShellContractChoice, + raised_to: Option<(&str, &str)>, + source_sha: &str, +) -> Result { + let mut minimum_shell_version = previous.minimum_shell_version.clone(); + let mut floor_source_sha = previous.floor_source_sha.clone(); + match choice { + DesktopShellContractChoice::Unchanged => ensure!( + changes.is_empty(), + "The shell contract changed since {contract_base_sha}, the source of the oldest shell these modules still reach:\n {}\nOlder shells may not run these modules, re-run with {DESKTOP_SHELL_CONTRACT_INPUT} set to compatible if the renderer still works on them, or raise to cut them off", + changes.join("\n ") + ), + DesktopShellContractChoice::Compatible => {} + DesktopShellContractChoice::Raise => { + ensure!( + !changes.is_empty(), + "Nothing in the shell contract changed since {contract_base_sha}, the source of the oldest shell these modules still reach, there is no reason to raise" + ); + let (version, sha) = + raised_to.context("Raising the floor needs the shell that carries the contract")?; + if shell_floor_instant(version)? > shell_floor_instant(&minimum_shell_version)? { + minimum_shell_version = version.to_string(); + floor_source_sha = Some(sha.to_string()); + } + } + DesktopShellContractChoice::Baseline => { + bail!("A release states its shell contract as unchanged, compatible or raise") + } + } + Ok(DesktopShellCompatibility { + minimum_shell_version, + source_sha: source_sha.to_string(), + previous_source_sha: previous.source_sha.clone(), + floor_source_sha, + decision: choice, + contract_base_sha: contract_base_sha.to_string(), + contract_changes: changes.to_vec(), + }) +} + +fn parse_strict_bool(name: &str, value: &str) -> Result { + match value { + "true" => Ok(true), + "false" => Ok(false), + other => bail!("{name} must be true or false, got {other:?}"), + } +} + +fn shell_contract_path() -> PathBuf { + desktop_modules_dir().join(DESKTOP_SHELL_CONTRACT_FILE) +} + +fn read_shell_contract(channel: &str) -> Result> { + let path = shell_contract_path(); + if !path.is_file() { + return Ok(None); + } + let contract: DesktopShellContract = serde_json::from_slice( + &fs::read(&path).with_context(|| format!("Failed to read {}", path.display()))?, + ) + .with_context(|| format!("Failed to parse {}", path.display()))?; + ensure!( + contract.channel == channel, + "{} describes the {} channel, not {channel}", + path.display(), + contract.channel + ); + if let Some(source_sha) = env_string("SOURCE_SHA") { + ensure!( + contract.source_sha == source_sha, + "{} was decided for {}, not {source_sha}", + path.display(), + contract.source_sha + ); + } + Ok(Some(contract)) +} + +fn channel_baseline_sha( + history: &mut DesktopShellHistory, + cache: &mut Option, + source_sha: &str, +) -> Result { + if let Some(sha) = cache { + return Ok(sha.clone()); + } + let sha = history + .oldest_module_era_full_shell()? + .map_or_else(|| source_sha.to_string(), |(_, sha)| sha); + *cache = Some(sha.clone()); + Ok(sha) +} + +async fn check_shell_contract_step() -> Result<()> { + let channel = require_env("CHANNEL")?; + let source_sha = validate_full_sha("SOURCE_SHA", &require_env("SOURCE_SHA")?)?; + let version = require_env("VERSION")?; + crate::common::parse_version_instant(&version)?; + let choice = + DesktopShellContractChoice::parse_input(&require_env(DESKTOP_SHELL_CONTRACT_ENV)?)?; + let modules_only = parse_strict_bool( + DESKTOP_MODULES_ONLY_RELEASE_ENV, + &require_env(DESKTOP_MODULES_ONLY_RELEASE_ENV)?, + )?; + let releases = desktop_releases(&channel)?; + let mut feeds = if modules_only { + fetch_live_channel_feeds(&channel).await? + } else { + BTreeMap::new() + }; + let mut live = BTreeMap::new(); + for (platform, arch) in desktop_release_coordinates() { + let key = desktop_coordinate_key(platform, arch); + let manifest = if modules_only { + let feed = feeds.remove(&key).unwrap_or(DesktopLiveFeed::Missing); + check_live_feed(&releases, &channel, platform, arch, feed)? + } else { + published_channel_manifest(&releases, &channel, platform, arch)? + }; + live.insert(key, manifest); + } + if modules_only { + for (key, manifest) in &live { + ensure!( + manifest.is_some(), + "No {channel} shell serves {key} yet, ship a full desktop release before a modules-only release" + ); + } + } + let mut history = DesktopShellHistory::new(&channel, releases); + let mut workspace = DesktopContractWorkspace::open(&source_sha)?; + let mut baseline: Option = None; + let mut planned = Vec::new(); + for (key, manifest) in &live { + let previous = previous_shell_compatibility(manifest.as_ref(), || { + channel_baseline_sha(&mut history, &mut baseline, &source_sha) + }) + .with_context(|| format!("Failed to establish the previous {key} shell floor"))?; + let base = shell_contract_base( + &previous, + &channel_baseline_sha(&mut history, &mut baseline, &source_sha)?, + ); + let changes = workspace.shell_contract_changes(&base)?; + planned.push((key.clone(), previous, base, changes)); + } + let raised_to = if choice == DesktopShellContractChoice::Raise + && planned.iter().any(|(_, _, _, changes)| !changes.is_empty()) + { + Some(raised_shell_floor( + &mut history, + &mut workspace, + &live, + &version, + &source_sha, + modules_only, + )?) + } else { + None + }; + let mut coordinates = BTreeMap::new(); + for (key, previous, base, changes) in planned { + let compatibility = decide_shell_compatibility( + &previous, + &base, + &changes, + choice, + raised_to + .as_ref() + .map(|(version, sha)| (version.as_str(), sha.as_str())), + &source_sha, + ) + .with_context(|| format!("The {key} feed cannot take this release"))?; + println!( + "{key}: {:?} with a floor of {} (floor commit {}), {} contract change(s) since {}", + compatibility.decision, + compatibility.minimum_shell_version, + compatibility.floor_source_sha.as_deref().unwrap_or("none"), + compatibility.contract_changes.len(), + compatibility.contract_base_sha + ); + for path in &compatibility.contract_changes { + println!(" {path}"); + } + coordinates.insert( + key.clone(), + DesktopShellContractCoordinate { + live: live.get(&key).cloned().flatten(), + compatibility, + }, + ); + } + let contract = DesktopShellContract { + channel, + source_sha, + choice, + coordinates, }; let modules_dir = desktop_modules_dir(); fs::create_dir_all(&modules_dir) .with_context(|| format!("Failed to create {}", modules_dir.display()))?; - write_json_pretty(&modules_dir.join(DESKTOP_LIVE_SHELL_FILE), &live)?; - println!( - "The live {channel} shell is {shell_version} from {shell_sha}, the modules from {source_sha} run on it" - ); - Ok(()) + write_json_pretty(&shell_contract_path(), &contract) +} + +fn inherited_shell_range( + live: Option<&Value>, + module: &str, + sha256: &str, +) -> Option<(String, Option)> { + let live = live.filter(|manifest| !manifest["shell_compatibility"].is_null())?; + let entry = &live["modules"][module]; + if entry["sha256"].as_str() != Some(sha256) { + return None; + } + Some(( + entry["minimum_shell_version"].as_str()?.to_string(), + entry["maximum_shell_version"].as_str().map(str::to_string), + )) } #[allow(clippy::too_many_arguments)] @@ -5718,21 +6655,15 @@ fn modules_only_channel_manifest( pub_date: &str, metadata_version: u64, packed: &[DesktopPackedModule], + compatibility: &DesktopShellCompatibility, ) -> Result { let mut manifest = live .as_object() .cloned() .with_context(|| format!("The live {platform} {arch} manifest is not an object"))?; - let shell_version = live_shell_version(live) - .with_context(|| format!("The live {platform} {arch} manifest names no shell version"))? - .to_string(); - let modules = desktop_channel_manifest_entries(channel, packed) - .into_iter() - .map(|(module, mut entry)| { - entry.minimum_shell_version = shell_version.clone(); - (module, entry) - }) - .collect::>(); + live_shell_version(live) + .with_context(|| format!("The live {platform} {arch} manifest names no shell version"))?; + let modules = desktop_channel_manifest_entries(channel, packed, compatibility, Some(live)); manifest.insert( "manifest_version".to_string(), json!(DESKTOP_CHANNEL_MANIFEST_VERSION), @@ -5748,6 +6679,11 @@ fn modules_only_channel_manifest( "required_modules".to_string(), json!(DESKTOP_REQUIRED_MODULES), ); + manifest.remove("linux_security_minimum"); + manifest.insert( + "shell_compatibility".to_string(), + serde_json::to_value(compatibility)?, + ); Ok(Value::Object(manifest)) } @@ -5756,21 +6692,12 @@ fn build_modules_only_manifest_step() -> Result<()> { let build_version = require_env("VERSION")?; let pub_date = require_env("PUB_DATE")?; let modules_dir = desktop_modules_dir(); - let live_path = modules_dir.join(DESKTOP_LIVE_SHELL_FILE); - let live: DesktopLiveShell = - serde_json::from_slice(&fs::read(&live_path).with_context(|| { - format!( - "Failed to read {}, run the check_shell_drift step first", - live_path.display() - ) - })?) - .with_context(|| format!("Failed to parse {}", live_path.display()))?; - ensure!( - live.channel == channel, - "{} describes the {} channel, not {channel}", - live_path.display(), - live.channel - ); + let contract = read_shell_contract(&channel)?.with_context(|| { + format!( + "Missing {}, run the check_shell_contract step first", + shell_contract_path().display() + ) + })?; let payload_root = Path::new("payload_tree").join(DESKTOP_PAYLOAD_PREFIX); remove_dir_if_exists(Path::new("payload_tree"))?; fs::create_dir_all(&payload_root) @@ -5780,10 +6707,13 @@ fn build_modules_only_manifest_step() -> Result<()> { stage_desktop_module_packages(&payload_root, &channel, &packed)?; for (platform, arch) in desktop_release_coordinates() { let key = desktop_coordinate_key(platform, arch); - let live_manifest = live - .manifests + let coordinate = contract + .coordinates .get(&key) - .with_context(|| format!("{} holds no live {key} manifest", live_path.display()))?; + .with_context(|| format!("The shell contract holds no {key} decision"))?; + let live_manifest = coordinate.live.as_ref().with_context(|| { + format!("No live {key} manifest, ship a full desktop release first") + })?; let manifest = modules_only_channel_manifest( live_manifest, &channel, @@ -5793,6 +6723,7 @@ fn build_modules_only_manifest_step() -> Result<()> { &pub_date, metadata_version, &packed, + &coordinate.compatibility, )?; let manifest_path = desktop_channel_manifest_path(&payload_root, &channel, platform, arch); if let Some(parent) = manifest_path.parent() { @@ -5806,14 +6737,13 @@ fn build_modules_only_manifest_step() -> Result<()> { serde_json::to_vec(&manifest)?, )?; println!( - "Wrote the {platform} {arch} modules-only manifest for shell {}", - live.shell_version + "Wrote the {platform} {arch} modules-only manifest for shells from {}", + coordinate.compatibility.minimum_shell_version ); } println!( - "Staged {} module(s) for {build_version} on the live {channel} shell {}", - packed.len(), - live.shell_version + "Staged {} module(s) for {build_version} on the {channel} channel", + packed.len() ); Ok(()) } @@ -5876,9 +6806,35 @@ fn build_summary_step() -> Result<()> { file, "\n**Version:** {version}\n\n**Download prefix:** {DESKTOP_PAYLOAD_PREFIX}/{channel}/\n\n**Redirect endpoint shape:** /dl/{DESKTOP_PAYLOAD_PREFIX}/{channel}/{{plat}}/{{arch}}/{{format}}" )?; + if let Some(contract) = read_shell_contract(&channel)? { + write!(file, "{}", shell_contract_summary(&contract))?; + } Ok(()) } +fn shell_contract_summary(contract: &DesktopShellContract) -> String { + let mut summary = format!( + "\n### Shell contract\n\n**Decision:** {:?}\n\n| Feed | Oldest shell | Floor commit | Contract changes |\n| --- | --- | --- | --- |\n", + contract.choice + ); + let mut changes = BTreeSet::new(); + for (key, coordinate) in &contract.coordinates { + let compatibility = &coordinate.compatibility; + summary.push_str(&format!( + "| {key} | {} | {} | {} |\n", + compatibility.minimum_shell_version, + compatibility.floor_source_sha.as_deref().unwrap_or("none"), + compatibility.contract_changes.len() + )); + changes.extend(compatibility.contract_changes.iter().cloned()); + } + for path in changes { + summary.push_str(&format!("\n- `{path}`")); + } + summary.push('\n'); + summary +} + fn find_dist_file(dist: &Path, predicate: F) -> Option where F: Fn(&str) -> bool, @@ -6073,93 +7029,702 @@ mod tests { assert!(common_live_shell_version(&manifests).is_err()); } - #[test] - fn shell_drift_counts_only_shell_sources() { - let roots = vec![ - "fluxer_desktop/".to_string(), - "packages/desktop_ipc/".to_string(), - ]; - let changed = [ - "fluxer_app/src/App.tsx", - "fluxer_desktop/src/main/Updater.ts", - "fluxer_desktop/src/main/Updater.test.mjs", - "packages/desktop_ipc/src/Channels.ts", - "packages/desktop_ipc/README.md", - "packages/schema/src/Thing.ts", + const SHA_LIVE: &str = "1111111111111111111111111111111111111111"; + const SHA_SOURCE: &str = "2222222222222222222222222222222222222222"; + const SHA_BASELINE: &str = "3333333333333333333333333333333333333333"; + + fn contract_reader(files: &[(&str, &str)]) -> impl Fn(&str) -> Result> { + let files = files + .iter() + .map(|(path, contents)| ((*path).to_string(), (*contents).to_string())) + .collect::>(); + move |path: &str| Ok(files.get(path).cloned()) + } + + fn contract_fixture() -> Vec<(&'static str, &'static str)> { + vec![ + ( + "fluxer_desktop/src/preload/index.ts", + "import {isRecord} from '@fluxer/record_utils/src/RecordGuards';\nimport {BUILD_CHANNEL} from '@electron/common/BuildChannel';\nimport type {DesktopApi} from '@electron/common/Types';\nimport {bridge} from '@fluxer/voice_engine_v2/bridge';\nexport {helper} from './Helper';\nimport {contextBridge} from 'electron';\nconst later = import.meta.url;\nconst lazy = () => import('@electron/preload/Lazy');\n", + ), + ( + "fluxer_desktop/src/preload/Helper.ts", + "export const helper = 1;\n", + ), + ( + "fluxer_desktop/src/preload/Lazy.ts", + "export const lazy = 1;\n", + ), + ( + "fluxer_desktop/src/preload/splash.cjs", + "require('electron');\n", + ), + ( + "fluxer_desktop/src/common/Types.ts", + "export type * from '@fluxer/desktop_ipc/src/DesktopBridgeTypes';\n", + ), + ( + "packages/desktop_ipc/src/DesktopBridgeTypes.ts", + "export interface A {}\n", + ), + ( + "packages/desktop_ipc/src/ModuleContract.ts", + "export interface B {}\n", + ), + ( + "packages/record_utils/src/RecordGuards.ts", + "export const isRecord = 1;\n", + ), + ( + "packages/voice_engine_v2/package.json", + r#"{"name":"@fluxer/voice_engine_v2","exports":{"./bridge":"./src/bridge/index.ts"}}"#, + ), + ( + "packages/voice_engine_v2/src/bridge/index.ts", + "import type {Frame} from '@fluxer/voice_engine_v2/src/protocol/types';\n", + ), + ( + "packages/voice_engine_v2/src/protocol/types.ts", + "export interface Frame {}\n", + ), + ( + "fluxer_app/src/types/electron.d.ts", + "import type {B} from '@fluxer/desktop_ipc/src/ModuleContract';\nimport type {C} from '@simplewebauthn/browser';\nimport type {D} from '@app/features/Thing';\n", + ), + ( + "fluxer_desktop/src/main/LocalAppProtocol.ts", + "export const protocol = 1;\n", + ), + ( + "fluxer_desktop/src/main/Updater.ts", + "ipcMain.handle('updater-check', async () => {});\n", + ), + ( + "fluxer_desktop/src/main/Window.ts", + "export const window = 1;\n", + ), + ( + "fluxer_desktop/src/main/Ipc.test.ts", + "ipcMain.on('test', () => {});\n", + ), + ("fluxer_desktop/src/main/README.md", "ipcMain.on(\n"), ] - .map(str::to_string); + } + + fn contract_main_files(files: &[(&str, &str)]) -> Vec { + files + .iter() + .map(|(path, _)| (*path).to_string()) + .filter(|path| path.starts_with(DESKTOP_CONTRACT_MAIN_DIR)) + .collect() + } + + #[test] + fn the_contract_surface_follows_preload_imports() { + let files = contract_fixture(); + let surface = + desktop_shell_contract_surface(&contract_reader(&files), &contract_main_files(&files)) + .unwrap(); assert_eq!( - desktop_shell_drift(&changed, &roots), + surface.into_iter().collect::>(), vec![ - "fluxer_desktop/src/main/Updater.ts".to_string(), - "packages/desktop_ipc/src/Channels.ts".to_string(), + "fluxer_app/src/types/electron.d.ts", + "fluxer_desktop/src/common/Types.ts", + "fluxer_desktop/src/main/LocalAppProtocol.ts", + "fluxer_desktop/src/preload/Helper.ts", + "fluxer_desktop/src/preload/Lazy.ts", + "fluxer_desktop/src/preload/index.ts", + "packages/desktop_ipc/src/DesktopBridgeTypes.ts", + "packages/desktop_ipc/src/ModuleContract.ts", + "packages/record_utils/src/RecordGuards.ts", + "packages/voice_engine_v2/src/bridge/index.ts", + "packages/voice_engine_v2/src/protocol/types.ts", ] ); } #[test] - fn shell_source_roots_follow_the_desktop_workspace_dependencies() { - let repo = tempfile::tempdir().unwrap(); - let root = repo.path(); - fs::create_dir_all(root.join("fluxer_desktop")).unwrap(); - fs::write( - root.join("fluxer_desktop/package.json"), - r#"{"dependencies":{"@fluxer/desktop_ipc":"workspace:*","electron-log":"5.0.0"}}"#, - ) - .unwrap(); - for (dir, name) in [ - ("desktop_ipc", "@fluxer/desktop_ipc"), - ("schema", "@fluxer/schema"), - ] { - fs::create_dir_all(root.join("packages").join(dir)).unwrap(); - fs::write( - root.join("packages").join(dir).join("package.json"), - format!(r#"{{"name":"{name}"}}"#), + fn main_process_files_count_only_where_their_ipc_calls_change() { + let surface = BTreeSet::from(["fluxer_desktop/src/main/LocalAppProtocol.ts".to_string()]); + let updater = "fluxer_desktop/src/main/Updater.ts"; + let base = "ipcMain.handle('updater-check', async (_event, context) => {\n\treturn check(context);\n});\nfunction poll() {\n\treturn 1;\n}\n"; + let changed = |base: &str, head: &str, path: &str| { + let base = base.to_string(); + let head = head.to_string(); + changes_shell_contract( + path, + &surface, + &surface, + &move |_: &str| Ok(Some(base.clone())), + &move |_: &str| Ok(Some(head.clone())), ) - .unwrap(); - } + .unwrap() + }; + assert!(!changed( + base, + &base.replace("return 1", "return 2"), + updater + )); + assert!(!changed( + base, + &base.replace( + "async (_event, context) => {", + "async (_event, context) => {" + ), + updater + )); + assert!(changed( + base, + &base.replace("check(context)", "check({manual: true})"), + updater + )); + assert!(changed( + base, + &base.replace("'updater-check'", "'updater-probe'"), + updater + )); + let tray = "fluxer_desktop/src/main/DesktopTray.ts"; + let send = + "client.webContents.send('tray-action', {id: 'quit', label: ')'});\nconst other = 1;\n"; + assert!(!changed( + send, + &send.replace("other = 1", "other = 2"), + tray + )); + assert!(changed( + send, + &send.replace("id: 'quit'", "action: 'quit'"), + tray + )); + assert!(changed(send, "", tray)); + assert!(changed( + "a", + "b", + "fluxer_desktop/src/main/LocalAppProtocol.ts" + )); + assert!(!changed( + send, + "", + "fluxer_desktop/src/main/fixtures/Tray.ts" + )); + } + + #[test] + fn ipc_call_sites_end_at_the_matching_parenthesis() { assert_eq!( - desktop_shell_source_roots(root).unwrap(), + desktop_ipc_call_sites( + "ipcMain.on('a', () => fn(')', \"(\", `)`)); // )\nother();\nsender.send(/* ) */ 'b'\n);\n" + ), vec![ - "fluxer_desktop/".to_string(), - "packages/desktop_ipc/".to_string() + ".send(/* ) */ 'b' )".to_string(), + "ipcMain.on('a', () => fn(')', \"(\", `)`))".to_string(), ] ); } #[test] - fn modules_only_manifest_keeps_the_live_shell_and_pins_modules_to_it() { - let live = live_manifest("darwin", "arm64", "2026.1008.32323"); - let packed = vec![DesktopPackedModule { - module: "fluxer_renderer".to_string(), - sha256: "a".repeat(64), + fn an_alias_import_that_does_not_resolve_is_an_error() { + let files = contract_fixture() + .into_iter() + .map(|(path, contents)| { + if path == "fluxer_desktop/src/preload/Helper.ts" { + (path, "import {gone} from '@electron/common/Gone';\n") + } else { + (path, contents) + } + }) + .collect::>(); + let error = + desktop_shell_contract_surface(&contract_reader(&files), &contract_main_files(&files)) + .unwrap_err() + .to_string(); + assert!(error.contains("@electron/common/Gone"), "{error}"); + } + + #[test] + fn the_import_scan_reads_every_specifier_form() { + assert_eq!( + contract_import_specifiers( + "import a from 'a';\nimport type {B} from \"b\";\nexport * from 'c';\nexport {d} from './d';\nconst e = import('e');\nconst f = require(\n 'f'\n);\nimport 'g';\nconst h = Array.from(list);\nconst i = import.meta.url;\nconst fromage = 'cheese';\n" + ), + vec!["a", "b", "c", "./d", "e", "f", "g"] + ); + } + + #[test] + fn the_real_preload_closure_resolves() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let main_dir = root.join(DESKTOP_CONTRACT_MAIN_DIR); + let main_files = fs::read_dir(&main_dir) + .unwrap() + .map(|entry| { + format!( + "{DESKTOP_CONTRACT_MAIN_DIR}{}", + file_name_string(&entry.unwrap().path()).unwrap() + ) + }) + .collect::>(); + let read = |path: &str| read_worktree_file(&root, path); + let surface = desktop_shell_contract_surface(&read, &main_files).unwrap(); + for path in [ + "fluxer_desktop/src/preload/index.ts", + "fluxer_desktop/src/preload/LocalAppPreload.ts", + "fluxer_desktop/src/common/Types.ts", + "packages/desktop_ipc/src/ModuleContract.ts", + "packages/voice_engine_v2/src/bridge/index.ts", + "packages/voice_engine_v2/src/protocol/types.ts", + "fluxer_app/src/types/electron.d.ts", + ] { + assert!( + surface.contains(path), + "{path} is missing from the contract" + ); + } + for path in [ + "fluxer_desktop/src/preload/splash.cjs", + "fluxer_desktop/src/preload/LegacyHarvestPreload.ts", + ] { + assert!( + !surface.contains(path), + "{path} must not be in the contract" + ); + } + assert!(surface.iter().all(|path| !is_shell_irrelevant_path(path))); + } + + fn compatibility(floor: &str, source_sha: &str) -> DesktopShellCompatibility { + DesktopShellCompatibility { + minimum_shell_version: floor.to_string(), + source_sha: source_sha.to_string(), + previous_source_sha: source_sha.to_string(), + floor_source_sha: None, + decision: DesktopShellContractChoice::Compatible, + contract_base_sha: SHA_BASELINE.to_string(), + contract_changes: Vec::new(), + } + } + + fn packed_module(module: &str, sha256: &str) -> DesktopPackedModule { + DesktopPackedModule { + module: module.to_string(), + sha256: sha256.to_string(), bytes: 42, directory: PathBuf::from("unused"), - }]; + } + } + + #[test] + fn an_unchanged_contract_keeps_the_floor_below_the_live_shell() { + let previous = baseline_shell_compatibility(SHA_BASELINE); + let decided = decide_shell_compatibility( + &previous, + SHA_BASELINE, + &[], + DesktopShellContractChoice::Unchanged, + None, + SHA_SOURCE, + ) + .unwrap(); + assert_eq!(decided.minimum_shell_version, "0.0.0"); + assert_eq!(decided.decision, DesktopShellContractChoice::Unchanged); + assert_eq!(decided.source_sha, SHA_SOURCE); + assert_eq!(decided.previous_source_sha, SHA_BASELINE); + let live = live_manifest("linux", "x64", "2026.1009.45608"); let manifest = modules_only_channel_manifest( &live, "canary", - "darwin", - "arm64", - "2026.1008.90000", - "2026-10-08T09:00:00Z", - 1791450000, - &packed, + "linux", + "x64", + "2026.1010.1", + "2026-10-10T00:00:01Z", + 1791590401, + &[packed_module("fluxer_renderer", &"a".repeat(64))], + &decided, ) .unwrap(); - assert_eq!(manifest["shell"], live["shell"]); assert_eq!( - manifest["linux_security_minimum"], - live["linux_security_minimum"] + manifest["modules"]["fluxer_renderer"]["minimum_shell_version"], "0.0.0", + "a modules-only release must not cut off the shell 2026.1008.32323 the AUR package ships" ); - assert_eq!(manifest["build_version"], "2026.1008.90000"); - assert_eq!(manifest["metadata_version"], 1791450000); - let renderer = &manifest["modules"]["fluxer_renderer"]; - assert_eq!(renderer["sha256"], "a".repeat(64)); - assert_eq!(renderer["minimum_shell_version"], "2026.1008.32323"); - assert!(renderer["maximum_shell_version"].is_null()); - let parsed: DesktopChannelManifest = serde_json::from_value(manifest).unwrap(); - assert_eq!(parsed.shell.latest_version, "2026.1008.32323"); + } + + #[test] + fn a_contract_change_marked_unchanged_refuses_the_release() { + let changes = vec!["fluxer_desktop/src/preload/index.ts".to_string()]; + let error = decide_shell_compatibility( + &baseline_shell_compatibility(SHA_BASELINE), + SHA_BASELINE, + &changes, + DesktopShellContractChoice::Unchanged, + None, + SHA_SOURCE, + ) + .unwrap_err() + .to_string(); + assert!(error.contains("shell_contract"), "{error}"); + assert!( + error.contains("fluxer_desktop/src/preload/index.ts"), + "{error}" + ); + assert!(error.contains(SHA_BASELINE), "{error}"); + } + + #[test] + fn a_compatible_contract_change_keeps_the_floor_and_records_the_paths() { + let changes = vec!["fluxer_desktop/src/main/Updater.ts".to_string()]; + let mut previous = compatibility("2026.1009.1", SHA_LIVE); + previous.floor_source_sha = Some(SHA_BASELINE.to_string()); + let decided = decide_shell_compatibility( + &previous, + SHA_BASELINE, + &changes, + DesktopShellContractChoice::Compatible, + None, + SHA_SOURCE, + ) + .unwrap(); + assert_eq!(decided.minimum_shell_version, "2026.1009.1"); + assert_eq!(decided.floor_source_sha.as_deref(), Some(SHA_BASELINE)); + assert_eq!(decided.contract_changes, changes); + assert_eq!(decided.previous_source_sha, SHA_LIVE); + } + + #[test] + fn every_release_states_changes_since_the_oldest_shell_the_floor_admits() { + let changes = vec!["fluxer_desktop/src/preload/index.ts".to_string()]; + let acknowledged = decide_shell_compatibility( + &baseline_shell_compatibility(SHA_BASELINE), + SHA_BASELINE, + &changes, + DesktopShellContractChoice::Compatible, + None, + SHA_LIVE, + ) + .unwrap(); + let base = shell_contract_base(&acknowledged, SHA_BASELINE); + assert_eq!(base, SHA_BASELINE); + let error = decide_shell_compatibility( + &acknowledged, + &base, + &changes, + DesktopShellContractChoice::Unchanged, + None, + SHA_SOURCE, + ) + .unwrap_err() + .to_string(); + assert!(error.contains(SHA_BASELINE), "{error}"); + let raised = decide_shell_compatibility( + &acknowledged, + &base, + &changes, + DesktopShellContractChoice::Raise, + Some(("2026.1013.1", SHA_LIVE)), + SHA_SOURCE, + ) + .unwrap(); + assert_eq!(raised.minimum_shell_version, "2026.1013.1"); + assert_eq!(raised.contract_base_sha, SHA_BASELINE); + assert_eq!(shell_contract_base(&raised, SHA_BASELINE), SHA_LIVE); + } + + #[test] + fn a_raise_to_a_malformed_shell_version_is_an_error() { + let error = decide_shell_compatibility( + &compatibility("2026.1009.1", SHA_LIVE), + SHA_BASELINE, + &["fluxer_desktop/src/preload/index.ts".to_string()], + DesktopShellContractChoice::Raise, + Some(("2026.10x9.1", SHA_SOURCE)), + SHA_SOURCE, + ) + .unwrap_err(); + assert!( + format!("{error:#}").contains("is not a shell version"), + "{error:#}" + ); + } + + #[test] + fn raise_without_a_contract_change_is_refused() { + let error = decide_shell_compatibility( + &baseline_shell_compatibility(SHA_BASELINE), + SHA_BASELINE, + &[], + DesktopShellContractChoice::Raise, + Some(("2026.1013.1", SHA_SOURCE)), + SHA_SOURCE, + ) + .unwrap_err() + .to_string(); + assert!(error.contains("no reason to raise"), "{error}"); + } + + #[test] + fn raise_moves_the_floor_to_the_oldest_shell_carrying_the_contract_and_never_lowers_it() { + let mut older = vec![ + ("2026.1012.1".to_string(), "a".repeat(40)), + ("2026.1011.1".to_string(), "b".repeat(40)), + ("2026.1010.1".to_string(), "c".repeat(40)), + ("2026.1009.1".to_string(), "d".repeat(40)), + ] + .into_iter(); + let mut checked = Vec::new(); + let floor = oldest_shell_carrying_contract( + ("2026.1013.1".to_string(), SHA_SOURCE.to_string()), + || Ok(older.next()), + |sha| { + checked.push(sha.to_string()); + Ok(sha != "c".repeat(40)) + }, + ) + .unwrap(); + assert_eq!(floor, ("2026.1011.1".to_string(), "b".repeat(40))); + assert_eq!( + checked, + vec!["a".repeat(40), "b".repeat(40), "c".repeat(40)] + ); + let changes = vec!["fluxer_desktop/src/preload/index.ts".to_string()]; + let raised = decide_shell_compatibility( + &baseline_shell_compatibility(SHA_BASELINE), + SHA_BASELINE, + &changes, + DesktopShellContractChoice::Raise, + Some((&floor.0, &floor.1)), + SHA_SOURCE, + ) + .unwrap(); + assert_eq!(raised.minimum_shell_version, "2026.1011.1"); + assert_eq!(raised.floor_source_sha, Some("b".repeat(40))); + let mut higher = compatibility("2026.1012.5", SHA_LIVE); + higher.floor_source_sha = Some(SHA_LIVE.to_string()); + let kept = decide_shell_compatibility( + &higher, + SHA_LIVE, + &changes, + DesktopShellContractChoice::Raise, + Some((&floor.0, &floor.1)), + SHA_SOURCE, + ) + .unwrap(); + assert_eq!(kept.minimum_shell_version, "2026.1012.5"); + assert_eq!(kept.floor_source_sha.as_deref(), Some(SHA_LIVE)); + } + + #[test] + fn a_feed_without_a_record_diffs_from_the_first_module_era_shell() { + let live = live_manifest("linux", "x64", "2026.1009.45608"); + let mut asked = 0; + let previous = previous_shell_compatibility(Some(&live), || { + asked += 1; + Ok(SHA_BASELINE.to_string()) + }) + .unwrap(); + assert_eq!(asked, 1); + assert_eq!(previous, baseline_shell_compatibility(SHA_BASELINE)); + assert_eq!( + previous_shell_compatibility(None, || Ok(SHA_BASELINE.to_string())).unwrap(), + baseline_shell_compatibility(SHA_BASELINE) + ); + let mut recorded = live.clone(); + recorded["shell_compatibility"] = + serde_json::to_value(compatibility("2026.1009.1", SHA_LIVE)).unwrap(); + assert_eq!( + previous_shell_compatibility(Some(&recorded), || bail!("the baseline is not needed")) + .unwrap(), + compatibility("2026.1009.1", SHA_LIVE) + ); + recorded["shell_compatibility"]["source_sha"] = json!("main"); + assert!( + previous_shell_compatibility(Some(&recorded), || Ok(SHA_BASELINE.to_string())).is_err() + ); + recorded["shell_compatibility"] = json!({"minimum_shell_version": "0.0.0"}); + assert!( + previous_shell_compatibility(Some(&recorded), || Ok(SHA_BASELINE.to_string())).is_err() + ); + } + + #[test] + fn a_modules_only_raise_refuses_changes_no_live_shell_carries() { + let live_shell = ("2026.1013.15209".to_string(), SHA_LIVE.to_string()); + ensure_live_shell_carries_contract(&live_shell, &[]).unwrap(); + let error = ensure_live_shell_carries_contract( + &live_shell, + &["packages/desktop_ipc/src/ModuleContract.ts".to_string()], + ) + .unwrap_err() + .to_string(); + assert!(error.contains("ship a full desktop release"), "{error}"); + assert!( + error.contains("packages/desktop_ipc/src/ModuleContract.ts"), + "{error}" + ); + } + + #[test] + fn the_shell_contract_input_accepts_only_known_values() { + assert_eq!( + DesktopShellContractChoice::parse_input("unchanged").unwrap(), + DesktopShellContractChoice::Unchanged + ); + assert_eq!( + DesktopShellContractChoice::parse_input("compatible").unwrap(), + DesktopShellContractChoice::Compatible + ); + assert_eq!( + DesktopShellContractChoice::parse_input("raise").unwrap(), + DesktopShellContractChoice::Raise + ); + for value in ["", "baseline", "Raise", "true", " compatible"] { + assert!( + DesktopShellContractChoice::parse_input(value).is_err(), + "{value:?}" + ); + } + assert!(parse_strict_bool("X", "true").unwrap()); + assert!(!parse_strict_bool("X", "false").unwrap()); + for value in ["", "1", "yes", "True"] { + assert!(parse_strict_bool("X", value).is_err(), "{value:?}"); + } + } + + fn published(version: &str, coordinates: &[(&str, &str)]) -> DesktopPublishedRelease { + DesktopPublishedRelease { + version: version.to_string(), + tag: format!("fluxer-desktop-canary@{version}"), + assets: coordinates + .iter() + .map(|(platform, arch)| { + desktop_module_manifest_release_asset_name("canary", version, platform, arch) + .unwrap() + }) + .collect(), + } + } + + #[test] + fn a_live_feed_must_match_the_newest_release_that_published_it() { + let releases = vec![ + published("2026.1009.45608", &[("linux", "x64")]), + published("2026.1010.1", &[("linux", "x64")]), + published("2026.1011.1", &[]), + ]; + assert_eq!( + check_live_feed( + &releases, + "canary", + "linux", + "arm64", + DesktopLiveFeed::Missing + ) + .unwrap(), + None + ); + let error = check_live_feed( + &releases, + "canary", + "linux", + "x64", + DesktopLiveFeed::Missing, + ) + .unwrap_err() + .to_string(); + assert!(error.contains("2026.1010.1"), "{error}"); + let lagging = live_manifest("linux", "x64", "2026.1009.45608"); + let error = check_live_feed( + &releases, + "canary", + "linux", + "x64", + DesktopLiveFeed::Served(lagging), + ) + .unwrap_err() + .to_string(); + assert!(error.contains("has not caught up"), "{error}"); + let current = live_manifest("linux", "x64", "2026.1010.1"); + assert_eq!( + check_live_feed( + &releases, + "canary", + "linux", + "x64", + DesktopLiveFeed::Served(current.clone()) + ) + .unwrap(), + Some(current.clone()) + ); + assert!( + check_live_feed( + &releases, + "canary", + "linux", + "arm64", + DesktopLiveFeed::Served(current) + ) + .is_err() + ); + } + + #[test] + fn modules_only_manifest_inherits_the_floor() { + let live = live_manifest("linux", "x64", "2026.1009.45608"); + let renderer = "a".repeat(64); + let dictionary = "b".repeat(64); + let packed = vec![ + packed_module("fluxer_renderer", &renderer), + packed_module("fluxer_dict_en", &dictionary), + ]; + let decided = compatibility("2026.1009.1", SHA_SOURCE); + let mut recorded = live.clone(); + recorded["modules"] = json!({ + "fluxer_dict_en": {"sha256": dictionary, "bytes": 42, "url": "https://pkgs.invalid/x", "minimum_shell_version": "2026.1001.1", "maximum_shell_version": "2026.1100.1"}, + "fluxer_renderer": {"sha256": "c".repeat(64), "bytes": 42, "url": "https://pkgs.invalid/y", "minimum_shell_version": "2026.1001.1", "maximum_shell_version": null}, + }); + let mut unrecorded = recorded.clone(); + recorded["shell_compatibility"] = + serde_json::to_value(compatibility("0.0.0", SHA_LIVE)).unwrap(); + unrecorded["modules"]["fluxer_dict_en"]["minimum_shell_version"] = json!("2026.1009.45608"); + for (live, dictionary_floor, dictionary_ceiling) in [ + (&recorded, "2026.1001.1", json!("2026.1100.1")), + (&unrecorded, "2026.1009.1", Value::Null), + ] { + let manifest = modules_only_channel_manifest( + live, + "canary", + "linux", + "x64", + "2026.1010.1", + "2026-10-10T00:00:01Z", + 1791590401, + &packed, + &decided, + ) + .unwrap(); + assert_eq!(manifest["shell"], live["shell"]); + assert_eq!(manifest["build_version"], "2026.1010.1"); + assert_eq!(manifest["metadata_version"], 1791590401); + assert!(manifest.get("linux_security_minimum").is_none()); + assert_eq!( + manifest["shell_compatibility"], + serde_json::to_value(&decided).unwrap() + ); + assert_eq!( + manifest["modules"]["fluxer_renderer"]["minimum_shell_version"], + "2026.1009.1" + ); + assert!(manifest["modules"]["fluxer_renderer"]["maximum_shell_version"].is_null()); + assert_eq!( + manifest["modules"]["fluxer_dict_en"]["minimum_shell_version"], + dictionary_floor + ); + assert_eq!( + manifest["modules"]["fluxer_dict_en"]["maximum_shell_version"], + dictionary_ceiling + ); + let parsed: DesktopChannelManifest = serde_json::from_value(manifest).unwrap(); + assert_eq!(parsed.shell.latest_version, "2026.1009.45608"); + assert_eq!(parsed.shell_compatibility, Some(decided.clone())); + } } fn write_file(path: &Path, contents: &str) { @@ -6171,14 +7736,20 @@ mod tests { const BUILD_DESKTOP_WORKFLOW: &str = include_str!("../../../.github/workflows/build-desktop.yaml"); + const BUILD_DESKTOP_MODULES_WORKFLOW: &str = + include_str!("../../../.github/workflows/build-desktop-modules.yaml"); fn workflow_job(job: &str) -> &'static str { + workflow_job_in(BUILD_DESKTOP_WORKFLOW, job) + } + + fn workflow_job_in(workflow: &'static str, job: &str) -> &'static str { let header = format!("\n {job}:\n"); - let start = BUILD_DESKTOP_WORKFLOW + let start = workflow .find(&header) - .unwrap_or_else(|| panic!("build-desktop.yaml has no {job} job")) + .unwrap_or_else(|| panic!("the workflow has no {job} job")) + header.len(); - let body = &BUILD_DESKTOP_WORKFLOW[start..]; + let body = &workflow[start..]; let end = body .match_indices('\n') .map(|(index, _)| index + 1) @@ -6232,6 +7803,83 @@ mod tests { } } + fn workflow_step_index(body: &str, step: &str) -> usize { + body.find(&format!("--step {step}\n")) + .unwrap_or_else(|| panic!("no {step} step in {body}")) + } + + #[test] + fn shell_contract_reaches_the_contract_check_in_both_workflows() { + for workflow in [BUILD_DESKTOP_WORKFLOW, BUILD_DESKTOP_MODULES_WORKFLOW] { + let input = workflow + .split("\n shell_contract:\n") + .nth(1) + .expect("both workflows take a shell_contract input"); + let input = &input[..input.find("\npermissions:").unwrap_or(input.len())]; + assert!(input.contains("type: choice"), "{input}"); + assert!(input.contains("required: true"), "{input}"); + assert!(input.contains("default: unchanged"), "{input}"); + for option in ["- unchanged", "- compatible", "- raise"] { + assert!(input.contains(option), "{input}"); + } + } + for (body, modules_only) in [ + (workflow_job("meta"), "false"), + (workflow_job("upload"), "false"), + ( + workflow_job_in(BUILD_DESKTOP_MODULES_WORKFLOW, "assemble"), + "true", + ), + ] { + let check = workflow_step_index(body, "check_shell_contract"); + assert!(body.contains("fetch-depth: 0"), "{body}"); + assert!( + body.contains("SHELL_CONTRACT: ${{ inputs.shell_contract }}"), + "{body}" + ); + assert!( + body.contains(&format!("DESKTOP_MODULES_ONLY_RELEASE: {modules_only}")), + "{body}" + ); + assert!( + body[..check].contains("GH_TOKEN: ${{ steps.create-token.outputs.token }}"), + "{body}" + ); + assert!(body[..check].contains("actions/create-github-app-token@")); + } + let meta = workflow_job("meta"); + assert!( + workflow_step_index(meta, "set_metadata") + < workflow_step_index(meta, "check_shell_contract") + ); + let upload = workflow_job("upload"); + let check = workflow_step_index(upload, "check_shell_contract"); + assert!( + check + > upload + .find("name: Download desktop module packages") + .unwrap() + ); + assert!(check < workflow_step_index(upload, "build_module_manifest")); + let assemble = workflow_job_in(BUILD_DESKTOP_MODULES_WORKFLOW, "assemble"); + assert!( + workflow_step_index(assemble, "check_shell_contract") + < workflow_step_index(assemble, "build_modules_only_manifest") + ); + } + + #[test] + fn both_desktop_workflows_share_one_concurrency_group() { + for workflow in [BUILD_DESKTOP_WORKFLOW, BUILD_DESKTOP_MODULES_WORKFLOW] { + assert!( + workflow.contains( + "\nconcurrency:\n group: desktop-${{ inputs.channel }}\n cancel-in-progress: false\n" + ), + "a release on one channel must wait for the other instead of deciding a floor beside it or cancelling its publish" + ); + } + } + #[test] fn payload_artifact_dirs_prefer_signed_windows_artifacts() { let temp = tempfile::tempdir().unwrap(); @@ -6538,6 +8186,18 @@ mod tests { } } + fn layout_compatibility() -> DesktopShellCompatibility { + DesktopShellCompatibility { + minimum_shell_version: "2026.1001.1".to_string(), + source_sha: SHA_SOURCE.to_string(), + previous_source_sha: SHA_LIVE.to_string(), + floor_source_sha: Some(SHA_BASELINE.to_string()), + decision: DesktopShellContractChoice::Raise, + contract_base_sha: SHA_BASELINE.to_string(), + contract_changes: vec!["fluxer_desktop/src/preload/index.ts".to_string()], + } + } + fn write_fake_module_payload(root: &Path) -> (PathBuf, Vec) { let packed = ["fluxer_renderer", "fluxer_sourcemaps"] .into_iter() @@ -6548,6 +8208,18 @@ mod tests { .into_iter() .map(|(platform, arch)| (platform.to_string(), arch.to_string())) .collect::>(); + let coordinates = targets + .iter() + .map(|(platform, arch)| { + ( + desktop_coordinate_key(platform, arch), + DesktopShellContractCoordinate { + live: None, + compatibility: layout_compatibility(), + }, + ) + }) + .collect(); write_desktop_channel_manifests( &payload_root, "canary", @@ -6555,6 +8227,7 @@ mod tests { LAYOUT_PUB_DATE, &packed, &targets, + &coordinates, ) .unwrap(); (payload_root, packed) @@ -6575,6 +8248,8 @@ mod tests { assert_eq!(manifest.arch, arch); assert_eq!(manifest.build_version, LAYOUT_VERSION); assert_eq!(manifest.required_modules, vec!["fluxer_renderer"]); + assert_eq!(manifest.shell.minimum_version, "0.0.0"); + assert_eq!(manifest.shell_compatibility, Some(layout_compatibility())); assert_eq!( manifest.modules.keys().collect::>(), vec!["fluxer_renderer", "fluxer_sourcemaps"] @@ -6583,6 +8258,8 @@ mod tests { let entry = &manifest.modules[&module.module]; assert_eq!(entry.sha256, module.sha256); assert_eq!(entry.bytes, module.bytes); + assert_eq!(entry.minimum_shell_version, "2026.1001.1"); + assert_eq!(entry.maximum_shell_version, None); assert_eq!( entry.url, format!( diff --git a/tools/ci/src/release.rs b/tools/ci/src/release.rs index e86e6cc5b..da931e178 100644 --- a/tools/ci/src/release.rs +++ b/tools/ci/src/release.rs @@ -660,6 +660,20 @@ struct ReleaseSummary { #[serde(rename = "draft")] is_draft: bool, published_at: Option, + #[serde(default)] + assets: Vec, +} + +#[derive(Debug, Deserialize)] +struct ReleaseAssetSummary { + name: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct DesktopPublishedRelease { + pub(crate) version: String, + pub(crate) tag: String, + pub(crate) assets: BTreeSet, } #[derive(Debug)] @@ -933,6 +947,72 @@ fn release_summaries() -> Result> { Ok(pages.into_iter().flatten().collect()) } +pub(crate) fn desktop_releases(channel: &str) -> Result> { + desktop_releases_from(&release_summaries()?, channel) +} + +fn desktop_releases_from( + summaries: &[ReleaseSummary], + channel: &str, +) -> Result> { + let prefix = format!("fluxer-desktop-{channel}@"); + let mut releases = Vec::new(); + for release in summaries.iter().filter(|release| !release.is_draft) { + let Some(version) = release.tag_name.strip_prefix(&prefix) else { + continue; + }; + let Ok(instant) = parse_version_instant(version) else { + continue; + }; + releases.push(( + instant, + DesktopPublishedRelease { + version: version.to_string(), + tag: release.tag_name.clone(), + assets: release + .assets + .iter() + .map(|asset| asset.name.clone()) + .collect(), + }, + )); + } + releases.sort_by_key(|(instant, _)| *instant); + Ok(releases.into_iter().map(|(_, release)| release).collect()) +} + +pub(crate) fn download_desktop_release_asset(tag: &str, name: &str) -> Result> { + let output = CommandSpec::new("gh") + .args(["release", "download", tag]) + .args(["--repo", RELEASE_REPOSITORY]) + .args(["--pattern", name]) + .args(["--output", "-"]) + .to_command() + .output() + .with_context(|| format!("Failed to download {name} from {tag}"))?; + ensure!( + output.status.success(), + "Failed to download {name} from {tag}: {}", + String::from_utf8_lossy(&output.stderr).trim() + ); + Ok(output.stdout) +} + +pub(crate) fn desktop_release_kind(channel: &str, version: &str) -> Result { + let tag = format!("fluxer-desktop-{channel}@{version}"); + let pattern = desktop_release_descriptor_filename(channel, version)?; + let descriptor: DesktopReleaseDescriptor = + serde_json::from_slice(&download_desktop_release_asset(&tag, &pattern)?) + .with_context(|| format!("{pattern} from {tag} is not a release descriptor"))?; + ensure!( + descriptor.channel == channel && descriptor.version == version, + "{pattern} from {tag} describes {} {}", + descriptor.channel, + descriptor.version + ); + Ok(descriptor.kind) +} + fn qualified_releases( summaries: &[ReleaseSummary], component: &str, @@ -1564,6 +1644,7 @@ mod tests { .map(|(module, entry)| (module.clone(), entry.clone())) .collect(), required_modules: vec![DESKTOP_RENDERER_MODULE.to_string()], + shell_compatibility: None, }; let release_asset = desktop_module_manifest_release_asset_name( SAMPLE_CHANNEL,